Search
K
  1. Command Bus

In-Process Command Bus Pattern

Similarly to the Event Bus, you can take a decoupled, command driven approach with the distinction that a command can only have a single handler which may or may not return a result. Whereas an event can have many handlers and they cannot return results back to the publisher.

1. Define A Command

This is the data contract that will be handed to the command handler. Mark the class with either the ICommand or ICommand<TResult> interface in order to make any class a command. Use the former if no result is expected and the latter if a result is expected back from the handler.

public class GetFullName : ICommand<string>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

2. Define A Command Handler

This is the code that will be executed when a command of the above type is executed. Implement either the ICommandHandler<TCommand, TResult> or ICommandHandler<TCommand> interface depending on whether a result needs to be returned or not.

public class FullNameHandler : ICommandHandler<GetFullName, string>
{
    public Task<string> ExecuteAsync(GetFullName command, CancellationToken ct)
    {
        var result = command.FirstName + " " + command.LastName;
        return Task.FromResult(result);
    }
}

3. Execute The Command

Simply call the ExecuteAsync() extension method on the command object.

var fullName = await new GetFullName()
{
    FirstName = "john",
    LastName = "snow"
}
.ExecuteAsync();

Manipulating Endpoint Error State

By implementing command handlers using the CommandHandler<> abstract types instead of the interfaces mentioned above, you are able to manipulate the validation/error state of the endpoint that issued the command like so:

GetFullNameEndpoint.cs
public class GetFullNameEndpoint : EndpointWithoutRequest<string>
{
    ...

    public override async Task HandleAsync(CancellationToken c)
    {
        AddError("an error added by the endpoint!");

        //command handler will be adding/throwing it's own validation errors
        Response = await new GetFullName
        {
            FirstName = "yoda",
            LastName = "minch"
        }.ExecuteAsync();
    }
}
FullNameHandler.cs
public class FullNameHandler : CommandHandler<GetFullName, string>
{
    public override Task<string> ExecuteAsync(GetFullName cmd, CancellationToken ct = default)
    {
        if (cmd.FirstName.Length < 5)
            AddError(c => c.FirstName, "first name is too short!");

        if (cmd.FirstName == "yoda")
            ThrowError("no jedi allowed here!");

        ThrowIfAnyErrors();

        return Task.FromResult(cmd.FirstName + " " + cmd.LastName);
    }
}

In this particular case, the client will receive the following error response:

json
{
  "statusCode": 400,
  "message": "One or more errors occured!",
  "errors": {
    "GeneralErrors": [
      "an error added by the endpoint!",
      "no jedi allowed here!"
    ],
    "FirstName": [ "first name is too short!" ]
  }
}

Dependency Injection

Dependencies in command handlers can be resolved as described here.


© FastEndpoints 2023