This content originally appeared on Telerik Blogs and was authored by Assis Zang
CQRS is a well-known architectural pattern that can solve many problems encountered in complex scenarios. Check out in this blog post how this pattern works and how to implement it in an ASP.NET Core application.
There are some situations where it is necessary to separate reading and writing functions, mainly in complex scenarios or that demand great scalability of resources. Imagine an ecommerce site where the data is read several times by a customer while they browse the site—however, writing only begins once the user adds an item to the cart. In this scenario, reading requires many more resources than writing. In traditional models, it is difficult to handle this in a simple way, as the reading and writing functions are processed in the same way.
Happily, there are intelligent solutions that solve these and other problems. One of the best known is the CQRS architectural pattern. Check out in this blog post what CQRS is and how to implement it in an ASP.NET Core application.
What Is CQRS?
CQRS stands for Command and Query Responsibility Segregation, an architectural pattern for software development.
In CQRS, data-read operations are separated from data-write or update operations. This separation occurs in the interface or class where the read and write functions are kept.
Some of the advantages of using CQRS are:
- Separate teams can implement the operations.
- Write operations are much less used than reading operations (like that ecommerce site where you spend hours browsing the website and in just a moment you put the items in the cart), so it is possible to scale the resources according to the need.
- Every operation can have its own security as per the requirements.
The term Command Query Separation (CQS), which gave rise to CQRS, was defined by Bertrand Meyer in his book Object-Oriented Software Construction. In it, two well-defined layers are separated from each other:
- Queries: Queries just return a state and do not change it.
- Commands: Commands only change the state.
So CQRS (introduced by Greg Young) is based on CQS but is more detailed.
Why Use CQRS?
It is common to find in modern and old systems traditional architectural patterns that use the same data model or DTO to query and persist/update data. When the system only uses a simple CRUD, this can be a great approach, but as the system grows and becomes complex it can become a real disaster.
In these scenarios, reading and writing have incompatibilities with each other, such as properties that are needed to update but should not be returned in queries. This difference can lead to data loss and, at best, break the architectural design of the application.
Therefore, the main objective of CQRS is to allow an application to work correctly using different data models, offering flexibility in scenarios that require a complex model. You have the possibility to create multiple DTOs without breaking any architectural pattern or losing any data in the process.
Applying CQRS in a .NET Application
Next, we will implement CQRS in a .NET application. So, to create the project, just follow the next steps.
Creating the Application
Prerequisites:
- .NET 6 SDK
- Fiddler Everywhere (to test the app)
You can access the repository with the source code used in the examples here: source code. (Note: This post was written with .NET 6, but .NET 7 is now available!)
First of all, let’s create the base application that will be a Minimal API, add the SQLite dependencies, and run the Migrations commands to create the database.
Then, follow the steps below:
Creating the project in Visual Studio
- Create new project
- Choose ASP.NET Core Web API
- Name: ProductCatalog
- Next
- Choose .NET 6 (LTS)
- Uncheck the option “Use controllers”
- Create
Creating via Terminal
dotnet new web -o ProductCatalog
Project Dependencies
Following are the project dependencies. You can add them in the file “ProductCatalog.csproj” or via NuGet.
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Creating the Model Entity
Let’s create the Product entity model. Add a new folder called “Models” and inside it add the following class:
Product
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ProductCatalog.Models;
public class Product
{
public int Id { get; set; }
[StringLength(80, MinimumLength = 4)]
public string? Name { get; set; }
[StringLength(80, MinimumLength = 4)]
public string? Description { get; set; }
[StringLength(80, MinimumLength = 4)]
public string? Category { get; set; }
public bool Active { get; set; } = true;
[Column(TypeName = "decimal(10,2)")]
public decimal Price { get; set; }
}
Creating the Database Context
Let’s create the database context. Add a new folder called “Data” and inside it add the following class:
ProductDBContext
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Models;
namespace ProductCatalog.Data;
public class ProductDBContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) =>
options.UseSqlite("DataSource=products.db;Cache=Shared");
}
And finally, in the Program.cs archive, add the following code to configure the context class.
builder.Services.AddDbContext<ProductDBContext>();
Running EF Core Commands
You can run the commands below in a project root terminal.
dotnet ef migrations add InitialModel
dotnet ef database update
Alternatively, run the following commands from the Package Manager Console in Visual Studio:
Add-Migration InitialModel
Update-Database
Now that the base application and the database are ready, we can apply the CQRS pattern to implement the CRUD methods, separating the query from the persistence.
But to help with this implementation, there is a very important feature called mediator. Check below what the mediator does.
The Mediator Pattern
The mediator pattern uses a very simple concept that perfectly fulfills its role: Provide a mediator class to coordinate the interactions between different objects and thus reduce the coupling and dependency between them.
In short, mediator makes a bridge between different objects, which eliminates the dependency between them as they do not communicate directly.
The diagram below demonstrates how the mediator works, indirectly linking objects A, B and C.
Pros:
- Independence between different objects
- Centralized communication
- Easy maintenance
Cons:
- Greater complexity
- It can become a bottleneck in an application if there is a large amount of data being processed
Implementing the Mediator Pattern With MediatR
MediatR is a library created by Jimmy Bogard (also creator of AutoMapper) that helps in implementing the Mediator Pattern.
This library provides ready-made interfaces that serve as a mediating class for communication between objects—so when using MediatR, we don’t need to implement any of these classes, just use the resources available in MediatR.
To add MediatR to the project, just add the code below to the project’s dependencies or download it via Visual Studio’s NuGet Package.
<PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
Create a new folder called “Resources” and inside it create two new folders “Commands” and “Queries.”
Creating the Queries
Next, CQRS is used through the implementation of the query pattern composed of two objects:
- Query – Defines the objects to be returned.
- Query Handler – Responsible for returning objects defined by the class that implements the query pattern.
Get Product by Id
Inside the Queries folder, create the following class:
GetProductByIdQuery
using MediatR;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Queries;
public class GetProductByIdQuery : IRequest<Product>
{
public int Id { get; set; }
}
Here we define a class that returns a Product object. Through it, we send a request to the mediator that will execute the query.
The next class will execute the query and return the product, so inside the Queries folder adds the class below:
GetProductByIdQueryHandler
using MediatR;
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Data;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Queries;
public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product>
{
private readonly ProductDBContext _context;
public GetProductByIdQueryHandler(ProductDBContext context)
{
_context = context;
}
public async Task<Product> Handle(GetProductByIdQuery request, CancellationToken cancellationToken) =>
await _context.Products.FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);
}
Get All Products
Inside the Queries folder, create the class below:
GetAllProductsQuery
using MediatR;
using Microsoft.EntityFrameworkCore;
using ProductCatalog.Data;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Queries;
public class GetAllProductsQueryHandler : IRequestHandler<GetAllProductsQuery, IEnumerable<Product>>
{
private readonly ProductDBContext _context;
public GetAllProductsQueryHandler(ProductDBContext context)
{
_context = context;
}
public async Task<IEnumerable<Product>> Handle(GetAllProductsQuery request, CancellationToken cancellationToken) =>
await _context.Products.ToListAsync();
}
Creating the Commands
Next, CQRS is used through the implementation of the command pattern composed of two objects:
- Command – Defines which methods should be executed.
- Command Handler – Responsible for executing the methods defined by the Command classes.
The commands will execute the Create/Update/Delete persistence methods.
All command classes implement the IRequest<T>
interface, where the type of data to be returned is specified. This way, MediatR knows which ver
object is invoked during a request.
So, inside the “Commands” folder, create a new folder called “Create” and inside it, create the classes below:
CreateProductCommand
using MediatR;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Commands.Create;
public class CreateProductCommand : IRequest<Product>
{
public string? Name { get; set; }
public string? Description { get; set; }
public string? Category { get; set; }
public bool Active { get; set; } = true;
public decimal Price { get; set; }
}
CreateProductCommandHandler
using MediatR;
using ProductCatalog.Data;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Commands.Create;
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Product>
{
private readonly ProductDBContext _dbContext;
public CreateProductCommandHandler(ProductDBContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Product> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var product = new Product
{
Name = request.Name,
Description = request.Description,
Category = request.Category,
Price = request.Price,
};
_dbContext.Products.Add(product);
await _dbContext.SaveChangesAsync();
return product;
}
}
Then, create a new folder called “Update” and inside it, create the classes below:
UpdateProductCommand
using MediatR;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Commands.Update;
public class UpdateProductCommand : IRequest<Product>
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public string? Category { get; set; }
public bool Active { get; set; } = true;
public decimal Price { get; set; }
}
UpdateProductCommandHandler
using MediatR;
using ProductCatalog.Data;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Commands.Update
{
public class UpdateProductCommandHandler : IRequestHandler<UpdateProductCommand, Product>
{
private readonly ProductDBContext _dbContext;
public UpdateProductCommandHandler(ProductDBContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Product> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
{
var product = _dbContext.Products.FirstOrDefault(p => p.Id == request.Id);
if (product is null)
return default;
product.Name = request.Name;
product.Description = request.Description;
product.Category = request.Category;
product.Price = request.Price;
await _dbContext.SaveChangesAsync();
return product;
}
}
}
Then, create a new folder called “Delete” and, inside it, create the classes below:
DeleteProductCommand
using MediatR;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Commands.Delete;
public class DeleteProductCommand : IRequest<Product>
{
public int Id { get; set; }
}
DeleteProductCommandHandler
using MediatR;
using ProductCatalog.Data;
using ProductCatalog.Models;
namespace ProductCatalog.Resources.Commands.Delete;
public class DeleteProductCommandHandler : IRequestHandler<DeleteProductCommand, Product>
{
private readonly ProductDBContext _dbContext;
public DeleteProductCommandHandler(ProductDBContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Product> Handle(DeleteProductCommand request, CancellationToken cancellationToken)
{
var product = _dbContext.Products.FirstOrDefault(p => p.Id == request.Id);
if (product is null)
return default;
_dbContext.Remove(product);
await _dbContext.SaveChangesAsync();
return product;
}
}
Configuring the Program Class
In the Program.cs archive, add the following code line:
builder.Services.AddMediatR(Assembly.GetExecutingAssembly());
Adding the Endpoints
Still in the Program.cs archive, add the following code to configure the API endpoints:
app.MapGet("product/get-all", async (IMediator _mediator) =>
{
try
{
var command = new GetAllProductsQuery();
var response = await _mediator.Send(command);
return response is not null ? Results.Ok(response) : Results.NotFound();
}
catch (Exception ex)
{
return Results.BadRequest(ex.Message);
}
});
app.MapGet("product/get-by-id", async (IMediator _mediator, int id) =>
{
try
{
var command = new GetProductByIdQuery() { Id = id };
var response = await _mediator.Send(command);
return response is not null ? Results.Ok(response) : Results.NotFound();
}
catch (Exception ex)
{
return Results.BadRequest(ex.Message);
}
});
app.MapPost("product/create", async (IMediator _mediator, Product product) =>
{
try
{
var command = new CreateProductCommand()
{
Name = product.Name,
Description = product.Description,
Category = product.Category,
Price = product.Price,
Active = product.Active,
};
var response = await _mediator.Send(command);
return response is not null ? Results.Ok(response) : Results.NotFound();
}
catch (Exception ex)
{
return Results.BadRequest(ex.Message);
}
});
app.MapPut("product/update", async (IMediator _mediator, Product product) =>
{
try
{
var command = new UpdateProductCommand()
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Category = product.Category,
Price = product.Price,
Active = product.Active,
};
var response = await _mediator.Send(command);
return response is not null ? Results.Ok(response) : Results.NotFound();
}
catch (Exception ex)
{
return Results.BadRequest(ex.Message);
}
});
app.MapDelete("product/delete", async (IMediator _mediator, int id) =>
{
try
{
var command = new DeleteProductCommand() { Id = id };
var response = await _mediator.Send(command);
return response is not null ? Results.Ok(response) : Results.NotFound();
}
catch (Exception ex)
{
return Results.BadRequest(ex.Message);
}
});
Running the Project With Fiddler Everywhere
The GIF below demonstrates the execution of the project via Fiddler Everywhere, a secure web debugging proxy for any platform. You can see some CRUD functions executing perfectly as expected.
Conclusion
CQRS is a development standard that brings many advantages, such as the possibility for separate teams to work on the read and persistence layer and also to be able to scale database resources as needed.
Understanding how CQRS works and how to implement it in an application allows you to do very well when the need arises to use it in some project.
This content originally appeared on Telerik Blogs and was authored by Assis Zang
Assis Zang | Sciencx (2022-12-14T15:42:03+00:00) Applying the CQRS Pattern in an ASP.NET Core Application. Retrieved from https://www.scien.cx/2022/12/14/applying-the-cqrs-pattern-in-an-asp-net-core-application/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.