Table of Contents

Post-execution Handling for Text Commands

When developing commands, you may want to consider building a post-execution handling system so you can have finer control over commands. Discord.Net offers several post-execution workflows for you to work with.

If you recall, in the Command Guide, we have shown the following example for executing and handling commands,

public class CommandHandler
{
    private readonly DiscordSocketClient _client;
    private readonly CommandService _commands;

    // Retrieve client and CommandService instance via ctor
    public CommandHandler(DiscordSocketClient client, CommandService commands)
    {
        _commands = commands;
        _client = client;
    }
    
    public async Task InstallCommandsAsync()
    {
        // Hook the MessageReceived event into our command handler
        _client.MessageReceived += HandleCommandAsync;

        // Here we discover all of the command modules in the entry 
        // assembly and load them. Starting from Discord.NET 2.0, a
        // service provider is required to be passed into the
        // module registration method to inject the 
        // required dependencies.
        //
        // If you do not use Dependency Injection, pass null.
        // See Dependency Injection guide for more information.
        await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(), 
                                        services: null);
    }

    private async Task HandleCommandAsync(SocketMessage messageParam)
    {
        // Don't process the command if it was a system message
        var message = messageParam as SocketUserMessage;
        if (message == null) return;

        // Create a number to track where the prefix ends and the command begins
        int argPos = 0;

        // Determine if the message is a command based on the prefix and make sure no bots trigger commands
        if (!(message.HasCharPrefix('!', ref argPos) || 
            message.HasMentionPrefix(_client.CurrentUser, ref argPos)) ||
            message.Author.IsBot)
            return;

        // Create a WebSocket-based command context based on the message
        var context = new SocketCommandContext(_client, message);

        // Execute the command with the command context we just
        // created, along with the service provider for precondition checks.
        await _commands.ExecuteAsync(
            context: context, 
            argPos: argPos,
            services: null);
    }
}

You may notice that after we perform ExecuteAsync, we store the result and print it to the chat, essentially creating the most fundamental form of a post-execution handler.

With this in mind, we could start doing things like the following,

// Bad code!!!
var result = await _commands.ExecuteAsync(context, argPos, _services);
if (result.CommandError != null)
    switch(result.CommandError)
    {
        case CommandError.BadArgCount:
            await context.Channel.SendMessageAsync(
                "Parameter count does not match any command's.");
            break;
        default:
            await context.Channel.SendMessageAsync(
                $"An error has occurred {result.ErrorReason}");
            break;
    }

However, this may not always be preferred, because you are creating your post-execution logic with the essential command handler. This design could lead to messy code and could potentially be a violation of the SRP (Single Responsibility Principle).

Another major issue is if your command is marked with RunMode.Async, ExecuteAsync will always return a successful ExecuteResult instead of the actual result. You can learn more about the impact in @FAQ.Commands.General.

CommandExecuted Event

Enter CommandExecuted, an event that was introduced in Discord.Net 2.0. This event is raised whenever a command is executed regardless of its execution status. This means this event can be used to streamline your post-execution design, is not prone to RunMode.Async's ExecuteAsync drawbacks.

Thus, we can begin working on code such as:

public async Task SetupAsync()
{
    await _command.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
    // Hook the execution event
    _command.CommandExecuted += OnCommandExecutedAsync;
    // Hook the command handler
    _client.MessageReceived += HandleCommandAsync;
}
public async Task OnCommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
{
    // We have access to the information of the command executed,
    // the context of the command, and the result returned from the
    // execution in this event.

    // We can tell the user what went wrong
    if (!string.IsNullOrEmpty(result?.ErrorReason))
    {
        await context.Channel.SendMessageAsync(result.ErrorReason);
    }

    // ...or even log the result (the method used should fit into
    // your existing log handler)
    var commandName = command.IsSpecified ? command.Value.Name : "A command";
    await _log.LogAsync(new LogMessage(LogSeverity.Info, 
        "CommandExecution", 
        $"{commandName} was executed at {DateTime.UtcNow}."));
}
public async Task HandleCommandAsync(SocketMessage msg)
{
    var message = messageParam as SocketUserMessage;
    if (message == null) return;
    int argPos = 0;
    if (!(message.HasCharPrefix('!', ref argPos) || 
        message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || 
        message.Author.IsBot) return;
    var context = new SocketCommandContext(_client, message);
    await _commands.ExecuteAsync(context, argPos, _services);
}

So now we have a streamlined post-execution pipeline, great! What's next? We can take this further by using RuntimeResult.

RuntimeResult

RuntimeResult was initially introduced in 1.0 to allow developers to centralize their command result logic. In other words, it is a result type that is designed to be returned when the command has finished its execution.

However, it wasn't widely adopted due to the aforementioned ExecuteAsync drawback. Since we now have access to a proper result-handler via the CommandExecuted event, we can start making use of this class.

The best way to make use of it is to create your version of RuntimeResult. You can achieve this by inheriting the RuntimeResult class.

The following creates a bare-minimum required for a sub-class of RuntimeResult,

public class MyCustomResult : RuntimeResult
{
    public MyCustomResult(CommandError? error, string reason) : base(error, reason)
    {
    }
}

The sky is the limit from here. You can add any additional information you would like regarding the execution result.

For example, you may want to add your result type or other helpful information regarding the execution, or something simple like static methods to help you create return types easily.

public class MyCustomResult : RuntimeResult
{
    public MyCustomResult(CommandError? error, string reason) : base(error, reason)
    {
    }
    public static MyCustomResult FromError(string reason) =>
        new MyCustomResult(CommandError.Unsuccessful, reason);
    public static MyCustomResult FromSuccess(string reason = null) =>
        new MyCustomResult(null, reason);
}

After you're done creating your RuntimeResult, you can implement it in your command by marking the command return type to Task<RuntimeResult>.

Note

You must mark the return type as Task<RuntimeResult> instead of Task<MyCustomResult>. Only the former will be picked up when building the module.

Here's an example of a command that utilizes such logic:

public class MyModule : ModuleBase<SocketCommandContext>
{
    [Command("eat")]
    public async Task<RuntimeResult> ChooseAsync(string food)
    {
        if (food == "salad")
            return MyCustomResult.FromError("No, I don't want that!");
        return MyCustomResult.FromSuccess($"Give me the {food}!").
    }
}

And now we can check for it in our CommandExecuted handler:

public async Task OnCommandExecutedAsync(Optional<CommandInfo> command, ICommandContext context, IResult result)
{
    switch(result)
    {
        case MyCustomResult customResult:
            // do something extra with it
            break;
        default:
            if (!string.IsNullOrEmpty(result.ErrorReason))
                await context.Channel.SendMessageAsync(result.ErrorReason);
            break;
    }
}

CommandService.Log Event

We have so far covered the handling of various result types, but we have not talked about what to do if the command enters a catastrophic failure (i.e., exceptions). To resolve this, we can make use of the CommandService.Log event.

All exceptions thrown during a command execution are caught and sent to the Log event under the LogMessage.Exception property as a CommandException type. The CommandException class allows us to access the exception thrown, as well as the context of the command.

public async Task LogAsync(LogMessage logMessage)
{
    if (logMessage.Exception is CommandException cmdException)
    {
        // We can tell the user that something unexpected has happened
        await cmdException.Context.Channel.SendMessageAsync("Something went catastrophically wrong!");

        // We can also log this incident
        Console.WriteLine($"{cmdException.Context.User} failed to execute '{cmdException.Command.Name}' in {cmdException.Context.Channel}.");
        Console.WriteLine(cmdException.ToString());
    }
}