Previous Event Sourcing Integration with MassTransit or MediatR Next

⚡ CQRS (Command Query Responsibility Segregation)

Command Query Responsibility Segregation (CQRS) is a design pattern that separates the operations for reading data (queries) from those for updating data (commands). Unlike traditional CRUD (Create, Read, Update, Delete) architectures that use a single model for both, CQRS maintains distinct models, allowing each to be optimized for its specific purpose.

This separation is often implemented with different data stores or different schemas within a single data store. While the write-side (command) model focuses on business logic and data integrity, the read-side (query) model is typically denormalized and optimized for efficient querying and display.

📖 What is CQRS?

CQRS is a design pattern that separates read operations (queries) from write operations (commands). Instead of using the same model for both, CQRS uses different models optimized for their purpose:

  • Commands: Change state (Create, Update, Delete).
  • Queries: Read state (Get, List, Search).

This separation improves scalability, maintainability, and clarity in complex systems.

⚙️ CQRS with .NET Core using MediatR

The MediatR library is commonly used to implement CQRS in .NET Core. It acts as an in-process messaging system that decouples the sending of commands and queries from their respective handlers.

🧩 Example: A Simple Product Management API

1️⃣ Set up the project

In your ASP.NET Core API project, install the MediatR and MediatR.Extensions.Microsoft.DependencyInjection NuGet packages.

2️⃣ Structure the application

For a clean architecture, organize the command and query logic into separate folders or projects.

- YourProject.Api
  - Features
    - Products
      - Commands
        - CreateProduct
          - CreateProductCommand.cs
          - CreateProductCommandHandler.cs
      - Queries
        - GetProduct
          - GetProductQuery.cs
          - GetProductQueryHandler.cs
      - DTOs
        - ProductDto.cs
    

3️⃣ Define the query

A query is a simple data transfer object (DTO) that holds the data needed to retrieve information, and it returns a result without altering state.

// Features/Products/Queries/GetProduct/GetProductQuery.cs
public class GetProductQuery : IRequest<ProductDto?>
{
    public Guid Id { get; set; }
}

// Features/Products/DTOs/ProductDto.cs
public record ProductDto(Guid Id, string Name, decimal Price);
    

4️⃣ Create the query handler

This handler contains the specific logic for fetching the data, often projecting it directly to the DTO to avoid fetching unnecessary fields.

5️⃣ Define the command

A command represents an action that modifies the system's state and typically does not return a value.

6️⃣ Create the command handler

This handler contains the business logic for the write operation.

7️⃣ Implement the API controller

The controller uses MediatR's ISender to dispatch commands and queries.

🛠 Another Example in .NET Core (with MediatR)

Using the MediatR library to implement CQRS:

// Install-Package MediatR.Extensions.Microsoft.DependencyInjection

// Command
public record CreateOrderCommand(string OrderId, string Customer) : IRequest<bool>;

// Command Handler
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, bool>
{
    public Task<bool> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Order Created: {request.OrderId} for {request.Customer}");
        return Task.FromResult(true);
    }
}

// Query
public record GetOrderByIdQuery(string OrderId) : IRequest<Order>;

// Query Handler
public class GetOrderByIdHandler : IRequestHandler<GetOrderByIdQuery, Order>
{
    public Task<Order> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
    {
        return Task.FromResult(new Order { OrderId = request.OrderId, Customer = "Demo Customer" });
    }
}

// Model
public class Order
{
    public string OrderId { get; set; }
    public string Customer { get; set; }
}
    

✅ Advantages

  • Independent scaling and optimized models: Read and write operations can be scaled and optimized separately, potentially using different data stores.
  • Improved performance and separation of concerns: Separating read and write logic can enhance performance by reducing contention and leads to cleaner code.
  • Flexibility: Different persistence technologies can be used for each side.
  • Clear separation of concerns (read vs write).
  • Improves scalability (queries and commands can scale independently).
  • Supports complex domains with different read/write requirements.
  • Pairs well with Event Sourcing and microservices.

⚠️ Disadvantages

  • Increased complexity and eventual consistency: Implementing CQRS adds significant complexity, and using separate data stores can lead to eventual consistency issues.
  • Data synchronization and operational overhead: Synchronizing data between models and managing separate systems increases operational costs.
  • Increases architectural complexity.
  • Eventual consistency between read and write models.
  • Requires more infrastructure (e.g., separate databases for read/write).
  • Not always necessary for simple CRUD applications.

🧭 Best Practices

  • Use CQRS only when the domain is complex enough to justify it.
  • Combine with Event Sourcing for full auditability.
  • Keep command and query models independent.
  • Use MediatR or similar libraries to simplify handler management.

🕐 When to Use CQRS

  • Performance and scalability needs: Suitable for applications with high read-to-write ratios or demanding performance requirements.
  • Complex domains and collaboration: Beneficial for systems with complex business logic or where multiple users update the same data.
  • Integration with Event Sourcing: Works well with Event Sourcing by using events to update read models.

🚫 When Not to Use CQRS

  • Simple applications: Not recommended for basic CRUD applications where the added complexity is unnecessary.
  • Limited resources: The overhead may be too much for small teams.
  • Strict real-time consistency: Not ideal if the read model requires immediate updates.

🔒 Precautions

  • Ensure eventual consistency is acceptable for your business case.
  • Secure command handlers (they change state).
  • Optimize query handlers for performance (caching, projections).
  • Monitor and log both command and query pipelines.

💡 Tips

  • Use a mediator like MediatR for communication.
  • Start with a simple separation and add complexity as needed.
  • Use DTOs for queries for better performance and separation.
  • Consider caching for performance-critical reads.
  • Combine with Event Sourcing for audit trails and temporal consistency.

🎯 Summary

CQRS is a powerful architectural pattern for scalable, maintainable, and event-driven systems . In .NET Core, it is often implemented with MediatR and works best in complex domains where read and write workloads differ significantly.

Back to Index
Previous Event Sourcing Integration with MassTransit or MediatR Next
*