As you may have noticed in the code, I chose the same pattern to build the commands as I did with the CQS sample, so we have a class per use case containing two nested classes: Command and Handler. This structure makes for very clean code when you have a 1-on-1 relationship between the command class and its handler.Using the MediatR request/response model, the command (or query) becomes a request and must implement the IRequest<TResponse> interface. The handlers must implement the IRequestHandler<TRequest, TResponse> interface. Instead, we could implement the IRequest and IRequestHandler<TRequest> interfaces for a command that returns nothing (void).
More options are part of MediatR, and the documentation is complete enough to dig deeper yourself.
Let’s analyze the anatomy of the AddStocks use case. Here is the old code as a reference:
namespace Core.Services;
public class StockService
{
private readonly IProductRepository _repository;
// Omitted constructor
public async Task<int> AddStockAsync(int productId, int amount, CancellationToken cancellationToken)
{
var product = await _repository.FindByIdAsync(productId, cancellationToken);
if (product == null)
{
throw new ProductNotFoundException(productId);
}
product.AddStock(amount);
await _repository.UpdateAsync(product, cancellationToken);
return product.QuantityInStock;
}
// Omitted RemoveStockAsync method
}
The first difference is that we moved the loose parameters (highlighted) into the Command class, which encapsulates the whole request:
public class Command : IRequest<int>
{
public int ProductId { get; set; }
public int Amount { get; set; }
}
Then the Command class specifies the handler’s expected return value by implementing the IRequest<TResponse> interface, where TResponse is an int. That gives us a typed response when sending the request through MediatR. This is not “pure CQS” because the command handler returns an integer representing the updated QuantityInStock. However, we could call that optimization since executing one command and one query would be overkill for this scenario (possibly leading to two database calls instead of one).I’ll skip the RemoveStocks use case to avoid repeating myself, as it follows the same pattern. Instead, let’s look at the consumption of those use cases. I omitted the exception handling to keep the code streamlined and because try/catch blocks would only add noise to the code in this case and hinder our study of the pattern:
app.MapPost(“/products/{productId:int}/add-stocks”, async (
int productId,
AddStocks.Command command,
IMediator mediator,
CancellationToken cancellationToken) =>
{
command.ProductId = productId;
var quantityInStock = await mediator.Send(command, cancellationToken);
var stockLevel = new StockLevel(quantityInStock);
return Results.Ok(stockLevel);
});
app.MapPost(“/products/{productId:int}/remove-stocks”, async (
int productId,
RemoveStocks.Command command,
IMediator mediator,
CancellationToken cancellationToken) =>
{
command.ProductId = productId;
var quantityInStock = await mediator.Send(command, cancellationToken);
var stockLevel = new StockLevel(quantityInStock);
return Results.Ok(stockLevel);
});
// Omitted code
public record class StockLevel(int QuantityInStock);
In both delegates, we inject an IMediator and a command object (highlighted). We also let ASP.NET Core inject a CancellationToken, which we pass to MediatR. The model binder loads the data from the HTTP request into the objects that we send using the Send method of the IMediator interface (highlighted). Then we map the result into the StockLevel DTO before returning its value and an HTTP status code of 200 OK. The StockLevel record class is the same as before.This example contains almost the same code as our CQS example, but we used MediatR instead of manually programming the pieces.
The default model binder cannot load data from multiple sources. Because of that, we must inject productId and assign its value to the command.ProductId property manually. Even if both values could be taken from the body, the resource identifier of that endpoint would become less exhaustive (no productId in the URI).
With MVC, we could create a custom model binder.
With minimal APIs, we could create a static BindAsync method to manually do the model binding, which is not very extensible and would tightly couple the Core assembly with the HttpContext. I suppose we will need to wait for .NET 9+ to get improvements into that field.
I’ve left a few links in the further reading section relating to this.
Leave a Reply