cqrs-patterns

star 1

CQRS implementation — commands use EF Core DbContext, queries use Dapper IDbConnection. Use when implementing or modifying command/query handlers.

z3d By z3d schedule Updated 3/7/2026

name: cqrs-patterns description: CQRS implementation — commands use EF Core DbContext, queries use Dapper IDbConnection. Use when implementing or modifying command/query handlers. user-invocable: false

CQRS Implementation Patterns

Core CQRS Separation

  • Commands → DTOs: Write operations return DTOs for client communication
  • Queries → ReadModels: Read operations return ReadModels optimized for display
  • Never mix: Don't return DTOs from queries or ReadModels from commands

Interface Definitions

public interface ICommand { }
public interface IQuery<TResult> { }

public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    Task HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default);
}

Return Type Standards

// CORRECT - Commands return DTOs
public async Task<ActionResult<CustomerDto>> CreateCustomer(CreateCustomerCommand command)
public async Task<ActionResult<ProductDto>> UpdateProduct(int id, UpdateProductCommand command)

// CORRECT - Queries return ReadModels
public async Task<ActionResult<CustomerReadModel>> GetCustomer(int id)
public async Task<ActionResult<IEnumerable<ProductReadModel>>> GetAllProducts()

// WRONG - Mixed concerns
// public async Task<ActionResult<CustomerDto>> GetCustomer(int id)      // Should be ReadModel
// public async Task<ActionResult<CustomerReadModel>> CreateCustomer()   // Should be DTO

Data Access Design

Commands use DbContext directly — no repository abstraction:

  • DbContext already implements Unit of Work and Repository patterns internally
  • A repository layer added indirection without value
  • Direct DbContext usage is simpler, more explicit, and easier to debug
  • Load entities as tracked (no AsNoTracking) so EF detects only changed properties
  • Use .Include() to load navigations (e.g., Orders.Include(o => o.Items))
  • Mutate through domain methods, then single SaveChangesAsync(cancellationToken)
  • Never use AsNoTracking + Reconstitute + Update in handlers — it marks all columns modified and creates lost-update risks

Queries use Dapper for optimized reads directly against SQL, returning lightweight ReadModels.

Command Handler Example

public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto>
{
    private readonly ApplicationDbContext _dbContext;

    public CreateCustomerCommandHandler(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<CustomerDto> HandleAsync(CreateCustomerCommand command, CancellationToken cancellationToken)
    {
        var email = Email.Create(command.Email);
        var customer = new Customer(command.Name, email);

        _dbContext.Customers.Add(customer);
        await _dbContext.SaveChangesAsync(cancellationToken);

        return MapToDto(customer); // Explicit mapping
    }
}

Query Handler Example

public class GetCustomerQueryHandler : IRequestHandler<GetCustomerQuery, CustomerReadModel>
{
    private readonly IDbConnection _connection;

    public GetCustomerQueryHandler(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<CustomerReadModel> HandleAsync(GetCustomerQuery query, CancellationToken cancellationToken)
    {
        const string sql = "SELECT Id, Name, Email FROM Customers WHERE Id = @Id";
        var customer = await _connection.QuerySingleOrDefaultAsync<CustomerReadModel>(sql, new { query.Id });
        if (customer == null)
            throw new KeyNotFoundException($"Customer with ID {query.Id} not found");
        return customer;
    }
}

Auto-Registration

// In Program.cs - Single line registers ALL handlers
builder.Services.AddMediator(Assembly.GetExecutingAssembly());

// Auto-discovers and registers:
// - All command handlers implementing IRequestHandler<,>
// - All query handlers implementing IRequestHandler<,>
// - Zero manual registration needed

Dual Interface Requirement

Commands must implement both ICommand (marker) AND IRequest<T>/IRequest (mediator dispatch). Same for queries: IQuery<T> AND IRequest<T>. Convention tests enforce this bidirectionally.

Install via CLI
npx skills add https://github.com/z3d/Starter-App-Dotnet-0 --skill cqrs-patterns
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator