In my previous post, I described command DTOs and service methods for booking a table at a restaurant. Now, we just need something to interpret this command, and do something useful with it.
- Brownfield CQRS part 1 – Commands
- Brownfield CQRS part 2 – Command Handlers
- Brownfield CQRS part 3 – Queries, Parameters and Results
- Brownfield CQRS part 4 – Command Dispatcher
To do this, we create a corresponding command handler for each command:
public interface ICommandHandler where T : ICommand{ void Handle(T command);}
Command handlers are responsible for:
- Performing any required validation on the command.
- Invoking the domain — coordinating objects in the domain, and invoking the appropriate behaviour on them.
Command handlers are application services, and each execution represents a separate unit of work (e.g. a database transaction). There is only one command handler per command, because commands can only be handled once — they are not broadcast out to all interested listeners like event handlers.
Here’s an example for handling our BookTableCommand. A one-to-one handler/command mapping makes it easy to add/remove features from our service.
public class BookTableCommandHandler : ICommandHandler<BookTableCommand>{ IDinnerServiceRepository nights; public void Handle(BookTableCommand command) { var dinnerService = nights[command.TimeAndDay]; var party = new Party(command.PartySize, command.PartyName); night.TakeBooking(party); }}
Note each command implements ICommand — a simple explicit role marker interface that also allows us to use constraints on generic types and automate IoC registration of command handlers.
public interface ICommand { }
Command validation and errors
Aside from transient technical faults, there are two business reasons a command might fail:
- The command was not valid — e.g. you tried to book a table for zero people.
- The command could not succeed — e.g. the restaurant is fully booked that night.
Ideally, the client will have pre-checked these to save time, but if the command handler detects a problem, how do we report it back to the user, given commands are not allowed to have return values? How would we report success even?
Actually, this is not a problem at all — commands have no return value, but they can throw a detailed validation/command failed exception back to the client. If they didn’t throw anything, it is assumed to have succeeded.
What if you execute commands asynchronously — e.g. queued and executed at some later time? We can’t throw an exception back to the client in this case. But that’s fine — the client must always assume their asynchronous command will succeed. If it does fail, it will be reported back through some alternate channel (e.g. via e-mail or the query side). This is why it is important to pre-validate commands on the client as much as possible.