Event Sourcing on Azure – part 3: command validation
Hi All! Welcome back for the third part of the Event Sourcing on Azure series. Today we’ll see how we can do some easy validation on a Command before triggering its execution.
Last time we saw how we can use CosmosDB and ServiceBus to store the events for our Aggregates. It’s not a full solution and there are still some gray areas, but I think we covered most of the ground.
However, every respectable application needs to validate the data before processing and storing it. We can’t just create a new Customer in our system because somebody told us to. What if we got bad input data?
I wrote already about Command Validation in CQRS in the past. Well, that was almost 4 years ago. It’s ancient, but the idea stands still.
For SuperSafeBank I decided to take a more agile approach and start by adding validation code directly in the Command Handlers. Why? Well because I want to keep things simple, that’s it.
So, let’s go back to our Create Customer command:
public class CreateCustomer { public Guid CustomerId { get; } public string FirstName { get; } public string LastName { get; } public string Email { get; } }
For the sake of the example, let’s say that the only thing we want to make sure is that the email address is unique across the system. We are not doing any validation on the format though.
Command Validation should make sure the business rules are satisfied. The other “basic” concerns like ranges, formats and so on, should be handled before it by creating the proper Value Objects.
The Command includes also a pre-populated Customer ID. We don’t want to rely on the Persistence layer to give it back to us because CQRS Commands should be almost fire-and-forget. Command execution won’t return any result. It’s either they work or they immediately throw.
But we need an ID back, so an option would be to generate a random GUID when we create the Command:
[HttpPost] public async Task<IActionResult> Create(CreateCustomerDto dto, CancellationToken cancellationToken = default) { if (null == dto) return BadRequest(); var command = new CreateCustomer(Guid.NewGuid(), dto.FirstName, dto.LastName, dto.Email); await _commandHandler.Process(command, cancellationToken); return CreatedAtAction("GetCustomer", new { id = command.Id }, command); }
Now, another thing we need is a Customer Emails service. Something basic, responsible of just storing emails and checking if one exists already:
public interface ICustomerEmailsService { Task<bool> ExistsAsync(string email); Task CreateAsync(string email, Guid customerId); }
We’ll write an implementation based on CosmosDB, using the Email address as Partition Key.
The final step is to connect the dots and add the validation to the Command handler:
public class CreateCustomerHandler : INotificationHandler<CreateCustomer> { private readonly IEventsService<Customer, Guid> _eventsService; private readonly ICustomerEmailsService _customerEmailsRepository; public async Task Handle(CreateCustomer command, CancellationToken cancellationToken) { if (await _customerEmailsRepository.ExistsAsync(command.Email)){ var error = new ValidationError(nameof(CreateCustomer.Email), $"email '{command.Email}' already exists"); throw new ValidationException("Unable to create Customer", error); } var customer = new Customer(command.Id, command.FirstName, command.LastName, command.Email); await _eventsService.PersistAsync(customer); await _customerEmailsRepository.CreateAsync(command.Email, command.Id); } }
Since we’re nice people, we can configure our system to capture ValidationExceptions and return them to the user in the proper format. Andrew Lock wrote a very good post about Problem Details, showing how to leverage a Middleware to handle them.
Now, in an ideal world this could be enough. But what happens if an error occurs when we store the customer email? We already have persisted the events, but not saving the email means that we might get past the validation with the same address. This will result in two customers with the same email being created, which would break our business rules.
So how can we handle this? One option is to add Transaction support and rollback the whole Handler execution if things go south. For more details, you can take a look at the Two-Phase-Commit technique or the Outbox Pattern.
The next time we’ll see what happens to the Aggregate Events once a Command is executed.
Ciao!