CQRS in ASP.NET Core Web API

• By OmerZ Solutions

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.

CQRS in ASP.NET Core Web API
CQRS separates write operations (Commands) from read operations (Queries) to create scalable and maintainable applications.

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; }
}
The command only performs insertion. It does not return the full student object.

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.

Commands modify data. Queries retrieve data. They should never be mixed together.

Need Help Building Enterprise .NET Applications?

OmerZ Solutions helps businesses build scalable, secure, and maintainable ASP.NET Core applications.

Contact Us