Differences Between CQRS, MediatR, and CRUD

Introduction

In the ever-evolving world of software development, choosing the right architectural pattern or framework is crucial to building scalable, maintainable, and efficient applications. Three widely discussed patterns in the .NET ecosystem are CQRS (Command Query Responsibility Segregation), MediatR, and CRUD (Create, Read, Update, Delete). Each of these techniques has its unique characteristics, use cases, and benefits.

In this article, we'll delve into the differences between CQRS, MediatR, and CRUD and explore scenarios where each one shines.

Image

CQRS (Command Query Responsibility Segregation)

CQRS is an architectural pattern that separates the read and write operations of an application into distinct paths. It advocates maintaining two separate models: one for reading data (the Query model) and another for modifying data (the Command model).

This separation provides several benefits

  1. Scalability: CQRS enables horizontal scaling by allowing you to scale the read and write sides independently. You can have multiple read models optimized for different queries, improving performance.
  2. Performance Optimization: By optimizing the read model for specific queries, you can significantly improve query performance. This is particularly useful for applications with complex or resource-intensive queries.
  3. Enhanced Security: CQRS allows you to apply different security measures to the read and write sides. For example, you can implement stricter security controls on the right side to prevent unauthorized modifications.
  4. Event Sourcing Integration: CQRS is often used in conjunction with Event Sourcing, a pattern where all changes to the application state are captured as a series of immutable events. This combination enables robust auditing and event-driven architectures.

Use Cases for CQRS

  • Complex Queries: When your application involves complex, data-intensive queries that require optimization for performance.
  • Scalability Demands: In scenarios where the read and write loads are significantly different and need independent scaling.
  • Audit Trails: When you require detailed audit logs or event sourcing capabilities for compliance or analytics.
  • Security Constraints: For applications with stringent security requirements where read and write operations need different levels of access control.

Example

Scenario. E-commerce Platform

// Command Model
public class CreateProductCommand
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// Command Handler
public class CreateProductCommandHandler
{
    public void Handle(CreateProductCommand command)
    {
        // Add logic to create a new product in the database
        var product = new Product { Name = command.Name, Price = command.Price };
        // Save product to the database
    }
}

// Query Model
public class ProductQueryModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// Query Handler
public class ProductQueryHandler
{
    public ProductQueryModel GetProductById(int id)
    {
        // Query the read model to get product details by Id
        // Return a ProductQueryModel
        return new ProductQueryModel { Id = id, Name = "Sample Product", Price = 19.99m };
    }
}

In this CQRS example, we have commands for placing orders and a separate read model for querying order details, suitable for an e-commerce platform.

Exploring MediatR

MediatR, on the other hand, is a library that simplifies the implementation of the Mediator pattern in C#. It provides a straightforward way to decouple components by promoting the use of a mediator to send requests and notifications between different parts of the application.

Benefits of using MediatR

Here are the key benefits of using MediatR.

  1. Decoupling: MediatR promotes loose coupling between components by abstracting the way requests are sent and handled. This enhances the maintainability and testability of your code.
  2. Single Responsibility Principle (SRP): It encourages adhering to the SRP by keeping request and response handling in separate classes, making your codebase more modular.
  3. Pipeline Behavior: MediatR allows you to implement pipeline behaviors, enabling cross-cutting concerns like logging, validation, or caching to be applied consistently to multiple request handlers.
  4. Extensibility: You can easily extend MediatR to handle various types of requests and integrate it with other libraries and frameworks.

Use Cases for MediatR

  • Reducing Coupling: When you want to reduce tight coupling between different parts of your application, making it more maintainable and adaptable to change.
  • Modularity: In scenarios where you want to enforce the Solid Responsibility Principles (SRP) and keep your codebase modular by separating request and response handling.
  • Cross-cutting Concerns: For implementing cross-cutting concerns such as logging, validation, and caching consistently across multiple request handlers.

Example

Scenario. Microservices Architecture

// Define a request
public class CalculateShippingCostQuery : IRequest<decimal>
{
    public int OrderId { get; set; }
}

// Define a handler for the request
public class CalculateShippingCostQueryHandler : IRequestHandler<CalculateShippingCostQuery, decimal>
{
    public async Task<decimal> Handle(CalculateShippingCostQuery request, CancellationToken cancellationToken)
    {
        // Query a separate shipping microservice to calculate shipping cost
        var shippingCost = await _shippingService.CalculateCostAsync(request.OrderId);
        return shippingCost;
    }
}

// In your microservice
var mediator = new Mediator(); // Instantiate MediatR's mediator
var shippingCost = await mediator.Send(new CalculateShippingCostQuery { OrderId = 123 });

In this MediatR example, we have a query for calculating shipping costs, suitable for a microservices architecture where services communicate through requests.

CRUD

CRUD is a fundamental acronym in software development that represents the Create, Read, Update, and Delete operations on data. It's not a specific architectural pattern but rather a common way to describe how data is manipulated in applications.

Characteristics and benefits of CRUD

Here are some characteristics and benefits of CRUD.

  1. Simplicity: CRUD operations are simple and intuitive, making them easy to implement and understand, especially for smaller applications or prototypes.
  2. Quick Development: When time-to-market is critical, CRUD provides a straightforward approach to building basic data-driven applications rapidly.
  3. Small-Scale Projects: CRUD is well-suited for small-scale projects or applications with basic data manipulation requirements.
  4. Database Abstraction: It often abstracts the underlying database operations, allowing developers to focus on application logic rather than SQL queries.

Use Cases for CRUD

  • Prototyping: In the early stages of a project or for rapid prototyping, where simplicity and speed are paramount.
  • Small-Scale Applications: For small-scale applications or utilities where complex architectural patterns might introduce unnecessary complexity.
  • Learning Purposes: For educational purposes or when teaching basic database interactions and application development.

Example

Scenario. Personal Blog

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
}

public class BlogPostRepository
{
    private readonly List<BlogPost> _posts = new List<BlogPost>();

    public void Create(BlogPost post)
    {
        // Add a new blog post to the list
        _posts.Add(post);
    }

    public BlogPost Read(int id)
    {
        // Find and return a blog post by Id
        return _posts.FirstOrDefault(p => p.Id == id);
    }

    public void Update(BlogPost post)
    {
        // Find the blog post by Id and update its properties
        var existingPost = _posts.FirstOrDefault(p => p.Id == post.Id);
        if (existingPost != null)
        {
            existingPost.Title = post.Title;
            existingPost.Content = post.Content;
        }
    }

    public void Delete(int id)
    {
        // Find and remove a blog post by Id
        var postToRemove = _posts.FirstOrDefault(p => p.Id == id);
        if (postToRemove != null)
        {
            _posts.Remove(postToRemove);
        }
    }
}

In this CRUD example, we have a simple blog post management system suitable for a personal blog scenario mentioned in the article.

Scenarios Where Each Technique Shines

Now that we've explored the characteristics and benefits of CQRS, MediatR, and CRUD, let's look at scenarios where each technique excels:

1. CQRS

  • E-commerce Platform: In an e-commerce platform, where you need to handle high loads of read operations for product listings and low-latency write operations for order processing.
  • Finance and Banking: For financial applications, where auditing and traceability are crucial, and you want to maintain a complete history of transactions using event sourcing.

2. MediatR

  • Microservices Architecture: In a microservices-based application, where you want to maintain loose coupling between services and ensure each service has its own set of request handlers.
  • RESTful APIs: When building RESTful APIs, MediatR can help structure the request-handling process and enforce the separation of concerns.

3. CRUD

  • Personal Blog: If you're building a personal blog or a simple content management system, CRUD operations may suffice for managing articles and comments.
  • To-Do List Application: A straightforward to-do list application where the primary operations involve creating, reading, updating, and deleting tasks.

Conclusion

The choice between CQRS, MediatR, and CRUD depends on the specific needs and complexity of your application. CQRS excels in scenarios requiring scalability, complex queries, and event sourcing, while MediatR provides a clean way to decouple components and enforce the SRP. CRUD, on the other hand, offers simplicity and quick development for smaller-scale applications. Understanding the strengths and weaknesses of each technique will empower you to make informed architectural decisions, ensuring that your application meets its requirements efficiently and effectively.