As enterprise applications grow, combining read and write operations in the same services often creates tightly coupled and difficult-to-maintain systems.
CQRS (Command Query Responsibility Segregation) solves this problem by separating operations that modify data from operations that retrieve data.
What is CQRS?
CQRS stands for Command Query Responsibility Segregation. It is an architectural pattern where:
- Commands modify data
- Queries retrieve data
A command should never be responsible for returning complete entities. Its responsibility is only to perform the action successfully.
Student Management System Example
In this tutorial, we will create:
- Create Student Command
- Get Student By ID Query
- Command Handlers
- Query Handlers
- DTOs
1. Student Entity
public class Student
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
This entity maps directly to the Student table in SQL Server.
2. DbContext Configuration
public class SchoolDbContext : DbContext
{
public SchoolDbContext(DbContextOptions<SchoolDbContext> options)
: base(options)
{
}
public DbSet<Student> Student { get; set; }
}
3. Student DTO
DTOs help expose only required fields to the client.
public class StudentDTO
{
public int ID { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
4. Create Student Command
public record CreateStudentCommand
{
public required string Name { get; set; }
public required string Email { get; set; }
}
5. Get Student By ID Query
public record GetStudentByIdQuery
{
public int ID { get; set; }
}
6. ICommandHandler Interface
public interface ICommandHandler<TCommand, TResult>
{
Task<TResult> HandleAsync(TCommand command);
}
7. IQueryHandler Interface
public interface IQueryHandler<TQuery, TResult>
{
Task<TResult> HandleAsync(TQuery query);
}
8. Create Student Command Handler
public class CreateStudentCommandHandler
: ICommandHandler<CreateStudentCommand, int>
{
private readonly SchoolDbContext _studentDbContext;
public CreateStudentCommandHandler(SchoolDbContext studentDbContext)
{
_studentDbContext = studentDbContext;
}
public async Task<int> HandleAsync(CreateStudentCommand command)
{
var studentNew = new Student()
{
Name = command.Name,
Email = command.Email
};
await _studentDbContext.Student.AddAsync(studentNew);
await _studentDbContext.SaveChangesAsync();
return studentNew.ID;
}
}
9. Get Student By ID Query Handler
public class GetStudentByIdQueryHandler
: IQueryHandler<GetStudentByIdQuery, StudentDTO?>
{
private readonly SchoolDbContext _context;
public GetStudentByIdQueryHandler(SchoolDbContext context)
{
_context = context;
}
public async Task<StudentDTO?> HandleAsync(GetStudentByIdQuery query)
{
var result = await _context.Student
.Where(s => s.ID == query.ID)
.FirstOrDefaultAsync();
if (result == null)
return null;
return new StudentDTO()
{
ID = result.ID,
Name = result.Name,
Email = result.Email
};
}
}
10. Connection String
"ConnectionStrings": {
"StudentConn":
"Server=localhost;Initial Catalog=MyCQRS;
Integrated Security=True;TrustServerCertificate=True;"
}
11. Register Services in Program.cs
builder.Services.AddDbContext<SchoolDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("StudentConn")));
builder.Services.AddScoped<
ICommandHandler<CreateStudentCommand, int>,
CreateStudentCommandHandler>();
builder.Services.AddScoped<
IQueryHandler<GetStudentByIdQuery, StudentDTO?>,
GetStudentByIdQueryHandler>();
12. Student Controller
[Route("api/[controller]")]
[ApiController]
public class StudentController : ControllerBase
{
private readonly ICommandHandler<CreateStudentCommand, int> _createStudent;
private readonly IQueryHandler<GetStudentByIdQuery, StudentDTO?> _getStudentById;
public StudentController(
ICommandHandler<CreateStudentCommand, int> createStudent,
IQueryHandler<GetStudentByIdQuery, StudentDTO?> getStudentById)
{
_createStudent = createStudent;
_getStudentById = getStudentById;
}
[HttpPost]
public async Task<IActionResult> Create(CreateStudentCommand command)
{
var studentId = await _createStudent.HandleAsync(command);
return Ok(new
{
Message = "Student created successfully",
StudentId = studentId
});
}
[HttpGet("{id}")]
public async Task<IActionResult> GetByStudentId(int id)
{
var student = await _getStudentById
.HandleAsync(new GetStudentByIdQuery()
{
ID = id
});
if (student == null)
return NotFound();
return Ok(student);
}
}
Advantages of CQRS
- Better separation of concerns
- Improved scalability
- Cleaner architecture
- Independent read/write optimization
- Higher maintainability
- Easier testing
Conclusion
CQRS helps developers create scalable and maintainable enterprise applications by clearly separating read and write operations.
Using Commands, Queries, DTOs, and Handlers in ASP.NET Core improves architecture quality and keeps applications clean as they grow.