CQRS (Command Query Responsibility Segregation) is a pattern that separates the responsibilities of reading and writing data into distinct models. This separation can help optimize both aspects independently and improve the scalability and maintainability of applications. Let’s dive into an overview of CQRS and explore a simple implementation in C#.
CQRS Fundamentals
CQRS divides the application into two parts.
- Command Side: Responsible for handling requests that change the state of the system (e.g., creating, updating, or deleting data).
- Query Side: Responsible for handling requests that retrieve data without modifying it.
This segregation allows each side to evolve independently, optimize performance for their specific tasks, and potentially scale differently.
Benefits and Trade-Offs
- Benefits
- Optimized Data Models: The command and query models can be tailored to their respective needs, which can simplify complex queries and commands.
- Scalability: The read and write operations can be scaled independently. For instance, if reading is more frequent than writing, the read model can be scaled horizontally without affecting the write model.
- Security and Authorization: Different security requirements can be applied to the command and query sides, providing finer control over access.
- Trade-Offs
- Increased Complexity: Maintaining two separate models and their synchronization can add complexity. This often requires additional infrastructure, such as messaging systems or event stores.
- Data Consistency: Since the read and write sides are separated, you need to handle eventual consistency. This might involve asynchronous updates or complex data synchronization strategies.
Detailed Example with C#
Let’s build a more robust CQRS example, incorporating additional concepts like event sourcing and asynchronous processing.
Define the Models
We'll use the same UserCommand and UserDto models. For more complexity, we’ll include a domain event model.
// Domain Event
public class UserCreatedEvent
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
Implement Command Handlers
We'll enhance the command handler to publish domain events after handling commands.
public interface ICommandHandler<in TCommand>
{
Task HandleAsync(TCommand command);
}
public class CreateUserCommandHandler : ICommandHandler<UserCommand>
{
private readonly List<UserDto> _userStore;
private readonly IEventPublisher _eventPublisher;
public CreateUserCommandHandler(List<UserDto> userStore, IEventPublisher eventPublisher)
{
_userStore = userStore;
_eventPublisher = eventPublisher;
}
public async Task HandleAsync(UserCommand command)
{
var user = new UserDto
{
Id = command.Id,
Name = command.Name,
Email = command.Email
};
_userStore.Add(user);
var userCreatedEvent = new UserCreatedEvent
{
Id = user.Id,
Name = user.Name,
Email = user.Email
};
await _eventPublisher.PublishAsync(userCreatedEvent);
}
}
Implement Query Handlers
The query handler remains mostly the same, but it can be enhanced to handle more complex querying logic.
public interface IQueryHandler<in TQuery, out TResponse>
{
Task<TResponse> HandleAsync(TQuery query);
}
public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
private readonly List<UserDto> _userStore;
public GetUserQueryHandler(List<UserDto> userStore)
{
_userStore = userStore;
}
public Task<UserDto> HandleAsync(GetUserQuery query)
{
var user = _userStore.SingleOrDefault(u => u.Id == query.UserId);
return Task.FromResult(user);
}
}
Event Publishing and Handling
A basic event publisher implementation.
public interface IEventPublisher
{
Task PublishAsync<TEvent>(TEvent @event);
}
public class InMemoryEventPublisher : IEventPublisher
{
private readonly List<object> _events = new List<object>();
public Task PublishAsync<TEvent>(TEvent @event)
{
_events.Add(@event);
Console.WriteLine($"Event published: {@event.GetType().Name}");
return Task.CompletedTask;
}
}
Putting It All Together
Here’s how you could wire everything up in a simple application.
public class Program
{
public static async Task Main()
{
var userStore = new List<UserDto>();
var eventPublisher = new InMemoryEventPublisher();
var createUserHandler = new CreateUserCommandHandler(userStore, eventPublisher);
var getUserHandler = new GetUserQueryHandler(userStore);
var createUserCommand = new UserCommand
{
Id = Guid.NewGuid(),
Name = "John Doe",
Email = "[email protected]"
};
await createUserHandler.HandleAsync(createUserCommand);
var getUserQuery = new GetUserQuery
{
UserId = createUserCommand.Id
};
var user = await getUserHandler.HandleAsync(getUserQuery);
Console.WriteLine($"User Name: {user.Name}, Email: {user.Email}");
}
}
Additional Considerations
- Event Sourcing: Event sourcing is often used in conjunction with CQRS. Instead of persisting in the current state of an entity, you persist in a series of events that represent state changes. This allows for more flexible queries and the ability to reconstruct past states.
- Asynchronous Processing: Commands and events might be processed asynchronously, especially in distributed systems. This helps in scaling and ensures that commands are handled reliably even if the system experiences a high load.
- Data Synchronization: In scenarios where the read model is eventually consistent, mechanisms such as domain events, message queues, or event streams can be used to synchronize the read model with the write model.
Conclusion
CQRS is a powerful pattern for designing scalable and maintainable applications that separates the read and write responsibilities. The additional complexity introduced by CQRS can be managed by leveraging tools and techniques like event sourcing, asynchronous processing, and robust event handling.
By understanding and implementing CQRS effectively, developers can build systems that are more adaptable to changing requirements and better suited to handle high-scale scenarios.