In software development, efficient management of read and write operations is crucial to keeping applications scalable and maintainable. This article presents a practical implementation of the CQRS (Command Query Responsibility Segregation) architecture using C# and MediatR. What is the main difference between CQRS and other architectures? The main one is to focus on the process and not on the data; let me explain: CQRS is a pattern that separates reading operations (Queries) from writing operations (Commands). Instead of having a one-size-fits-all model, we separate our operations by purpose.
This gives us some advantages, such as maintaining a cleaner and more organized code, and different technologies or ORM can be used for readings and writing; with MediatR, we simplify the management of handlers in queries and commands in addition to reducing the coupling between components.
This article describes an architecture with the above in a very simple way. It contains the basics: authentication, security, data validations, and data mapping. I hope it is useful to you.

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
public class BaseEntity
{
public virtual DateTime? CreatedAt { get; set; }
public virtual string? CreatedBy { get; set; } = string.Empty;
public virtual DateTime? ModifiedAt { get; set; }
public virtual string? ModifiedBy { get; set; } =string.Empty;
[NotMapped]
public virtual long Id { get; set; }
}
public class Person :BaseEntity
{
public string Name { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
}
public partial class User
{
public long UserId { get; set; }
public string? Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string? PhoneNumber { get; set; } = string.Empty;
}

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
public class PersonRepository : RepositoryBase<Person>, IPersonRepository
{
public PersonRepository(AppDbContext context) : base(context)
{
}
}
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.
public class UserRepository : IUserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context)
{
_context = context;
}
public async Task<long> Insert(User data)
{
try
{
_context.Set<User>().Add(data);
await _context.SaveChangesAsync();
return data.UserId;
}
catch (Exception ex)
{
throw new Exception(ex.ToString());
}
}
public async Task<User> SelectByIdASync(long userId)
{
User obj = await _context.Set<User>().FindAsync(userId);
return obj;
}
public async Task DeleteASync(long userId)
{
_context.Set<User>().Remove(await SelectByIdASync(userId));
await _context.SaveChangesAsync();
}
public async Task<User> UpdateASync(long userId, User obj)
{
var objPrev = (from iClass in _context.Users
where iClass.UserId == userId
select iClass).FirstOrDefault();
_context.Entry(objPrev).State = EntityState.Detached;
_context.Entry(obj).State = EntityState.Modified;
await _context.SaveChangesAsync();
return obj;
}
public Task<User> GetByEmail(string email)
{
var objPrev = (from iClass in _context.Users
where iClass.Email.ToLower() == email.ToLower()
select iClass).FirstOrDefaultAsync();
return objPrev;
}
}
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.
public MappingProfile()
{
CreateMap<PersonRequestDto, Person>();
CreateMap<Person,PersonRequestDto>();
CreateMap<Person, PersonListDto>();
}
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
public class GetAllPersonQueryHandler : IRequestHandler<GetAllPersonQuery, List<PersonListDto>>
{
private readonly IPersonRepository _repository;
private readonly IMapper _mapper;
public GetAllPersonQueryHandler(IPersonRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<List<PersonListDto>> Handle(GetAllPersonQuery request, CancellationToken cancellationToken)
{
var persons = await _repository.GetAllAsync();
return _mapper.Map<List<PersonListDto>>(persons);
}
}
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
public class CreatePersonCommandValidator : AbstractValidator<CreatePersonCommand>
{
public CreatePersonCommandValidator()
{
RuleFor(x => x.Person.Name)
.NotEmpty().WithMessage("The name is required.")
.MaximumLength(45).WithMessage("The name cannot exceed 45 characters.");
RuleFor(x => x.Person.LastName)
.NotEmpty().WithMessage("The last name is required.")
.MaximumLength(45).WithMessage("The last name cannot exceed 45 characters.");
RuleFor(x => x.Person.Email)
.NotEmpty().WithMessage("The email is required.")
.EmailAddress().WithMessage("Invalid email format.")
.MaximumLength(45).WithMessage("The email cannot exceed 45 characters.");
RuleFor(x => x.Person.BirthDate)
.NotEmpty().WithMessage("The birth date is required.")
.LessThan(DateTime.Now).WithMessage("Birth date must be in the past.");
}
}



Andrej APosted Nov 23, 2025, 8:29 AM
Should not UserRepository inherit from RepositoryBase ?