CQRS with MediatR and Autofac in .NET 6
CQRS is an application architecture pattern and stands for Command query responsibility segregation. My intent with this article is not to go through the architecture pattern and explain the details involved. Martin Fowler1 and Microsoft2 have already done that, and I am not repeating it here. The purpose of this article is to show how you can leverage CQRS with MediatR and Autofac in .NET 6.
What is MediatR?
MediatR is a library that was created by Jimmy Boggard3 based on the Mediator pattern4, which promotes loose coupling by keeping objects from referring to each other explicitly.
MediatR deals with two kinds of messages it dispatches:
- Request/response messages, dispatched to a single handler.
With requests and responses we implement Command to perform actions and Queries to retrieve information.
In this article we have Queries and Commands: GetProductQuery
, GetProductsQuery
, AddOrUpdateProductCommand
and DeleteProductCommand
.
- Notification messages, dispatched to multiple handlers.
It works basically as a broadcast, where the notification is sent to multiple listeners that wish to be notified.
In this article, we have Notifications available on PublishProductNotify
.
Why Autofac?
When you think about MediatR, the Inversion of Control (IoC) container that comes handy is Autofac.
MediatR works really well with Autofac due to the extension libraries available. In this particular article, we use MediatR.Extensions.Autofac.DependencyInjection
, which helps connecting MediatR to Autofac.
Autofac fits well in any solutions where you need dependency injection.
If you want to explore a different IoC, the Microsoft dependency injection container for instance, check this other article.
How does it work?
Looking at the solution we have an API with a Product Controller and a Class Library that has the Command, Query and Notification implementations.
This solution is available on PlayGoKids repository
To start let's check the Program.cs
, which is responsible to initialize and link all the dependencies of the solution:
using Autofac;
using Autofac.Extensions.DependencyInjection;
using CQRSAndMediatrSampleApplication.Product;
using MediatR.Extensions.Autofac.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Call UseServiceProviderFactory on the Host sub property
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>
{
containerBuilder.RegisterMediatR(typeof(Program).Assembly);
containerBuilder.RegisterModule<ProductModule>();
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
In particular with Autofac and MediatR, libraries work really well together to simplify how to IoC a .NET application, and how it handles modules and the MediatR registration itself.
The ProductController.cs
contains all endpoints using HTTP verbs, and they use MediatR
for all Queries, Commands and Notification:
using CQRSAndMediatrSampleApplication.Product.Command;
using CQRSAndMediatrSampleApplication.Product.Dto;
using CQRSAndMediatrSampleApplication.Product.Notify;
using CQRSAndMediatrSampleApplication.Product.Query;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace CQRSAndMediatrSampleApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class ProductController : Controller
{
private readonly IMediator _mediator;
public ProductController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("product")]
public async Task<IActionResult> Get(string productSKu)
{
var result = await _mediator.Send(new GetProductQuery() {Sku = productSKu});
return Ok(result);
}
[HttpGet("products")]
public async Task<IActionResult> GetAll()
{
var result = await _mediator.Send(new GetProductsQuery());
return Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] ProductDto product)
{
// Create product
var result = await _mediator.Send(new AddOrUpdateProductCommand() {ProductDto = product});
// Notify consumers
await _mediator.Publish(new PublishProductNotify() {Message = $"Product {product.Sku} created"});
return Ok(result);
}
[HttpPut]
public async Task<IActionResult> Update([FromBody] ProductDto product)
{
// Update product
var result = await _mediator.Send(new AddOrUpdateProductCommand() { ProductDto = product });
// Notify consumers
await _mediator.Publish(new PublishProductNotify() { Message = $"Product {product.Sku} updated" });
return Ok(result);
}
[HttpDelete]
public async Task<IActionResult> Remove(string productSku)
{
// Remove product
var result = await _mediator.Send(new DeleteProductCommand() {Sku = productSku});
// Notify consumers
await _mediator.Publish(new PublishProductNotify() { Message = $"Product {productSku} removed" });
return Ok(result);
}
[HttpPost("notify")]
public async Task<IActionResult> NotifyAsync(string message)
{
await _mediator.Publish(new PublishProductNotify() {Message = message});
return Ok();
}
}
}
Note that on Create
, Update
and Remove
we also notify consumers. Depending on how solutions are integrated, you need to notify other parts (consumers) about the actions executed via Publish
methods in MediatR.
To handle in-memory data, the following ProductsInMemory.cs
library was created to help with data:
using CQRSAndMediatrSampleApplication.Product.Dto;
namespace CQRSAndMediatrSampleApplication.Product.Data
{
public class ProductsInMemory
{
private readonly List<ProductDto> _productList;
public ProductsInMemory()
{
_productList = new List<ProductDto>()
{
new ProductDto()
{
Id = 1,
Name = "Milk",
Sku = "MILK",
Quantity = 10,
DateCreated = DateTime.Now,
DateModified = DateTime.Now
},
new ProductDto()
{
Id = 2,
Name = "Coffee",
Sku = "COFFEE",
Quantity = 10,
DateCreated = DateTime.Now,
DateModified = DateTime.Now
},
new ProductDto()
{
Id = 3,
Name = "Toast",
Sku = "TOAST",
Quantity = 10,
DateCreated = DateTime.Now,
DateModified = DateTime.Now
},
new ProductDto()
{
Id = 4,
Name = "Butter",
Sku = "BUTTER",
Quantity = 10,
DateCreated = DateTime.Now,
DateModified = DateTime.Now
}
};
}
public List<ProductDto> ProductDtos => _productList;
}
}
I was thinking about breakfast when I wrote this article. :D
Queries
It is important to observe that IRequest
and IRequestHandler
are available on the same file. This makes our dev life easier, as you can more easily drill down to the references and code implementation when you need to go to the Declaration or Implementation.
using CQRSAndMediatrSampleApplication.Product.Data;
using CQRSAndMediatrSampleApplication.Product.Dto;
using MediatR;
namespace CQRSAndMediatrSampleApplication.Product.Query
{
public class GetProductQuery : IRequest<ProductDto>
{
public string Sku { get; set; }
}
public class GetProductQueryHandler : IRequestHandler<GetProductQuery, ProductDto>
{
private readonly ProductsInMemory _productsInMemory;
public GetProductQueryHandler()
{
_productsInMemory = new ProductsInMemory();
}
public Task<ProductDto> Handle(GetProductQuery request, CancellationToken cancellationToken)
{
var product = _productsInMemory.ProductDtos.FirstOrDefault(p => p.Sku.Equals(request.Sku));
if (product == null)
{
throw new InvalidOperationException("Invalid product");
}
return Task.FromResult(product);
}
}
}
using CQRSAndMediatrSampleApplication.Product.Data;
using CQRSAndMediatrSampleApplication.Product.Dto;
using MediatR;
namespace CQRSAndMediatrSampleApplication.Product.Query
{
public class GetProductsQuery : IRequest<List<ProductDto>> {}
public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, List<ProductDto>>
{
private readonly ProductsInMemory _productsInMemory;
public GetProductsQueryHandler()
{
_productsInMemory = new ProductsInMemory();
}
public Task<List<ProductDto>> Handle(GetProductsQuery request, CancellationToken cancellationToken)
{
var products = _productsInMemory.ProductDtos;
return Task.FromResult(products);
}
}
}
Commands
Imagine if you had some business logic rules to apply when executing Commands, below is just an example of how you could do it:
using CQRSAndMediatrSampleApplication.Product.Data;
using CQRSAndMediatrSampleApplication.Product.Dto;
using MediatR;
namespace CQRSAndMediatrSampleApplication.Product.Command
{
public class AddOrUpdateProductCommand : IRequest<bool>
{
public ProductDto ProductDto { get; set; }
}
public class AddOrUpdateProductCommandHandler : IRequestHandler<AddOrUpdateProductCommand, bool>
{
private readonly ProductsInMemory _productsInMemory;
public AddOrUpdateProductCommandHandler()
{
_productsInMemory = new ProductsInMemory();
}
public Task<bool> Handle(AddOrUpdateProductCommand request, CancellationToken cancellationToken)
{
var existingProduct =
_productsInMemory.ProductDtos.FirstOrDefault(p => p.Sku.Equals(request.ProductDto.Sku));
if (existingProduct != null)
{
var index = _productsInMemory.ProductDtos.FindIndex(p => p.Sku.Equals(request.ProductDto.Sku));
_productsInMemory.ProductDtos[index] = request.ProductDto;
return Task.FromResult(true);
}
_productsInMemory.ProductDtos.Add(request.ProductDto);
return Task.FromResult(true);
}
}
}
using CQRSAndMediatrSampleApplication.Product.Data;
using MediatR;
namespace CQRSAndMediatrSampleApplication.Product.Command
{
public class DeleteProductCommand : IRequest<bool>
{
public string Sku { get; set; }
}
public class DeleteProductCommandHandler : IRequestHandler<DeleteProductCommand, bool>
{
private readonly ProductsInMemory _productsInMemory;
public DeleteProductCommandHandler()
{
_productsInMemory = new ProductsInMemory();
}
public Task<bool> Handle(DeleteProductCommand request, CancellationToken cancellationToken)
{
var existingProduct =
_productsInMemory.ProductDtos.FirstOrDefault(p => p.Sku.Equals(request.Sku));
if (existingProduct != null)
{
var result = _productsInMemory.ProductDtos.Remove(existingProduct);
return Task.FromResult(result);
}
throw new InvalidOperationException("Invalid product");
}
}
}
Notification
The notification follows a similar implementation by using INotification
and INotificationHandler
. Again, keep them on the same file to make your life easier.
using MediatR;
namespace CQRSAndMediatrSampleApplication.Product.Notify
{
public class PublishProductNotify : INotification
{
public string Message { get; set; }
}
public class PublishProductNotifyMessageHandler : INotificationHandler<PublishProductNotify>
{
public Task Handle(PublishProductNotify notification, CancellationToken cancellationToken)
{
//TODO: Send message
Console.WriteLine($"Message: {notification.Message}");
return Task.CompletedTask;
}
}
public class PublishProductNotifyTextHandler : INotificationHandler<PublishProductNotify>
{
public Task Handle(PublishProductNotify notification, CancellationToken cancellationToken)
{
//TODO: Send text
Console.WriteLine($"Text: {notification.Message}");
return Task.CompletedTask;
}
}
}
Running the solution
Run the solution and open Swagger to perform the operations:
When you execute Create, Update and Remove you can see the notifications:
Using MediatR makes CQRS much easier to be implemented and digested in solutions.
Make sure you create a logical hierarchy in your solution, (using the example I shared) as this implementation requires a methodic approach. A common understanding of the team to work on it is also needed, as it is not trivial for developers to jump on it without knowing the pattern.
- Martin Fowler CQRS Martin Fowler CQRS↩
- Command query responsibility segregation CQRS↩
- Jimmy Bogard Jimmy Bogard↩
- Mediator Pattern Mediator Pattern↩