Building Robust ASP.NET Core Web APIs with CQRS and MediatR

Introduction

In the world of web development, creating robust and maintainable APIs is crucial. ASP.NET Core has emerged as a powerful framework for building web applications and APIs. When it comes to designing APIs that are both scalable and maintainable, Command Query Responsibility Segregation (CQRS) and MediatR can be your best friends. In this article, we'll explore how to build an ASP.NET Core Web API using CQRS and MediatR, backed up with practical examples.

What are CQRS and MediatR?

Command Query Responsibility Segregation (CQRS) is a design pattern that separates the read (query) and write (command) operations of an application into two distinct parts. It helps improve scalability, maintainability, and performance. In CQRS, commands are used to modify the data, while queries are used to retrieve data.

MediatR is a library that simplifies the implementation of the Mediator pattern in C#. It provides an easy way to decouple components in your application by using a mediator to send and receive messages (commands and queries). This makes your code more organized and easier to test.

Setting up an ASP.NET Core Web API Project

To get started, let's create an ASP.NET Core Web API project using Visual Studio or the .NET CLI. Open your terminal and run:

dotnet new webapi -n MyApi

This will create a new ASP.NET Core Web API project named "MyApi."

Adding the Required NuGet Packages

To use CQRS and MediatR in your project, you'll need to add some NuGet packages. Open your project in Visual Studio or your favorite code editor and add the following packages:

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
dotnet add package FluentValidation
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
  • MediatR is the core library for implementing the Mediator pattern.
  • FluentValidation is used for input validation and validation rules.
  • AutoMapper.Extensions.Microsoft.DependencyInjection is used for mapping data between DTOs (Data Transfer Objects) and domain models.

Creating the CQRS Structure

Next, let's create the folder structure for our CQRS implementation. In your project, add folders for Commands, Queries, Handlers, Models, and Validators. This will help keep your code organized.

Defining Commands and Queries

Commands and Queries are at the heart of CQRS. Commands represent actions that modify data, while Queries represent actions that retrieve data. Let's create a simple example.

Command

// Commands/CreateProductCommand.cs

public class CreateProductCommand : IRequest<int>
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Query

// Queries/GetAllProductsQuery.cs

public class GetAllProductsQuery : IRequest<List<ProductDto>>
{
}

Creating Handlers

Handlers are responsible for executing Commands and Queries. Let's create handlers for our example Command and Query.

Command Handler

// Handlers/CreateProductCommandHandler.cs

public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
    private readonly ApplicationDbContext _context;

    public CreateProductCommandHandler(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price
        };

        _context.Products.Add(product);
        await _context.SaveChangesAsync();

        return product.Id;
    }
}

Query Handler

// Handlers/GetAllProductsQueryHandler.cs

public class GetAllProductsQueryHandler : IRequestHandler<GetAllProductsQuery, List<ProductDto>>
{
    private readonly ApplicationDbContext _context;
    private readonly IMapper _mapper;

    public GetAllProductsQueryHandler(ApplicationDbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    public async Task<List<ProductDto>> Handle(GetAllProductsQuery request, CancellationToken cancellationToken)
    {
        var products = await _context.Products.ToListAsync();
        return _mapper.Map<List<ProductDto>>(products);
    }
}

Adding Validation

We can use FluentValidation for input validation. Create a validator for the CreateProductCommand.

// Validators/CreateProductCommandValidator.cs

public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
    public CreateProductCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Price).GreaterThan(0);
    }
}

Setting up AutoMapper

AutoMapper is a great tool for mapping between DTOs and domain models. Let's set it up in your Startup.cs.

// Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // ...
    
    services.AddAutoMapper(typeof(Startup));
}

Configuring MediatR

Now, let's configure MediatR in your Startup.cs.

// Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddMediatR(typeof(Startup));
}

Creating Controllers

Finally, let's create controllers for handling the API requests.

// Controllers/ProductController.cs

[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
    private readonly IMediator _mediator;

    public ProductController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> CreateProduct(CreateProductCommand command)
    {
        var productId = await _mediator.Send(command);
        return Ok(productId);
    }

    [HttpGet]
    public async Task<IActionResult> GetAllProducts()
    {
        var query = new GetAllProductsQuery();
        var products = await _mediator.Send(query);
        return Ok(products);
    }
}

Conclusion

In this article, we've explored how to build an ASP.NET Core Web API using CQRS and MediatR. This design pattern helps improve the organization and maintainability of your code by separating commands and queries, making your API more robust and scalable. Additionally, we've seen how to add input validation with FluentValidation and use AutoMapper for mapping between DTOs and domain models.

By implementing CQRS and MediatR in your ASP.NET Core Web API projects, you can create clean and maintainable code that is easy to extend and test, making your development process more efficient and your application more robust.