Implementing CQRS Pattern in ASP.NET Core 6.0


Last Updated: 5/1/2022

CQRS

Definition

  • CQRS stands for Command and Query Responsibility Segregation. Command represents Insert, Update and Delete operations. Query represent Read operations
  • A pattern that separates read and update operations for a data store

Problems

In Simple application same data model is used to query and update a database.

In complex applications, the application may perform different read queries returning data transfer objects (DTO) of different shapes. Because of this object mapping gets complicated.

For update operation, the model may implement complex validation and business logic. As a result, you can end up with an overly complex model that does too much.

Because of this you may face following problems:

  • Mismatch between the read and write representations of the data.
  • Can have a negative effect on performance due to load on the data store and data access layer, and the complexity of queries required to retrieve information.
  • Managing security and permissions can become complex, because each entity is subject to both read and write operations, which might expose data in the wrong context.

Benefits

  • Implementing CQRS in your application can maximize its performance, scalability, and security.
  • The flexibility created by migrating to CQRS allows a system to better evolve over time and prevents update commands from causing merge conflicts at the domain level.

When to use

  • Scenarios where the system is expected to evolve over time and might contain multiple versions of the model, or where business rules change regularly.
  • Scenarios where performance of data reads must be fine-tuned separately from performance of data writes, especially when the number of reads is much greater than the number of writes.
  • Task-based user interfaces where users are guided through a complex process as a series of steps or with complex domain models.

When not to use

  • The domain or the business rules are simple.
  • A simple CRUD-style user interface and data access operations are sufficient.

Implementing CQRS using Mediator in ASP.NET Core

To implement CQRS in ASP.NET you will use another design pattern Mediator

Mediator

The Mediator design pattern defines an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.

MediatR is an open source library that provides simple mediator implementation in .NET

Create Project

  • Create new ASP.NET Core Web API Project using .Net 6

Add MediatR Package

  • Install Nuget Packages using package manager console

    • Install-Package MediatR
    • Install-Package MediatR.Extensions.Microsoft.DependencyInjection
  • In Program.cs add

using MediatR;
builder.Services.AddMediatR(typeof(Program).Assembly);

Add Product Model

  • Add new folder Models
  • Create Product.cs inside Models folder
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Add Product Repository

  • Add new folder Repositories
  • Create ProductRepository.cs in Repositories. Here the products are stored in-memory list for simplicity. You can use EF Core to store in database
public class ProductRepository
{
    public List<Product> dataStore = new List<Product>() {
        new Product { Id = 1, Name = "Butter", Price = 40 },
        new Product { Id = 2, Name = "Bread", Price = 50 },
        new Product { Id = 3, Name = "Milk", Price = 30 }
    };

    public async Task<IEnumerable<Product>> GetAll()
    {
        return await Task.FromResult(dataStore.AsEnumerable());
    }
}
  • Extract Interface IProductRepository
public interface IProductRepository
{
    Task<IEnumerable<Product>> GetAll();
}
  • In Program.cs register IProductRepository Dependency
builder.Services.AddTransient<IProductRepository, ProductRepository>();

Implement Query Get All Products

  • Create new file GetAllProductsQuery.cs in Models folder. Add marker interface IRequest from MediatR to represent the request with a response
public class GetAllProductsQuery: IRequest<IEnumerable<Product>> { }
  • Add new folder Handlers
  • Create new file GetAllProductsQueryHandler.cs in Handlers folder. Implement interface IRequestHandler to define the handler for the above request.
public class GetAllProductsQueryHandler : IRequestHandler<GetAllProductsQuery, IEnumerable<Product>>
{
    private readonly IProductRepository productRepository;

    public GetAllProductsQueryHandler(IProductRepository productRepository)
    {
        this.productRepository = productRepository;
    }

    public async Task<IEnumerable<Product>> Handle(GetAllProductsQuery request, CancellationToken cancellationToken)
    {
        return await productRepository.GetAll();
    }
}

Send Query through Mediator

  • CreateProductController.cs in Controllers folder. Inject mediator and call Send method to send the request. The mediator invokes the handle method in the associated request handler
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
    private readonly IMediator mediator;

    public ProductController(IMediator mediator)
    {
        this.mediator = mediator;
    }

    [HttpGet("GetAll")]
    public async Task<IEnumerable<Product>> GetAll()
    {
        return await mediator.Send(new GetAllProductsQuery());
    }
}

Implementing Query Get Product By Id

  • In ProductRepository.cs add new method
public async Task<Product> GetById(int id)
{
    return await Task.FromResult(dataStore.Single(x => x.Id == id));
}
  • In ProductRepostiory.cs add interface method
Task<Product> GetById(int id);
  • Create GetProductByIdQuery.cs in Models folder
public class GetProductByIdQuery: IRequest<Product>
{
    public int Id { get; set; }
}
  • Create GetProductByIdQueryHandler.cs in Handlers folder
public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product>
 {
     private readonly IProductRepository productRepository;

     public GetProductByIdQueryHandler(IProductRepository productRepository)
     {
         this.productRepository = productRepository;
     }

     public async Task<Product> Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
     {
         return await productRepository.GetById(request.Id);
     }
 }
  • In ProductController.cs add method
[HttpGet("{id}")]
public async Task<Product> GetById(int id)
{
	return await mediator.Send(new GetProductByIdQuery{ Id = id });
}

Implement Command Add Product

  • In ProductRepository.cs add method
public async Task<Product> Add(Product product)
{
    dataStore.Add(new Product
    {
        Id = dataStore.Count() + 1,
        Name = product.Name,
        Price = product.Price
    });
    return await Task.FromResult(dataStore.Last());
}
  • Add method to interface
Task<Product> Add(Product product);
  • Create model AddProductCommand. Here the model contains the necessary properties for creating a product.
 public class AddProductCommand: IRequest<Product>
 {
     public string Name { get; set; }
     public decimal Price { get; set; }
 }
  • Create handler AddProductCommandHandler
public class AddProductCommandHandler : IRequestHandler<AddProductCommand, Product>
{
    private readonly IProductRepository productRepository;

    public AddProductCommandHandler(IProductRepository productRepository)
    {
        this.productRepository = productRepository;
    }
    public async Task<Product> Handle(AddProductCommand request, CancellationToken cancellationToken)
    {
        return await productRepository.Add(new Product { Name = request.Name, Price = request.Price });
    }
}
  • In ProductController.cs add method
[HttpPost]
public async Task<Product> Add(AddProductCommand product)
{
    return await mediator.Send(product);
}

Implement Update Product Command

ProductRepository.cs

public async Task<Product> Update(int id, Product product)
{
    var entity = await GetById(id);
    entity.Price = product.Price;
    return entity;
}

Update IProductRepository

Task<Product> Update(int id, Product product);

UpdateProductCommand.cs

public class UpdateProductCommand : IRequest<Product>
{
    public int Id { get; set; }
    public decimal Price { get; set; }
}

UpdateProductCommandHandler .cs

public class UpdateProductCommandHandler : IRequestHandler<UpdateProductCommand, Product>
{
   private readonly IProductRepository productRepository;

   public UpdateProductCommandHandler(IProductRepository productRepository)
   {
       this.productRepository = productRepository;
   }
   public async Task<Product> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
   {
       return await productRepository.Update(request.Id, new Product { Id = request.Id, Price = request.Price });
   }
}

ProductController.cs

[HttpPut("{id}")]
public async Task<Product> Update(int id, UpdateProductCommand product)
{
    return await mediator.Send(product);
}

Implement Delete Product Command

ProductRepository.cs

public async Task<Product> Delete(int id)
{
    var entity = await GetById(id);
    dataStore.Remove(entity);   
    return entity;
}

Update IProductRepository

Task<Product> Delete(int id);

DeleteProductCommand.cs

public class DeleteProductCommand: IRequest<Product>
{
    public int Id { get; set; }
}

DeleteProductCommandHandler.cs

public class DeleteProductCommandHandler : IRequestHandler<DeleteProductCommand, Product>
{
    private readonly IProductRepository productRepository;

    public DeleteProductCommandHandler(IProductRepository productRepository)
    {
        this.productRepository = productRepository;
    }

    public async Task<Product> Handle(DeleteProductCommand request, CancellationToken cancellationToken)
    {
        return await productRepository.Delete(request.Id);
    }
}

ProductController.cs

[HttpDelete("{id}")]
public async Task<Product> Delete(int id)
{
    return await mediator.Send(new DeleteProductCommand { Id = id});
}

Source Code

expeo-in/AspNetCoreWebApiCQRSSample: ASP.NET Core Web API CQRS Implementation Sample (github.com)

References: