Package Description
The "Command Dispatch Pattern" is a great way to keep your code SOLID. This library attempts to give real-world applications a simple way to implement and benefit from the Command Pattern while staying out of the way so that developers can implement the rest of the application as they see fit.
To read more on the Command Dispatch Pattern, check out this great blog article. https://olvlvl.com/2018-04-command-dispatcher-pattern
dotnet add AcklenAvenue.Commands
In order for command handlers to be available to the dispatcher at runtime, you need to provide a list of those command handlers to an instance of the dispatcher and then make sure that dispatcher instance is the same that is used to dispatch commands later on.
Command handlers have their own dependencies (just enough to carry out their orders). But that can get hairy very quickly. We recommend you use reflection to find all the command handlers in your appDomain and then register them with IoC. Of course, you'll need to register the dependencies as well. Lastly, you'll want to register the command dispatcher itself. In a perfect world, your command dispatcher will instantiate with a complete list of command handlers, ready to roll.
Here's an example of how to do that in the Startup.cs
file that is used in ASP.NET Core to bootstrap the application:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add the concrete command dispatcher to IoC.
services.AddSingleton<ICommandDispatcher, WorkerCommandDispatcher>();
// If you want to queue you commands and defer processing to another worker,
// use the QueuedCommandDispatcher and implement the ICommandQueuePublisher
// interface.
// Somehow add all the command handlers to IoC so that they will be available
// when the dispatcher is instantiated.
RegisterAllCommandHandlers(services);
}
static void RegisterAllCommandHandlers(IServiceCollection services)
{
// Here, we're using the "TypeScanner" nuget package as a helper.
var typeScanner = new TypeScanner();
var commandHandlerTypes = typeScanner.GetTypesOf<ICommandHandler>();
commandHandlerTypes.ForEach(x => services.AddSingleton(x));
}
API Endpoints: An endpoint should take in whatever information is needed and then map that to an appropriate command to dispatch. Ideally, POST, PUT, and DELETE endpoints should return simple HTTP response confirming the receipt of the message. If you return any result of the command being carried out, you might be inadvertently ruling out the possibility of sending your commands to a queue and processing them asyncronously.
Commands: These objects should be simple "data bags". We recommend the name of the command be something imperative like "TakeOffShoes" or "TakeOutTheTrash". The object should be immutable and not contain any logic or functionality. A good way to think of a command is to consider that it will soon be serialized to JSON and then deserialized back to a C# class. If that's not going to work, then you have something wrong in your command object.
Command Handlers: A command handler is the code that will carry out the orders contained in the command object. If the command is "TakeOutTheTrash" (along with the ID of the trash can), then we would have a "TrashTakerOuter" command handler that will do/invoke the work. An imaginary, yet typical, workflow might be to communicate with the repository to pull the trashCan entity, interact with the trashCan, generate some domain events, and then persist changes back to the repository. In many cases, the command handler will live in the same server, app domain, and codebase as the command and the API. But, if the other pieces of the puzzle are done properly, the command handler could even be in another microservice, codebase or even language.
Here's an example of a typical workflow from HTTP request to final processing in a command handler:
API/Controllers/TrashController.cs
[Route("api/[controller]")]
[ApiController]
public class TrashController : ControllerBase
{
readonly ICommandDispatcher _commandDispatcher;
public TrashController(ICommandDispatcher commandDispatcher)
{
_commandDispatcher = commandDispatcher;
}
[HttpPost]
public async void Post([FromBody] string trashCanId)
{
await _commandDispatcher.Dispatch(new TakeOutTheTrash(trashCanId));
}
}
Domain/Commands/TakeOutTheTrash.cs
public class TakeOutTheTrash
{
public string TrashCanId { get; }
public TakeOutTheTrash(string trashCanId)
{
TrashCanId = trashCanId;
}
}
Domain/Commands/Handlers/TrashTakerOuter.cs
public class TrashTakerOuter : IHandler<TakeOutTheTrash>
{
readonly IRepository<TrashCan> _trashCanRepo;
public TrashTakerOuter(IRepository<TrashCan> trashCanRepo)
{
_trashCanRepo = trashCanRepo;
}
public async Task Handle(TakeOutTheTrash command)
{
var trashCan = await _trashCanRepo.get(command.TrashCanId);
trashCan.TakeOutTrash();
await _trashCanRepo.save(trashCan);
}
}
Acklen Avenue welcomes contributions from the community. Please create an issue describing the problem you are solving and submit a corresponding pull request with your solution.