Skip to content

Mastering CQRS (Command Query Responsibility Segregation) in .NET Core: A Comprehensive Guide

Command Query Responsibility Segregation (CQRS) is a powerful pattern that separates read and write operations in an application, promoting scalability, maintainability, and performance. By splitting commands (which change state) from queries (which read state), CQRS allows each to be optimized independently.

In this article, we’ll explore the CQRS pattern and provide a step-by-step guide to implementing it in a .NET Core application.

Table of Contents

  1. Introduction to CQRS
  2. Benefits of Using CQRS
  3. Setting Up the Development Environment
  4. Creating the Solution Structure
  5. Implementing the Command Side
  6. Implementing the Query Side
  7. Integrating with a Database
  8. Using MediatR for CQRS in .NET Core
  9. Testing the Application
  10. Conclusion

1. Introduction to CQRS

CQRS stands for Command Query Responsibility Segregation. It splits the application into two main parts:

  • Commands: Operations that change the state of the application (create, update, delete).
  • Queries: Operations that read data without changing the state.
CQRS-architecture
CQRS Architecture

2. Benefits of Using CQRS

  • Scalability: Each part can be scaled independently.
  • Performance: Optimized read and write models improve performance.
  • Maintainability: Clear separation of concerns makes the codebase easier to maintain.
  • Flexibility: Allows for different data storage strategies for reads and writes.

3. Setting Up the Development Environment

Ensure you have the following tools installed:

  • .NET Core SDK
  • Visual Studio or Visual Studio Code

4. Creating the Solution Structure

We will create a solution with three projects: API, Command, and Query.

  1. Create the Solution and Projects: Open your terminal or command prompt and run:
mkdir CQRSApp
cd CQRSApp
dotnet new sln

dotnet new webapi -o Api
dotnet new classlib -o Command
dotnet new classlib -o Query

dotnet sln add Api/Api.csproj
dotnet sln add Command/Command.csproj
dotnet sln add Query/Query.csproj
  1. Add Project References: Add references to ensure the projects can communicate:
dotnet add Api/Api.csproj reference Command/Command.csproj
dotnet add Api/Api.csproj reference Query/Query.csproj

5. Implementing the Command Side

The Command side handles operations that change the state of the application.

  1. Define Command Models: In the Command project, create a Models folder and add a ProductCommandModel.cs file:
namespace Command.Models
{
    public class ProductCommandModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

2. Create Command Handlers: In the Command project, create a Handlers folder and add a ProductCommandHandler.cs file:

using System.Threading.Tasks;
using Command.Models;

namespace Command.Handlers
{
    public class ProductCommandHandler
    {
        public async Task HandleCreateAsync(ProductCommandModel model)
        {
            // Add logic to handle create operation
        }

        public async Task HandleUpdateAsync(ProductCommandModel model)
        {
            // Add logic to handle update operation
        }

        public async Task HandleDeleteAsync(int id)
        {
            // Add logic to handle delete operation
        }
    }
}

6. Implementing the Query Side

The Query side handles operations that read data from the application.

  1. Define Query Models: In the Query project, create a Models folder and add a ProductQueryModel.cs file:
namespace Query.Models
{
    public class ProductQueryModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

2. Create Query Handlers: In the Query project, create a Handlers folder and add a ProductQueryHandler.cs file:

using System.Collections.Generic;
using System.Threading.Tasks;
using Query.Models;

namespace Query.Handlers
{
    public class ProductQueryHandler
    {
        public async Task<IEnumerable<ProductQueryModel>> HandleGetAllAsync()
        {
            // Add logic to handle get all operation
        }

        public async Task<ProductQueryModel> HandleGetByIdAsync(int id)
        {
            // Add logic to handle get by id operation
        }
    }
}

7. Integrating with a Database

Integrate the Command and Query sides with a database to persist and retrieve data.

  1. Add Entity Framework Core Packages: In both Command and Query projects, add EF Core packages:
dotnet add Command/Command.csproj package Microsoft.EntityFrameworkCore
dotnet add Command/Command.csproj package Microsoft.EntityFrameworkCore.SqlServer

dotnet add Query/Query.csproj package Microsoft.EntityFrameworkCore
dotnet add Query/Query.csproj package Microsoft.EntityFrameworkCore.SqlServer

2. Create DbContext: In the Command project, add a DataContext.cs file:

using Microsoft.EntityFrameworkCore;

namespace Command
{
    public class DataContext : DbContext
    {
        public DataContext(DbContextOptions<DataContext> options) : base(options) { }

        public DbSet<Models.ProductCommandModel> Products { get; set; }
    }
}

In the Query project, add a DataContext.cs file:

using Microsoft.EntityFrameworkCore;

namespace Query
{
    public class DataContext : DbContext
    {
        public DataContext(DbContextOptions<DataContext> options) : base(options) { }

        public DbSet<Models.ProductQueryModel> Products { get; set; }
    }
}

8. Using MediatR for CQRS in .NET Core

MediatR is a popular library for implementing CQRS and Mediator patterns in .NET Core.

  1. Add MediatR Package: In the Api project, add the MediatR package:
dotnet add Api/Api.csproj package MediatR.Extensions.Microsoft.DependencyInjection

2. Register MediatR in Startup.cs: Modify Startup.cs in the Api project:

using Command.Handlers;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Query.Handlers;

namespace Api
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<Command.DataContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddDbContext<Query.DataContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddMediatR(typeof(ProductCommandHandler).Assembly);
            services.AddMediatR(typeof(ProductQueryHandler).Assembly);

            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

9. Testing the Application

Test the entire application to ensure the CQRS pattern is correctly implemented.

  1. Run Migrations and Update Database: Apply migrations to set up the database schema:
dotnet ef migrations add InitialCreate -p Command -s Api
dotnet ef database update -p Command -s Api

2. Test API Endpoints: Run the Api project and use tools like Postman or Swagger to test the endpoints for creating, updating, deleting, and querying products.

10. Conclusion

Implementing the CQRS pattern in .NET Core separates read and write operations, improving scalability, maintainability, and performance. By following this guide, you have created a .NET Core application with a clear separation of concerns, using MediatR to manage commands and queries effectively.

Leave a Reply