How to implement CQRS in ASP.NET using MediatR. A guided example using CQRS with separate Read and Write models using Enity Framework Core for Commands and Dapper for Queries.
When people think about CQRS they often think about complex, event-driven, distributed architectures with separate Read and Write databases. Using different databases for Read and Write means we can use a Polyglot Architecture, where we pick a database that perfectly fits the problem on each side. If you don’t require mega scale, then it is generally absolutely fine to run your CQRS setup using just a single database. This makes things much simpler because we don’t have to deal with propagating changes from the Write side to the Read side or scenarios where each side becomes out of sync.
Single Database vs Different Read/Write Databases
There are lots of different options for using CQRS: Choosing a CQRS Architecture That Works for You. In this article I will be focusing on ‘Different Read/Write Models, Single Database’. The full code can be found in my GitHub repo at the end of the article.
• • •
Different Read/Write Models, Single Database
Even when using a single database we can still take advantage of CQRS and design our Commands on the Write side and our Queries on the Read side slightly differently. In this example SQL Server is used as a database.
Write Side — Create/Update/Delete data
Entity Framework Core will be used for the Write side. Entity Framework is a powerful ORM which helps encapsulate business logic and validation for writing data. We will also use Domain-Driven Design for the Write side so that business and validation logic can be encapsulated in our Write models/Entities.
Read Side — Read data
Dapper will be used on the Read side to Query data. Dapper is a lightweight Object Mapper which allows data to be read extremely efficiently. Direct SQL Queries will be used on the Read side so we can ensure that the most optimal way of reading data is used. There will be no business logic or DTO mappings slowing us down.
• • •
Command Query Responsibility Segregation (CQRS)
CQRS splits data access into Commands and Queries. Every Write or Read operation has a dedicated Command or Query, compared to more traditional patterns where larger ‘Service’ or ‘Business’ classes with lots of different methods are used.
- Commands: Write data — Create/Update/Delete
- Queries: Read data
The following shows how a Weather Forecast can be Created and Read using CQRS compared to Service-Based implementations.
CQRS vs Service-Based Data Access
Each Command and Query class has a corresponding Handler class. Commands and Queries are dispatched to their Handler using a synchronous in-process Mediator implementation using the MediatR Nuget package.
Splitting our code into granular Commands/Queries/Handlers ensures that the Single Responsibility Principle (SRP) is adhered to, which makes our solutions flexible for change and easy to test. When using Service-Based patterns our classes can quickly become large and unwieldy. For this reason even if you are not using different Read/Write models, following CQRS can make your code much cleaner and easier to maintain.
Another benefit to using CQRS is that the only dependency your code now needs is the Mediator instance. The Mediator is responsible for dispatching your Commands and Queries to their corresponding Handler.
Before we get into the nuts and bolts, let’s look at the difference between the 2 approaches when accessing data via an API Controller.
CQRS vs Service-Based at High Level
For the Service-Based approach the same WeatherForecastsService
class is reused for each endpoint, whereas for CQRS each endpoint uses a dedicated Command or Query. MediatR’s ISender
interface is used to dispatch the Commands and Queries to their retrospective Handlers. In the next section we will take a look under the hood and see how everything is implemented.
• • •
CQRS Implementation
First we will look at how the Write side is implemented using Commands via Entity Framework Core and then we will look at how the Read side is implemented using Queries via Dapper.
Commands
Commands are used to Create/Update/Delete data using our Write models. In this example Domain-Driven Design is being followed so that we can encapsulate as much Business and Validation logic as possible within our Aggregates/Entities. When I refer to Write models here, I am talking about our Entities that are stored in the database via Entity Framework.
All Commands implement the Command
base class and their Handlers implement the CommandHandler
base class. These base classes inherit from MediatR IRequest
and IRequestHandler
interfaces, which allows them to be dispatched via an ISender
.
namespace AspNetCore.Cqrs.Application.Abstractions.Commands
{
public abstract record Command : IRequest<Unit>;
public abstract class CommandHandler<TCommand> : IRequestHandler<TCommand, Unit> where TCommand : Command
{
public async Task<Unit> Handle(TCommand request, CancellationToken cancellationToken)
{
await HandleAsync(request);
return Unit.Value;
}
protected abstract Task HandleAsync(TCommand request);
}
}
Command and Command Handler Base Classes
Using records to create the Commands works well because they should be simple immutable data structures. The Command Handlers don’t return anything at all — this should generally be the case when following CQRS.
Example Command
Using a Mediator to dispatch Commands and Queries breaks dependencies when navigating through your code. That can make it difficult to quickly find the Handler implementations when looking at your consuming code. For this reason I usually put the Handler classes in the same file as the Command or Query.
Here is an example Command for creating a new Weather Forecast. In this case the WeatherForecast
class is our Write model.
namespace AspNetCore.Cqrs.Application.Weather.Commands
{
public sealed record CreateWeatherForecastCommand(Guid Id, int Temperature, DateTime Date, string? Summary, Guid LocationId) : Command;
public sealed class CreateWeatherForecastCommandHandler : CommandHandler<CreateWeatherForecastCommand>
{
private readonly IRepository<WeatherForecast> _repository;
private readonly IUnitOfWork _unitOfWork;
public CreateWeatherForecastCommandHandler(IRepository<WeatherForecast> repository,
IUnitOfWork unitOfWork) : base(unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
protected override async Task HandleAsync(CreateWeatherForecastCommand request)
{
var created = WeatherForecast.Create(request.Id,
request.Date,
Temperature.FromCelcius(request.Temperature),
request.Summary,
location.Id);
_repository.Insert(created);
await UnitOfWork.CommitAsync();
}
}
}
Example Create Weather Forecast Command
The WeatherForecast
Write model uses Domain-Driven Design principles to encapsulate validation/update logic using private setters and Guards.
namespace AspNetCore.Cqrs.Core.Weather.Entities
{
public sealed class WeatherForecast : AggregateRoot
{
private WeatherForecast(Guid id, DateTime date, Temperature temperature, string summary, Guid locationId) : base(id)
{
Date = date;
Temperature = temperature;
Summary = summary;
LocationId = locationId;
}
public static WeatherForecast Create(Guid id, DateTime date, Temperature temperature, string? summary, Guid locationId)
{
return new WeatherForecast(id, date, temperature, ValidateSummary(summary), locationId);
}
public DateTime Date { get; private set; }
public Temperature Temperature { get; private set; }
public string Summary { get; private set; }
public Guid LocationId { get; private set; }
public void Update(Temperature temperature, string summary)
{
Temperature = temperature;
Summary = ValidateSummary(summary);
}
private static string ValidateSummary(string? summary)
{
summary = (summary ?? string.Empty).Trim();
Guard.Against.NullOrEmpty(summary, nameof(Summary));
return summary;
}
}
}
Weather Forecast Write Model
The Write side uses a generic Entity Framework Core Repository implementation. EF will handle all of the complexity with storing our data. Since our Writes are less frequent, we are not as concerned with performance.
namespace AspNetCore.Cqrs.Infrastructure.Repositories
{
internal class Repository<T> : IRepository<T> where T : AggregateRoot
{
private readonly WeatherContext _context;
private readonly DbSet<T> _entitySet;
public Repository(WeatherContext context)
{
_context = context;
_entitySet = _context.Set<T>();
}
public IQueryable<T> GetAll(bool noTracking = true)
{
var set = _entitySet;
if (noTracking)
{
return set.AsNoTracking();
}
return set;
}
public async Task<T?> GetByIdAsync(Guid id)
{
return await GetAll(false)
.FirstOrDefaultAsync(e => e.Id == id);
}
public void Insert(T entity)
{
_entitySet.Add(entity);
}
public void Insert(List<T> entities)
{
_entitySet.AddRange(entities);
}
public void Delete(T entity)
{
_entitySet.Remove(entity);
}
public void Remove(IEnumerable<T> entitiesToRemove)
{
_entitySet.RemoveRange(entitiesToRemove);
}
}
}
Write Model Entity Framework Repository
Returning Data From Commands
If you are following ‘Pure’ CQRS, then no data should be returned from your Commands. There are cases where validations or metadata can be returned, however responses should not include any information about the Write model. One of the reasons for this is so that your Commands can be handled asynchronously in a fire-and-forget manner in the future if you need — in our example we are implementing an in-process Mediator so this will not be the case.
In the previous example a new Guid Id is passed into the Command so that it doesn’t need to be returned again. If you are using a database generated Identifier then this principle can be difficult to adhere to. I’d recommend just being pragmatic here — if you really think you need to return data from your Commands and it works for your team, then do it!
Queries
Queries are used to Read data. The Query and QueryHandler base classes also inherit from the MediatR interfaces so they can be dispatched via an ISender. The key difference is that Queries must specify a response type — this is the Read model that it knows how to query.
namespace AspNetCore.Cqrs.Application.Abstractions.Queries
{
public abstract record Query<T> : IRequest<T>;
public abstract class QueryHandler<TQuery, TResponse> : IRequestHandler<TQuery, TResponse> where TQuery : Query<TResponse>
{
public async Task<TResponse> Handle(TQuery request, CancellationToken cancellationToken)
{
return await HandleAsync(request);
}
protected abstract Task<TResponse> HandleAsync(TQuery request);
}
}
Query and Query Handler Base Classes
Example Query
Here is an example Query for reading a Weather Forecast. In this case the WeatherForecastReadModel
class is our Read model.
namespace AspNetCore.Cqrs.Application.Weather.Queries
{
public sealed record GetWeatherForecastQuery(Guid Id) : Query<WeatherForecastReadModel>;
public sealed class GetWeatherForecastQueryHandler : QueryHandler<GetWeatherForecastQuery, WeatherForecastReadModel>
{
private readonly IWeatherForecastsReadModelRepository _repository;
public GetWeatherForecastQueryHandler(IWeatherForecastsReadModelRepository repository)
{
_repository = repository;
}
protected override async Task<WeatherForecastReadModel> HandleAsync(GetWeatherForecastQuery request)
{
var forecast = await _repository.GetByIdAsync(request.Id);
return Guard.Against.NotFound(forecast);
}
}
}
Example Get Weather Forecast Query
Our Query Handler and Read model are super simple and lightweight. There should be no complex logic or mapping so that the Read models can be read and serialized as efficiently as possible.
namespace AspNetCore.Cqrs.Core.Weather.ReadModels
{
public sealed class WeatherForecastReadModel : ReadModel
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)Math.Round((TemperatureC / 0.5556), 0);
public string? Summary { get; set; }
public Guid LocationId { get; set; }
}
}
Weather Forecast Read Model
The main difference on the Read side is the repository implementation. A custom WeatherForecastsReadModelRepository
is used to read data using Dapper. This means that we can craft our SQL queries exactly how we want to optimise performance, without Entity Framework getting in the way. Here we are reading from the same Table that Entity Framework used for the Write models, however we could use other options if we wanted e.g. Views, Materialised Views ect.
namespace AspNetCore.Cqrs.Infrastructure.Repositories
{
public sealed class WeatherForecastsReadModelRepository : IWeatherForecastsReadModelRepository
{
private readonly DapperContext _context;
public WeatherForecastsReadModelRepository(DapperContext context)
{
_context = context;
}
public async Task<List<WeatherForecastReadModel>> GetAllAsync()
{
var query = "SELECT * FROM WeatherForecasts ORDER BY Date";
using var connection = _context.CreateConnection();
var locations = await connection.QueryAsync<WeatherForecastReadModel>(query);
return locations.ToList();
}
public async Task<List<WeatherForecastReadModel>> GetByLocationAsync(Guid locationId)
{
var query = "SELECT * FROM WeatherForecasts WHERE LocationId = @LocationId ORDER BY Date";
using var connection = _context.CreateConnection();
var locations = await connection.QueryAsync<WeatherForecastReadModel>(query, new { locationId });
return locations.ToList();
}
public async Task<WeatherForecastReadModel?> GetByIdAsync(Guid id)
{
var query = "SELECT TOP 1 * FROM WeatherForecasts WHERE Id = @Id";
using var connection = _context.CreateConnection();
var location = await connection.QueryFirstOrDefaultAsync<WeatherForecastReadModel>(query, new { id });
return location;
}
}
}
Weather Forecasts Read Model Dapper Repository
Where The Code Lives
If you are following Clean Architecture then your CQRS classes belong in the following layers:
Application
- Commands
- Command Handlers
- Queries
- Query Handlers
Core/Domain
- Write Models (Entities/Aggregates)
- Read Models
Infrastructure
- Write Model Repositories (Entity Framework implementation)
- Read Model Repositories (Dapper implementation)
Project Setup for CQRS
Wiring It Up
MediatR can be added as a Nuget package using:
dotnet add package MediatR
Registering all of your Handlers is easy using an extension method provided by MediatR. Since all of the Commands, Queries and Handlers are in the Application layer, you just need to provide any type from that project:
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(GetWeatherForecastQuery).Assembly));
Consuming Commands and Queries
Now that your code is split neatly into Commands and Queries, these can be consumed using MediatR’s ISender
or IMediator
interfaces. The mediator will match up the Command or Query with their Handler.
namespace AspNetCore.Cqrs.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public sealed class WeatherForecastsController : ControllerBase
{
private readonly ISender _mediator;
public WeatherForecastsController(ISender mediator)
{
_mediator = mediator;
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(WeatherForecastReadModel), StatusCodes.Status200OK)]
public async Task<IActionResult> Get(Guid id)
{
var forecast = await _mediator.Send(new GetWeatherForecastQuery(id));
return Ok(forecast);
}
[HttpPost]
[ProducesResponseType(typeof(CreatedResultEnvelope), StatusCodes.Status201Created)]
public async Task<IActionResult> Post([FromBody] WeatherForecastCreateDto forecast)
{
var id = Guid.NewGuid();
await _mediator.Send(new CreateWeatherForecastCommand(id, forecast.TemperatureC, forecast.Date, forecast.Summary, forecast.LocationId));
return CreatedAtAction(nameof(Get), new { id }, new CreatedResultEnvelope(id));
}
// ...
}
}
Consuming Commands and Queries
• • •
Using CQRS is a brilliant technique for breaking down complex projects and cleaning your code. If you are just getting started with CQRS then using a single database for Read and Write models is an easy way to get going so you don’t have to manage syncing data or distributed transactions.
Using Dapper on the Read side may seem unnecessary to you, it is really just a demonstration of what is possible whilst still using the same database. Even if you still want to use Entity Framework or another ORM on the Read side then there is still value in using CQRS to break down your app into more granular Commands and Queries.
All of the code from the article can be found in my GitHub: https://github.com/matt-bentley/AspNetCore.Cqrs