Microservice with CQRS in ASP.NET Core
What is CQRS?
CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates the read and writes operations of a data source. Here Query refers to querying data from a source and Command refers to database command that can be either Insert/Update or Delete operations.
So the main idea with CQRS is to allow an application to work with different models. In traditional architectures, the same data model is used to query and update data sources. But with CQRS you can have different data models for update/insert and query which provides flexibility for complex scenarios. Even in more complex applications like microservices or any kind of application that has high demand of data consumption, in that case the traditional architecture doesn’t fit well because you will stick with the same data source and having much writing and reading in the same data source can affect the performance of the system. In that scenario, we can think about CQRS because it gives us the flexibility to use a separate data source that ensures the scalability of our application.
Advantages of CQRS
- Independent scaling
- Optimized data transfer objects
- Provides separation of concern
- High scalability
Disadvantages of CQRS
- More complexity especially in bigger systems because often you have reads which also update some data, for example, a user logs in (read) and you want to store its IP and time of login (write)
- Eventual consistency when using a database for writing and one for reading. The read database needs to be synchronized to hold the new data. This could take a while.
- Not applicable in all projects: CQRS brings some complexity to your system and especially simple applications that do only basic CRUD operations shouldn’t use CQRS.
Implementation of the CQRS in our ProductMicroservice
Here for the Implementation of the CQRS, we will use the MediatR Library that helps the implementation of Mediator Pattern in .NET. The mediator is a behavioral pattern that let us reduce dependencies between objects by restricting direct communications between the objects and forces them to collaborate only via a mediator object.
Open the solution: ProductMicroservice-DotNET-Core-Master and in project: ProductMicroservice
add a new folder: CQRS and under this add two folders: Commands and Queries.
in this project install install the MediatR , AutoMapper and MediatR.Extension.Microsoft.DependencyInjection from NuGet Package Manager. as we will add the dependency of the MediatR and AutoMapper in the startup class of the this project
the NuGet Package Manager.
Now let’s do the CreateProductCommand operation which is an insert operation so this will be our command. so in the Commands folder create a class CreateProductCommand.cs file.
In the Commands folder create Interface class: CreateProductCommand.cs as following:
using MediatR;
using ProductMicroservice.Models;
namespace ProductMicroservice.CQRS.Commands
{
public class CreateProductCommand : IRequest<Product>
{
public Product Product { get; set; }
}
}
As this is also a request so it will implement the IRequest interface. And for this command, we also have to create a handler. The concrete class CreateProductCommandHandler as following:
using MediatR;
using ProductMicroservice.Models;
using ProductMicroservice.Repository;
using System.Threading;
using System.Threading.Tasks;
namespace ProductMicroservice.CQRS.Commands
{
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Product>
{
private readonly IProductRepository _ProductRepository;
public CreateProductCommandHandler(IProductRepository productRepository)
{
_ProductRepository = productRepository;
}
public async Task<Product> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
return await Task.FromResult(_ProductRepository.InsertProduct(request.product));
}
}
}
As we see in the code above, CreateProductCommandHandler is calling Product repository to access the data ( database) which is an insert to the database. now we should change the code in the Conroller that can call to the CreateProductCommand then it shall be a Madiator (CQRS plays as a midiator roll here) and that is because the code for Post in controller should be changed as following:
[HttpPost]
public async Task<ActionResult<Product>> Post([FromBody] Product product)
{
try
{
var prod = new CreateProductCommand
{
product = _mapper.Map<Product>(product)
};
return await _mediator.Send(prod);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
In the Queries folder create GetProductLisQuery Interface as following:
using System;
using System.Collections.Generic;
using MediatR;
using ProductMicroservice.Models;
namespace ProductMicroservice.CQRS.Queries
{
public class GetProductLisQuery : IRequest<IEnumerable<Product>>
{
}
}
And a concrete class to the above Interface as a Handeller GetProductByIdQueryHandler as following:
using System;
using System.Threading.Tasks;
using ProductMicroservice.Repository;
using ProductMicroservice.Models;
using System.Threading;
using MediatR;
using System.Collections.Generic;
namespace ProductMicroservice.CQRS.Queries
{
public class GetProductListQueryHandler : IRequestHandler<GetProductLisQuery, IEnumerable<Product>>
{
private readonly IProductRepository _productRepository;
public GetProductListQueryHandler(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<IEnumerable<Product>> Handle(GetProductLisQuery request, CancellationToken cancellationToken)
{
return await Task.FromResult( _productRepository.GetProducts());
}
}
}
Here the handler must implement the IRequestHandler interface where we need to pass two generics the first one is the request name that the handler will process (for our example as we will handle GetProductLisQuery so we are passing this) and in the second generic we will pass the output of the request(as this request will return a product.
As a result we should change the Get() action in the controller should be changed to call the CQRS handeller ( GetProductListQueryHandler)
The code for this Get() Action in the Controller should be as folling:
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> Get()
{
try
{
var products = await _mediator.Send(new GetProductLisQuery());
return new OkObjectResult(products);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
I have created CQRS Handeller for all functions in the Controller and change all action methods in the Controller to call CQRS handeller methods.
Adding dependency
Now in the MicroserviceProject inside the startup.cs at ConfigureServices() method register the services for MediatR. and AutoMapper as following:
services.AddMediatR(Assembly.GetExecutingAssembly()); services.AddAutoMapper(typeof(Startup));
I have modified small changes in the Repository to adapt to the CQRS.
All Completed code can be find in the GitHub.
Conclusion
In this post we gone through overview of CQRS and how it can be used to separate the read and write operations and how to implement it in the ASP.net Core in application. In my ProductMicroservice I have implemented CQRS for all the Action methods in the Controller.
In my Next post I will describe Mediator Pattern in ASP.NET Core
This post is part of “Microservices-Step by step”.