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();
Generic Commands & Handlers
Generic commands & handlers require a bit of special handling. Say for example, you have a generic command type and a generic handler that's supposed to handle that generic command such as the following:
//command
public class MyCommand<T> : ICommand<IEnumerable<T>> { ... }
//handler
public class MyCommandHandler<T> : ICommandHandler<MyCommand<T>, IEnumerable<T>> { ... }
In order to make this work, you need to register the association between the two with open generic types like so:
app.Services.RegisterGenericCommand(typeof(MyCommand<>), typeof(MyCommandHandler<>));
Once registered, it's business as usual and you can execute generic commands such as this:
var results = await new MyCommand<SomeType>().ExecuteAsync();
var results = await new MyCommand<AnotherType>().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.