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:
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();
}
}
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:
{
"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.