Creating a CQRS Architecture in .NET Core 8
Effective read and write operation management is essential to software development in order to ensure the scalability and maintainability of applications. This article uses C# and MediatR to demonstrate a real-world implementation of the CQRS (Command Query Responsibility Segregation) architecture. What distinguishes CQRS from alternative architectures? The first one is to concentrate on the procedure rather than the information; allow me to clarify: The CQRS pattern distinguishes between writing activities (commands) and reading operations (questions). We divide our operations according to purpose rather than using a model that works for everyone.
In addition to lowering the coupling between components, MediatR makes it easier to manage handlers in queries and commands. This offers us several benefits, including the ability to keep a cleaner and more organized code and the ability to utilize other technologies or ORM for readings and writing.
This article provides a very basic description of an architecture that includes the aforementioned. It includes the fundamentals: data validation, security, authentication, and mapping. I hope you find it helpful.
We will start with the domain and make our repositories.
DOMAIN
We add a project of type library “Project. Domain” in .NET Core, and we add 1 Entities folder to that folder, which we will create.
BaseEntity.cs
- Person. cs //table for people
- User.cs //system users table
The base entity is an abstraction for the fields common to our tables, it will also help us in creating our generic repository.
The user table handles sensitive information; we do not use it with the base entity.
INFRASTRUCTURE
We create a library-type project “Project. Infrastructure”, and add those references to this project.
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.SqlServer
- Pomelo.EntityFrameworkCore.MySql
create the folders
- Migrations (if you are going to work code first, this is not the case)
- Persistence
- Context
- Repositories
In the Context folder, create an AppDbContext.cs file in order to define a custom Entity Framework Core DbContext; in this app part, we manage the database data interactions (CRUD)
in this example, I only have two DbSets Person and User, by another hand in the method “OnModelCreating” I’m using an overridden in order to configure the entity mappings.
In the repositories folder, we will create BaseRepository.cs, PersonRepository.cs, and UserRepository.cs.
Let’s start with BaseRepository, there we will create all the methods to interact with the data referring to its interface IAsyncRepository<T> where T: BaseEntity.
- GetAllAsync
- GetAllIquerable
- AddAsync
- UpdateAsync
- DeleteAsync
- GetByIdAsync
- AddEntity
- UpdateEntity
- DeleteEntity
- GetAsync (has several overrides for queries, simple, combined, and pagination; usage examples are in the attached file).
PersonRepository
In this part, we use the RepositoryBase<Person> class
If you need additional information or any process that is not in the base repository, you can add it here and publish it in your personal interface.
User Repository
There are basic methods to handle users in a small project. If you wish, you could add more stuff like the locked user, unlocked user, levels, etc.
APPLICATION
We add a project of type library “Project. Application” in netCore, then add the next references to Project. Application.
- AutoMapper.Extensions.Microsoft.DependencyInjection Version 12.0.1
- JWT Version 10.1.1
- MediatR Version 12.4.1
- Pediatr.Extensions.FluentValidation.AspNetCore Version 5.1.0
- Pediatr.Extensions.Microsoft.DependencyInjection Version 11.1.0
- Microsoft.AspNetCore.Cryptography.KeyDerivation Version 8.0.8
- Microsoft.Extensions.Configuration Version 8.0.0
- Microsoft.IdentityModel.Tokens Version 8.1.0
- System.IdentityModel.Tokens.Jwt Version 8.1.0
Please create the next folder path in this project.
I’m going to explain the most important classes in this project,
Mapping in order to make the programming work, we use the Mapper package order to map from DTO’s to Class.
COMMANDS AND QUERIES
Since this is a CQRS architecture project, this is where the magic happens. The CQRS pattern focuses on processes rather than data. Let me explain: if you have an “employee” entity, you focus on the processes of that entity – creating, deleting, modifying, and querying. We must also remember that this pattern separates reads and writes into different models, using commands to update data and queries to read data. Commands should be task-based rather than data-focused. To achieve this, we use the MediatR library, where we will declare handlers that inherit the properties and methods of IRequestHandler from the MediatR library.
For example, in the person class, we have Hire a Person (Create), Update Person info (Update), Fired Person (Delete), and Get info for Person (Query).
Example code for GetAllPersonHandler
This code defines a handler class called GetAllPersonQueryHandler. It handles queries of type GetAllPersonQuery and returns a list of PersonListDto.
The class uses two dependencies: IPersonRepository for accessing data and IMapper for mapping data objects. When the Handle method is called, it fetches all persons from the repository asynchronously.
It then maps the fetched persons to a list of PersonListDto and returns the result.
In the case of a command, such as creating a person, the process is a bit more complex. Besides using mapping to convert from DTO to the person class, we also need to perform validations. Fortunately, we use FluentValidation, which centralizes our validations into a single process and makes programming easier.
VALIDATORS
To implement validations in a CQRMS (Command Query Responsibility Segregation with Microservices) architecture, FluentValidation makes it easy to define rules in a fluid and concise way. In the CreatePersonCommandValidator example, each property of the CreatePersonCommand command is validated individually, ensuring the consistency and accuracy of the data received before being processed. For example, it is established that the first and last names must not be empty and cannot exceed 45 characters. The email property is checked for both its mandatory nature and its correct format, while the birth must be a valid date in the past.
This modular structure not only ensures data integrity but also provides custom error messages, improving the user experience and facilitating code maintenance in complex systems. Another good news is the validator focuses on processes rather than data. Let me explain again; we can create different validators for different business rules and easily find them because they are in the same folder of commands or queries.
Example for Validator in process Creates Person
Example for Result.
SECURITY
For the security of the application, we will use two JWT tools and our password generator, with passwordhasher, we mix the user password to create maximum security encryption and store it in the database, On the other hand, services after the user is correctly identified, this service provides them with a token. With this token, the user can authenticate herself across different system endpoints. Together, these two systems ensure that only the right people can access and use the system securely.
Program. cs
In ASP.NET Core applications, the app’s startup code is located in the Program.cs
file. This is where all the necessary services for the application are configured, including database connections, middleware, security, and other essential components. In our program, we will set up the following configuration
- Database Configuration: A connection to a MySQL database is established using Pomelo lib.
- Dependency Injection: The different repositories are registered, it is important to indicate that the IPasswordHandler is also registered.
- MediatR Logging: MediatR is used to handle commands and queries.
- FluentValidation: A validation behavior is added to the MediatR pipeline to automatically verify data before executing each command or query.
- Swagger: Swagger is configured at a basic level to have documentation of the APIs that allow us to test easily.
- JWT: JWT authentication parameters are configured.
- AutoMapper: to facilitate conversions between models and DTOs in the application.
As an additional item, I leave some examples of how to use the generic Repository for simple and complex queries.
BASICS QUERIES
Example 1. Filter products with a price greater than a specific value.
Example 2. Filter products with a specific name.
Example 3. Filter products with stock greater than 50.
Example 4. Filter products created in the last month.
Example 5. Filter products whose name contains a specific word.
Example 6. Filter products whose price is within a specific range.
Example 7. Filter products by multiple conditions (for example, a specific name and price that is less than a value).
WITH ORDER BY PARAMETER
Example 1. Sort products by name alphabetically.
Example 2. Sort products by price from highest to lowest.
Example 3. Sort products by creation date and then by price.
Example 8. Filter products with stock between 10 and 100 units.
INCLUDE STRING: INCLUDE RELATIONSHIPS BETWEEN ENTITIES
The includeString parameter is used to include related entities in the query, that is, to load data from related entities (known as “eager loading”). This is useful when you need to access related data in the same query to avoid multiple trips to the database.
Example 1. Include the Category entity in the query.
Example 2. Include a nested relationship (for example, if Category had a relationship with another entity).
Example 3. Bring the product list (id, name, price) of their respective categories (category. id).
MORE COMPLETE METHODS INCLUDE
Example 1. Include the Category entity.
Example 2. Include multiple related entities.
If the Product has another related entity, for example, a Supplier, you can include both relationships.
Example 3. Using orderBy and predicate together with includes.
Example 4. showing the id, name, and price from the product table and the name from the Category table.
USING PAGINATION
example