Implementing CQRS with MediatR in .NET6

Implementing CQRS with MediatR in .NET6

CQRS and the Mediator Pattern

The MediatR library was built to facilitate two primary software architecture patterns: CQRS and the Mediator pattern. Whilst similar, let’s spend a moment understanding the principles behind each pattern.

CQRS

CQRS stands for “Command Query Responsibility Segregation”. As the acronym suggests, it’s all about splitting the responsibility of commands (saves) and queries (reads) into different models.

If we think about the commonly used CRUD pattern (Create-Read-Update-Delete), usually we have the user interface interacting with a datastore responsible for all four operations. CQRS would instead have us split these operations into two models, one for the queries (aka “R”), and another for the commands (aka “CUD”).

The following image illustrates how this works:

No alt text provided for this image

As we can see, the Application simply separates the query and command models. The?CQRS pattern makes no formal requirements of how this separation occurs. It could be as simple as a separate class in the same application (as we’ll see shortly with MediatR), all the way up to separate physical applications on different servers. That decision would be based on factors such as scaling requirements and infrastructure, so we won’t go into that decision path today.

The key point being is that to create a CQRS system, we just need to?split the reads from the writes.

The problem with traditional architectural patterns is that the same data model or DTO is used to query as well as update a data source. This can be the go-to approach when your application is related to just CRUD operations and nothing more. But when your requirements suddenly start getting complex, this basic approach can prove to be a disaster.

In practical applications, there is always a mismatch between the read and write forms of data, like the extra properties you may require to update. Parallel operations may even lead to data loss in the worst cases. That means, you will be stuck with just one Data Transfer Object for the entire lifetime of the application unless you choose to introduce yet another DTO, which in-turn may break your application architecture.

The idea with CQRS is to allow an application to work with different models. Long story short, you have one model that has data needed to update a record, another model to insert a record, yet another to query a record. This gives you flexibility with varying and complex scenarios. You don’t have to rely on just one DTO for the entire CRUD Operations by implementing CQRS.

Pros of CQRS

There are quite of lot of advantages on using the CQRS Pattern for your application. Few of them are as follows.

Optimised Data Transfer Objects

Thanks to the segregated approach of this pattern, we will no longer need those complex model classes within our application. Rather we have one model per data operation that gives us all the flexibility in the world.

Highly Scalable

Having control over the models in accordance with the type of data operations makes your application highly scalable in the long run.

Improved Performance

Practically speaking there are always 10 times more Read Operations as compared to the Write Operation. With this pattern you could speed up the performance on your read operations by introducing a cache or NOSQL Db like Redis or Mongo. CQRS pattern will support this usage out of the box, you would not have to break your head trying to implement such a cache mechanism.

Secure Parallel Operations

Since we have dedicated models per oprtation, there is no possibility of data loss while doing parellel operations.

Cons of CQRS

Added Complexity and More Code

The one thing that may concern a few programmers is that this is a code demanding pattern. In other words, you will end up with at least 3 or 4 times more code-lines than you usually would. But everything comes for a price. This according to me is a small price to pay while getting the awesome features and possibilities with the pattern.

Implementing CQRS Pattern in .NET6 WebApi

Let’ s build a .NET6 WebApi to showcase the implementation and better understand the CQRS Pattern. I will push the implemented solution over to Github, you can find the link to my repository at the end of this post. Let us build an API endpoint that does CRUD operations for a Product Entity, ie, Create / Delete / Update / Delete product record from the Database. Here, I use Entity Framework Core as the ORM to access data from my local DataBase.

Setting up the Project

Open up Visual Studio and Create a new ASP.NET Core Web Application with the WebApi Template.

Installing the required Packages

Install these following packages to your API project via the Package Manager Console. Just Copy Paste the below lines over to your Package Manager Console. All the required packages get installed. We will explore these packages as we progress.

Install-Package Microsoft.EntityFrameworkCor
Install-Package Microsoft.EntityFrameworkCore.Relational
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
Install-Package Swashbuckle.AspNetCore
Install-Package Swashbuckle.AspNetCore.Swaggere        

Adding the Product, BaseEntity Model and IAuditEntity

Since we are following a code first Approach, let’s design our data models. Add a?Models?Folder and create a new class named Product with the following properties.

public interface IAuditEntit
? ? {
? ? ? ? DateTime CreatedAt { get; set; }


? ? ? ? DateTime ModifiedAt { get; set; }
? ? }

public class BaseEntit
? ? {
? ? ? ? public Guid Id { get; set; }
? ? }
public class Product : BaseEntity, IAuditEntit
? ? {
? ? ? ? public string Name { get; set; }


? ? ? ? public string Barcode { get; set; }


? ? ? ? public bool IsActive { get; set; }?


? ? ? ? public string Description { get; set; }


? ? ? ? public decimal Rate { get; set; }


? ? ? ? public decimal BuyingPrice { get; set; }


? ? ? ? public string ConfidentialData { get; set; }


? ? ? ? public DateTime CreatedAt { get; set; }


? ? ? ? public DateTime ModifiedAt { get; set; }
? ? }        

Adding the Context Class and Interface

Make a new Folder called Context and add a class named Application Context. This particular class will help us to access the data using Entity Framework Core ORM.

public class ProductDbContext : DbContext, IProductDbContex
? ? {
? ? ? ? public ProductDbContext(DbContextOptions<ProductDbContext> options)
? ? ? ? ? ? : base(options)
? ? ? ? {
? ? ? ? }

? ? ? ? public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
? ? ? ? {
? ? ? ? ? ? // Get all the entities that inherit from AuditableEntity
? ? ? ? ? ? // and have a state of Added or Modified
? ? ? ? ? ? var entries = ChangeTracker
? ? ? ? ? ? ? ? .Entries()
? ? ? ? ? ? ? ? .Where(e => e.Entity is IAuditEntity && (
? ? ? ? ? ? ? ? ? ? e.State == EntityState.Added
? ? ? ? ? ? ? ? ? ? || e.State == EntityState.Modified));


? ? ? ? ? ? // For each entity we will set the Audit properties
? ? ? ? ? ? foreach (var entityEntry in entries)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? // If the entity state is Added let's set
? ? ? ? ? ? ? ? // the CreatedAt and CreatedBy properties
? ? ? ? ? ? ? ? if (entityEntry.State == EntityState.Added)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ((IAuditEntity)entityEntry.Entity).CreatedAt = DateTime.Now;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? else
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? // If the state is Modified then we don't want
? ? ? ? ? ? ? ? ? ? // to modify the CreatedAt and CreatedBy properties
? ? ? ? ? ? ? ? ? ? // so we set their state as IsModified to false
? ? ? ? ? ? ? ? ? ? Entry((IAuditEntity)entityEntry.Entity).Property(p => p.CreatedAt).IsModified = false;
? ? ? ? ? ? ? ? }


? ? ? ? ? ? ? ? // In any case we always want to set the properties
? ? ? ? ? ? ? ? // ModifiedAt and ModifiedBy
? ? ? ? ? ? ? ? ((IAuditEntity)entityEntry.Entity).ModifiedAt = DateTime.Now;
? ? ? ? ? ? }

? ? ? ? ? ? return base.SaveChangesAsync(cancellationToken);
? ? ? ? }


? ? ? ? public DbSet<Product> Products { get; set; }
? ? }        

Configuring the API Services to support Entity Framework Core

Navigate to your API Project’s Startup class. This is the class where the Application knows about various services and registrations required. Let’s add the support for EntityFrameworkCore. Just add these lines to your Program Class. This will register the EF Core with the application.

builder.Services.AddDbContext<ProductDbContext>(options =
? ? options.UseSqlServer(
? ? ? ? builder.Configuration.GetConnectionString("DefaultConnection"),
? ? ? ? b => b.MigrationsAssembly(typeof(ProductDbContext).Assembly.FullName)));>        

Defining the Connection String in appsettings.json

We will need to connect a data source to the API. For this, we have to define a connection string in the appsettings.json found within the API Project. For demo purposes, I am using LocalDb Connection. You could scale it up to support multiple Database type. Here is what you would add to your appsettings.json.

"ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=developmentDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  },        

Configuring MediatR

We have already installed the required package to our application. To register the library, add this line to the our API Program class in the.

builder.Services.AddMediatR(cfg =
? ? cfg.RegisterServicesFromAssemblies(Assembly.GetExecutingAssembly()
? ? ));        

Implementing the CRUD Operations

CRUD essentially stands for Create, Read, Update, and Delete. These are the Core components of RESTFul APIs. Let’s see how we can implement them using our CQRS Approach. Create a Folder named Features in the root directory of the Project and subfolders for the Queries and Command.

No alt text provided for this image

Queries

Here is where we will wire up the queries, ie, GetAllProducts and GetProductById. Make 2 Classes under the ProductFeatures / Queries Folder and name them GetAllProductsQuery and GetProductByIdQuery

Query to Get All Products

public class GetAllProductsQuery : IRequest<IEnumerable<Product>
? ? {
? ? ? ? public class GetAllProductQueryHandler : IRequestHandler<GetAllProductsQuery, IEnumerable<Product>>
? ? ? ? {
? ? ? ? ? ? private readonly IProductDbContext _productDbContext;


? ? ? ? ? ? public GetAllProductQueryHandler(IProductDbContext productDbContext)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? _productDbContext = productDbContext;
? ? ? ? ? ? }


? ? ? ? ? ? public async Task<IEnumerable<Product>> Handle(GetAllProductsQuery request, CancellationToken cancellationToken)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? var productList = await _productDbContext.Products.ToListAsync(cancellationToken: cancellationToken);


? ? ? ? ? ? ? ? if (productList != null)
? ? ? ? ? ? ? ? ? ? return productList.AsReadOnly();


? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? }
? ? ? ? }        

Query to Get Product By Id

public class GetProductByIdQuery : IRequest<Product
? ? {
? ? ? ? public Guid Id { get; set; }

? ? ? ? public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product>
? ? ? ? {
? ? ? ? ? ? private readonly IProductDbContext _context;

? ? ? ? ? ? public GetProductByIdQueryHandler(IProductDbContext dbContext)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? _context = dbContext;
? ? ? ? ? ? }

? ? ? ? ? ? public async Task<Product> Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? var product = _context.Products.FirstOrDefault(a => a.Id == request.Id);

? ? ? ? ? ? ? ? if (product is not null)
? ? ? ? ? ? ? ? ? ? return product;

? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? }
? ? ? ? }
? ? }        

Commands

Add the following classes to ProductFeatures / Commands.

1.CreateProductCommand

2.DeleteProductByIdCommand

3.UpdateProductCommand

then add the following models to models folder:

1.CreateProductCommand

2.DeleteProductByIdCommand

3.UpdateProductCommand

Command to Create a New Product Handler

?public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Guid
? ? {
? ? ? ? private readonly IProductDbContext _context;

? ? ? ? public CreateProductCommandHandler(IProductDbContext productDbContext)
? ? ? ? {
? ? ? ? ? ? _context = productDbContext;
? ? ? ? }

? ? ? ? public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
? ? ? ? {
? ? ? ? ? ? var product = new Product();
? ? ? ? ? ? product.Barcode = request.Barcode;
? ? ? ? ? ? product.Name = request.Name;
? ? ? ? ? ? product.BuyingPrice = request.BuyingPrice;
? ? ? ? ? ? product.Rate = request.Rate;
? ? ? ? ? ? product.Description = request.Description;
? ? ? ? ? ? _context.Products.Add(product);
? ? ? ? ? ? await _context.SaveChangesAsync();
? ? ? ? ? ? return product.Id;
? ? ? ? }
? ? }        

Command to Delete a Product By Id Handler

?public class DeleteProductByIdCommandHandler : IRequestHandler<DeleteProductByIdCommand, Guid
? ? {
? ? ? ? private readonly IProductDbContext _context;

? ? ? ? public DeleteProductByIdCommandHandler(IProductDbContext context)
? ? ? ? {
? ? ? ? ? ? _context = context;
? ? ? ? }

? ? ? ? public async Task<Guid> Handle(DeleteProductByIdCommand request, CancellationToken cancellationToken)
? ? ? ? {
? ? ? ? ? ? var product = await _context.Products.Where(a => a.Id == request.Id).FirstOrDefaultAsync();

? ? ? ? ? ? if (product == null) return default;

? ? ? ? ? ? _context.Products.Remove(product);

? ? ? ? ? ? await _context.SaveChangesAsync();

? ? ? ? ? ? return product.Id;
? ? ? ? }
? ? }>        

Command to Update a Product Handler

public class UpdateProductCommandHandler : IRequestHandler<UpdateProductCommand, Guid
{
? ? private readonly IProductDbContext _context;

? ? public UpdateProductCommandHandler(IProductDbContext context)
? ? {
? ? ? ? _context = context;
? ? }

? ? public async Task<Guid> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
? ? {
? ? ? ? var product = _context.Products.FirstOrDefault(a => a.Id == request.Id);


? ? ? ? if (product == null)
? ? ? ? {
? ? ? ? ? ? return default;
? ? ? ? }
? ? ? ? else
? ? ? ? {
? ? ? ? ? ? product.Barcode = request.Barcode;
? ? ? ? ? ? product.Name = request.Name;
? ? ? ? ? ? product.BuyingPrice = request.BuyingPrice;
? ? ? ? ? ? product.Rate = request.Rate;
? ? ? ? ? ? product.Description = request.Description;
? ? ? ? ? ? await _context.SaveChangesAsync();
? ? ? ? ? ? return product.Id;
? ? ? ? }
? ? }        

Create Product Command

?public class CreateProductCommand : IRequest<Guid
? ? {
? ? ? ? public string Name { get; set; }
? ? ? ? public string Barcode { get; set; }
? ? ? ? public string Description { get; set; }
? ? ? ? public decimal BuyingPrice { get; set; }
? ? ? ? public decimal Rate { get; set; }
? ? }        

Delete Product Command

? ?public class DeleteProductByIdCommand : IRequest<Guid
? ? {
? ? ? ? public Guid Id { get; set; }
? ? }        

Update Product Command

public class UpdateProductCommand : IRequest<Guid
? ? {
? ? ? ? public Guid Id { get; set; }
? ? ? ? public string Name { get; set; }
? ? ? ? public string Barcode { get; set; }
? ? ? ? public string Description { get; set; }
? ? ? ? public decimal BuyingPrice { get; set; }
? ? ? ? public decimal Rate { get; set; }
? ? }        

Product Controller

public class ProductController : ControllerBas
? ? {
? ? ? ? private readonly IMediator _mediatr;

? ? ? ? public ProductController(IMediator mediator)
? ? ? ? {
? ? ? ? ? ? _mediatr = mediator;
? ? ? ? }

? ? ? ? [HttpPost]
? ? ? ? public async Task<IActionResult> Create(CreateProductCommand command)
? ? ? ? {
? ? ? ? ? ? await _mediatr.Send(command);


? ? ? ? ? ? return Ok();
? ? ? ? }

? ? ? ? [HttpGet]
? ? ? ? public async Task<ActionResult> GetProducts()
? ? ? ? {
? ? ? ? ? ? var products = await _mediatr.Send(new GetAllProductsQuery());
? ? ? ? ? ? return Ok(products);
? ? ? ? }

? ? ? ? [HttpGet("{id}")]
? ? ? ? public async Task<IActionResult> GetById(Guid id)
? ? ? ? {
? ? ? ? ? ? var product = await _mediatr.Send(new GetProductByIdQuery { Id = id });


? ? ? ? ? ? return Ok(product);
? ? ? ? }

? ? ? ? [HttpDelete("{id}")]
? ? ? ? public async Task<IActionResult> Delete(Guid id)
? ? ? ? {
? ? ? ? ? ? await _mediatr.Send(new DeleteProductByIdCommand { Id = id });


? ? ? ? ? ? return Ok();
? ? ? ? }

? ? ? ? [HttpPut("{id}")]
? ? ? ? public async Task<IActionResult> Update(Guid id, UpdateProductCommand command)
? ? ? ? {
? ? ? ? ? ? if (id != command.Id)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return BadRequest();
? ? ? ? ? ? }

? ? ? ? ? ? await _mediatr.Send(command);

? ? ? ? ? ? return Ok();
? ? ? ? }
? ? }        

Summary

We have covered CQRS implementation and definition, Mediator pattern and MediatR Library, Entity Framework Core Implementation, Swagger Integration, and much more. If I missed out on something or was not clear with the guide, let me know in the comments.

Source Code at Github

The finished source code of this implementation is available at Github.


Do follow me at GitHub as well

Hadi M.

Learning C# & ASP.NET Core

11 个月

To clarify my previous question: Is it possible to create [Model] class (input and output) in separate class library and reference them to all other projects? In this way, [Command] class can inherit from [DTO+IRequest] together.

回复
Hadi M.

Learning C# & ASP.NET Core

11 个月

Thanks. question about Models: 1- In [CommandHandlers] & [QueryHandlers] we have [Request Class] which is inherited from [IRequest]. 2- [Commands & Queries] class play a role like DTO which are used by handler class as input parameter. 3- [Queries] return a model and generally shall be different from [Domain] classes. If it is identical to [Domain] class, we create similar class (DTO) but don't expose domain class to upper layer. 4- So far, we have classes (Like Model) for commands (input) & queries (input) & DTO (output). 5- Item 1~4 can be used in API project and API project can be execute in server. 6- But, Client is a different project (WebApplication/WindowsForm/Android/…) and for sending/receiving data to/from API we need to have [Input Model] for command & query and get data in DTO format.? Question: As mentioned above, is it suggested to define new command/query/DTO classes in different project (Not in Application/Infrastructure layers) to reference them to [Client] project so, we do not have to create extra and identical classes? Is it accepted?

回复
Chia Karimi

.NET Developer| ASP.NET MVC| .NET CORE| Micro Service| Docker| React| C#

2 年

要查看或添加评论,请登录

Chia Karimi的更多文章

  • Fluent Validation in .NET 6

    Fluent Validation in .NET 6

    The Problem Data Validation is extremely vital for any Application. The GO-TO Approach for Model validation in any .

    1 条评论
  • Zen coding

    Zen coding

    ?? ????? ???? ?? ???? Zen coding https://www.smashingmagazine.

社区洞察

其他会员也浏览了