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

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

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

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

2. MediatR

3. CRUD

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.