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.

This segregation allows each side to evolve independently, optimize performance for their specific tasks, and potentially scale differently.

Benefits and Trade-Offs

  1. 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.
  2. 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

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.