This is a simple dispatcher for executing commands in a CQRS-ish manner. In Fidget, however, everything is a command. In my day job as both a database administrator and API developer, I've found that commands frequently end up needing to return a value, and queries frequently require auditing.
Consequently, Fidget has a unified interface for defining commands, handlers, and decorators.
Once you've added the Fidget.Commander
package to your project, you'll want to familiarize yourself with the following interfaces:
ICommand<TResult>
is a marker interface for the command itself.ICommandHandler<TCommand,TResult>
defines a handler for executing the command.ICommandDecorator<TCommand,TResult>
allows you to modify a handler's behavior without modifying the handler itself.ICommandDispatcher
is implemented for you, and true to its name, it dispatches commands to be executed by their handlers.
Each of the generic types above have a variant without the TResult
argument for commands that return no result. These commands will return the Unit
structure.
As of version 3, there are abstract types to reduce the amount of typing required to implement command handlers and command decorators. CommandHandler
and CommandDecorator
types both:
- Perform argument null checking.
- Check the cancellation token.
- Eliminate the need to return
Unit.Default
on commands that return no result.
To get started, you'll need to tell your favorite DI container how to find the implementations of those interfaces.
Here's a simple registration example using the standard dependency injection container in ASP.NET Core. It is augmented using Scrutor to scan the assembly for handlers and decorators that we would otherwise have to register individually.
public void ConfigureServices( IServiceCollection services )
{
// register the commander
services.AddFidgetCommander()
// note: this example uses Scrutor for assembly scanning
.Scan( scanner => scanner.FromAssemblyOf<Startup>()
.AddClasses( _ => _.AssignableTo( typeof( ICommandHandler<,> ) ) )
.AsImplementedInterfaces()
.WithTransientLifetime()
.AddClasses( _ => _.AssignableTo( typeof( ICommandDecorator<,> ) ) )
.AsImplementedInterfaces()
.WithTransientLifetime()
);
services.AddMvc();
}
Here's a registration example using StructureMap.AspNetCore:
public void ConfigureServices( IServiceCollection services )
{
services.AddFidgetCommander();
services.AddMvc();
}
public void ConfigureContainer( Registry registry )
{
// add all your handlers and decorators
registry.Scan( scanner =>
{
scanner.AssemblyContainingType<Startup>();
scanner.ConnectImplementationsToTypesClosing( typeof( ICommandHandler<,> ) );
scanner.ConnectImplementationsToTypesClosing( typeof( ICommandDecorator<,> ) );
});
}
CommandAdapterFactory
uses the IServiceProvider
interface that most of the common DI containers support. For those that don't, a simple wrapper or a custom implementation of ICommandAdapterFactory
can be used.
Here's a simple controller, command, handler, and decorator example. It returns a hello or goodbye greeting based on HTTP method, and a decorator to add some attitude:
[Route( "" )]
public class HelloController : ControllerBase
{
/// <summary>
/// Fidget command dispatcher.
/// </summary>
readonly ICommandDispatcher Dispatcher;
/// <summary>
/// Constructs a controller that says hello.
/// </summary>
/// <param name="dispatcher">Fidget command dispatcher.</param>
public HelloController( ICommandDispatcher dispatcher )
{
Dispatcher = dispatcher ?? throw new ArgumentNullException( nameof( dispatcher ) );
}
/// <summary>
/// Command for creating a greeting.
/// </summary>
public class GreetCommand : ICommand<string>
{
/// <summary>
/// Gets or sets the name to include in the greeting.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the method used to invoke the command.
/// </summary>
public string Method { get; set; }
}
/// <summary>
/// Handles the greet command.
/// </summary>
public class GreetCommandHandler : ICommandHandler<GreetCommand,string>
{
public Task<string> Handle( GreetCommand command, CancellationToken cancellationToken )
{
var greeting = "delete".Equals( command.Method, StringComparison.OrdinalIgnoreCase )
? "Goodbye"
: "Hello";
var name = string.IsNullOrWhiteSpace( command.Name )
? "World"
: command.Name;
return Task.FromResult( $"{greeting}, {name}!" );
}
}
/// <summary>
/// Decorates the greet command.
/// </summary>
public class GreetCommandDecorator : ICommandDecorator<GreetCommand, string>
{
public async Task<string> Execute( GreetCommand command, CancellationToken cancellationToken, CommandDelegate<GreetCommand, string> continuation ) =>
$"I've been commanded to say '{await continuation( command, cancellationToken )}'!";
}
/// <summary>
/// Says hello.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="name">Optional name to include in the message.</param>
[HttpGet]
[HttpDelete]
public async Task<IActionResult> Greet( CancellationToken cancellationToken, string name = null )
{
var command = new GreetCommand
{
Name = name,
Method = HttpContext.Request.Method,
};
return Ok( await Dispatcher.Execute( command, cancellationToken ) );
}
}