Back in May 2023 I'd been working a lot with a content management system (CMS) that used it's own implementation of the Command Query Responsibility Segregation (CQRS) pattern quite heavily. I found it quite easy to work with and also that the code was easy to maintain as a system grew. I had some other projects coming up which weren't CMS based and thought that they could also benefit from the CQRS pattern so set about developing a CQRS library that my company could use in its own projects.
Over the last two years this library has been developed, improved, actively maintained and used in a number of my company's projects. The library has been open source from the start, is available on Github and as a Nuget package.
A lot of developers have been using MediatR to implement the CQRS pattern. I had thought about doing this myself, however MediatR is a lot more than CQRS, it's an implementation of the mediator pattern, and it's request and response model can be used for command and query separation. Milan Jovanović has written a great article on the difference between CQRS and MediatR. It's well worth a read and please do consider supporting him.
With the recent announcement that MediatR is going commercial it's likely that some developers will be looking for an alternative. If you're currently using MediatR solely for CQRS or you're looking for an easy way to implement CQRS in your application then this library may well be a viable option.
What does the library do?
The library provides a simple way to implement Command Query Responsibility Segregation (CQRS) in an application. Commands are defined by implementing the ICommand
interface and queries by implementing the IQuery
interface. Handlers can then be used to process these commands and queries. Handlers implement the ICommandHandler
and IQueryHandler
interfaces. To simplify things, an executor ICommandExecutor
/IQueryExecutor
is registered with the DI container and this takes a command/query and passes it to the correct handler.
Installation
The library can be installed from Nuget: PurplePiranha.Cqrs.Core
.
To register the required services with the DI container call the AddCqrs()
extension method.
services
.AddCqrs();
Queries
Defining a Query
public record GetProductsByCategoryQuery(int CategoryId) : IQuery<IEnumerable<ProductModel>>;
Handling the Query
public class GetProductsByCategoryQuery :
IQueryHandler<GetProductsByCategoryQuery, IEnumerable<ProductModel>>
{
public async Task<Result<int>> ExecuteAsync(
GetProductsByCategoryQuery query,
CancellationToken cancellationToken = default
)
{
var products = await dbContext
.Set<Product>()
.Where(p => p.CategoryId = query.CategoryId)
.Select(p => new ProductModel {
Id = p.Id,
Name = p.Name,
Description = p.ShortDescription,
Price = p.Price
})
.ToListAsync(cancellationToken);
return Result.SuccessResult(products);
}
}
Executing the query (e.g. from an API controller with IQueryExecutor
injected as _queryExecutor
)
var query = new GetProductsByCategoryQuery(401);
var result = await _queryExecutor.ExecuteAsync(query);
Commands
Defining a Command
public record UpdatePriceCommand(int ProductId, int Price) : ICommand;
Handling the Command
public class UpdatePriceCommandHandler : ICommandHandler<UpdatePriceCommand>
{
public async Task<Result> ExecuteAsync(
UpdatePriceCommand command,
CancellationToken cancellationToken = default
)
{
var product = await dbContext
.Set<Product>()
.Where(p => p.Id= command.ProductId)
.SingleOrDefaultAsync(cancellationToken);
if (product is null)
return Result.FailureResult(new ProductDoesNotExistFailure(command.ProductId));
product.Price = command.Price;
dbContext.Update(product);
await dbContext.SaveChangesAsync();
return Result.SuccessResult();
}
}
Executing the command (e.g. from an API controller with ICommandExecutor
injected as _commandExecutor
)
var command= new UpdatePriceCommand(1095, 299);
var result = await _commandExecutor.ExecuteAsync(command);
Optional Libraries: Command & Query Validation and Permissions
There are optional libraries to perform validation of command/query inputs and also to check whether the caller has permission to execute the command/query. These are documented in the project wiki.
Note: Regarding the use of the Result Pattern
The library implements the result pattern by using the Purple Piranha's Open Source Fluent Results library when returning from a command or query handler. Whilst not necessary for the implementation of CQRS it provides an easy way to control flow based on whether a command/query executed successfully. Both the optional libraries (validation and permissions) require the use of this library in order to pass back a failure result should validation or the permission check fail. This means that the core library also has to implement it.
Wrapping up
The CQRS library from Purple Piranha provides an easy way to implement the Command Query Responsibility Segregation pattern in .NET. It's supported for .NET versions that are currently supported by Microsoft (.NET 8 and .NET 9, at time of writing). Please follow the links below for more information.