The Command Query Responsibility Segregation (CQRS) pattern is a design principle that separates the responsibility for handling queries (read operations) and commands (write operations). This separation allows developers to optimize the read and write parts of the system independently, resulting in cleaner code, better scalability, and easier testing.
Key Concepts of CQRS
- Command: Represents an operation that changes the state of the system (e.g., creating, updating, or deleting data).
- Query: Represents an operation that retrieves data without modifying the state.
- Segregation: Commands and queries are implemented in separate models, classes, or layers.
- Independence: Read and write models can use different technologies, databases, or schemas for optimization.
Basic Implementation of CQRS in C#
Define Commands
Commands represent actions that change the application's state.
public class CreateOrderCommand
{
public int OrderId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
}
Command Handler
Handles the logic for processing a command.
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand>
{
public void Handle(CreateOrderCommand command)
{
// Logic to save the order to the database
Console.WriteLine($"Order {command.OrderId} created for {command.ProductName}.");
}
}
Define Queries
Queries represent operations that fetch data without modifying it.
public class GetOrderQuery
{
public int OrderId { get; set; }
}
public class OrderDto
{
public int OrderId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
}
Query Handler
Handles the logic for processing a query.
public interface IQueryHandler<TQuery, TResult>
{
TResult Handle(TQuery query);
}
public class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, OrderDto>
{
public OrderDto Handle(GetOrderQuery query)
{
// Simulated data retrieval
return new OrderDto
{
OrderId = query.OrderId,
ProductName = "Sample Product",
Quantity = 1
};
}
}
Mediator for Dispatching
A mediator centralizes the logic for dispatching commands and queries to their respective handlers.
public class Mediator
{
private readonly IServiceProvider _serviceProvider;
public Mediator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Send<TCommand>(TCommand command)
{
var handler = _serviceProvider.GetService(typeof(ICommandHandler<TCommand>)) as ICommandHandler<TCommand>;
handler?.Handle(command);
}
public TResult Query<TQuery, TResult>(TQuery query)
{
var handler = _serviceProvider.GetService(typeof(IQueryHandler<TQuery, TResult>)) as IQueryHandler<TQuery, TResult>;
return handler != null ? handler.Handle(query) : default;
}
}
Register and Use
Set up the dependency injection and use the mediator to send commands and queries.
var services = new ServiceCollection();
// Register handlers
services.AddTransient<ICommandHandler<CreateOrderCommand>, CreateOrderCommandHandler>();
services.AddTransient<IQueryHandler<GetOrderQuery, OrderDto>, GetOrderQueryHandler>();
// Register mediator
services.AddSingleton<Mediator>();
var serviceProvider = services.BuildServiceProvider();
var mediator = serviceProvider.GetRequiredService<Mediator>();
// Use mediator
mediator.Send(new CreateOrderCommand { OrderId = 1, ProductName = "Laptop", Quantity = 2 });
var order = mediator.Query<GetOrderQuery, OrderDto>(new GetOrderQuery { OrderId = 1 });
Console.WriteLine($"Order Retrieved: {order.ProductName}, Quantity: {order.Quantity}");
Advantages of CQRS
- Separation of Concerns: Clear distinction between read and write logic.
- Scalability: Enables optimization of read and write operations independently.
- Flexibility: Allows the use of different technologies or data stores for read and write models.
- Easier Testing: Each command and query handler can be unit tested in isolation.
When to Use CQRS
- Complex systems with heavy read/write operations.
- Applications requiring different scaling for reads and writes.
- Systems with highly complex business rules for data modification.
When Not to Use CQRS
- Simple applications with straightforward data access needs.
- Systems where the added complexity outweighs the benefits.
CQRS can be combined with patterns like Event Sourcing for further benefits, but it should be implemented where justified to avoid over-engineering.
Leave Comment