CQRS with .NET 8 + MediatR

In this article, I will show you how to implement CQRS in .NET 8 and C# 13 with MediatR and Mapster for auto-mapping an entity result into a Data Transformation Object.

CQRS (Command Query Responsibility Segregation) is a powerful design pattern often used in modern, scalable applications, because it promotes a clean separation of concerns between reading and writing operations.

Mediator is a pattern where objects don’t communicate with each other directly. Instead, they send requests or messages through a mediator, which then decides which handler or service should process the request. This helps decouple components in an application, leading to more maintainable and testable code.

Data Transformation Objects (DTO) transfer data between different parts of an application or between services, in a clean and lightweight format typically using Records or Structs.

Benefits of CQRS

  • Separation of Concerns: By decoupling the logic for reading and updating data, each side can be optimized independently.
  • Performance and Scalability: You can use complex validation rules and processes on the write side while having fast, lightweight queries on the read side. This is useful in high-load systems where you need different scaling strategies.
  • Maintenance and Testing: By splitting the command and query logic, your code becomes simpler and more modular. This makes it easier to maintain and test each part independently.
  • Flexibility: The write side might use a relational database to ensure consistency, while the read side could use a NoSQL database or an in-memory cache to serve data faster.
  • Security: Write operations might require more stringent authentication and authorization checks, while read operations may have more lenient rules (depending on the data being queried).
  • Domain Driven Design: CQRS allows the write side to handle complex business rules, aggregates, and domain models, while simplifying the read side by exposing simple views or projections of data.

ICommandHandler

In my Clean Architecture project, I implemented ICommand and ICommandHandler interfaces in the Application Layer, leveraging the MediatR pattern. Each command and query is wrapped in a Result object, which encapsulates both success and failure states. There are two versions of the I Command interface: one is used when the operation only needs to return a Result, and the other is for cases where you want to return a Result<TResponse>, allowing the return of a specific object after a successful operation.

// ICommand
using MediatR;

// Inherit from MediatR's IRequest interface used to represent requests handled by MediatR. These requests could be commands that perform state-changing operations.
namespace BlazorCleanArchitecture.Application.Abstractions.RequestHandler
{
public interface ICommand : IRequest<Result>
{
}
public interface ICommand<TResponse> : IRequest<Result<TResponse>>
{
}
}

// ICommandHandler
using MediatR;

namespace BlazorCleanArchitecture.Application.Abstractions.RequestHandler
{
// CQRS Pattern
// Command Handler for commands that do not return any specific result other than success or failure
public interface ICommandHandler<TCommand> : IRequestHandler<TCommand, Result>
where TCommand : ICommand
{
}
// CQRS Pattern
// Command Handler for commands that do return a specific response type
public interface ICommandHandler<TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>>
where TCommand : ICommand<TResponse>
{
}
}

ICommandHandler Usage

This code is an implementation of a command handler for updating an article in a Clean Architecture project using the MediatR pattern.

using BlazorCleanArchitecture.Application.Exceptions;
using BlazorCleanArchitecture.Application.Users;

namespace BlazorCleanArchitecture.Application.Articles.UpdateArticle
{
public class UpdateArticleCommandHandler : ICommandHandler<UpdateArticleCommand, ArticleDto?>
{
private readonly IArticleRepository _articleRepository;
private readonly IUserService _userService;
public UpdateArticleCommandHandler(IArticleRepository articleRepository, IUserService userService)
{
_articleRepository = articleRepository;
_userService = userService;
}
public async Task<Result<ArticleDto?>> Handle(UpdateArticleCommand request, CancellationToken cancellationToken)
{
try
{
var updateArticle = request.Adapt<Article>();
if (!await _userService.CurrentUserCanEditArticleAsync(request.Id))
{
return Result.Fail<ArticleDto?>("You are not authorized to edit this article.");
}
var result = await _articleRepository.UpdateArticleAsync(updateArticle);
if (result is null)
{
return Result.Fail<ArticleDto?>("Failed to get this article.");
}
return result.Adapt<ArticleDto>();
}
catch (UserNotAuthorizedException)
{
return Result.Fail<ArticleDto?>("An error occurred updating this article.");
}
}
}
}
  • Dependencies: The handler receives an IArticleRepository and an IUserService via dependency injection. These services are used to interact with the articles and check user permissions, respectively.
  • Handling the Command: The Handle method is invoked to process the UpdateArticleCommand. It receives the command object (request), which contains the data required to update the article, along with a cancellation token for async operations.
  • Command ExecutionAdapt Article Object: The article from the command (UpdateArticleCommand) is mapped into an Article entity using a mapping method (request.Adapt<Article>()), likely from a library like Mapster.
  • Authorization Check: Before proceeding, the handler checks if the current user has permission to edit the article using _userService.CurrentUserCanEditArticleAsync(request.Id). If unauthorized, it returns a failure result with an appropriate message ("You are not authorized to edit this article.").
  • Updating the Article: The handler then attempts to update the article via the repository (_articleRepository.UpdateArticleAsync(updateArticle)). If the update fails (i.e., the repository returns null), it returns a failure result with a message like "Failed to get this article.".
  • Successful Update: If the update is successful, the article is adapted to ArticleDto and returned as a success result.
  • Error Handling: The code handles a specific UserNotAuthorizedException. If this exception is thrown, it catches it and returns a failure result with an error message ("An error occurred updating this article.").

IQueryHandler

This code defines a generic interface IQueryHandler<TQuery, TResponse> in the BlazorCleanArchitecture project, adhering to the CQRS (Command Query Responsibility Segregation) pattern. It is responsible for handling queries (read-only operations) that implement the IQuery<TResponse> interface. The IQueryHandler extends from MediatR’s IRequestHandler, meaning it processes a query (TQuery) and returns a Result<TResponse>, where TResponse is the expected type of the result. By enforcing that TQuery must implement IQuery<TResponse>, it ensures that only valid query types are handled, maintaining the separation between commands (write operations) and queries (read operations) in the system.

// IQuery
using MediatR;

namespace BlazorCleanArchitecture.Application.Abstractions.RequestHandler
{
// CQRS QUERY PATTERN
// The IQuery interface inherits from the IRequest interface, meaning it can be processed by a mediator.
// Returns Result<TResponse> which wraps both success and failure outcomes for the query.
public interface IQuery<TResponse> : IRequest<Result<TResponse>>
{
}
}

// IQueryHandler
using MediatR;

namespace BlazorCleanArchitecture.Application.Abstractions.RequestHandler
{
// CQRS QUERY PATTERN
// Query Handler responsible for handling queries of type IQuery<TResponse>
// Extends from IRequestHandler
// Responsible for processing a query (TQuery) and returning the corresponding result (Result<TResponse>)
// where TQuery : IQuery<TResponse> ensures that TQuery is a valid query (i.e., it must implement IQuery<TResponse>)
public interface IQueryHandler<TQuery, TResponse> : IRequestHandler<TQuery, Result<TResponse>>
where TQuery : IQuery<TResponse>
{
}
}

IQueryHandler Usage

This code is an implementation of a query handler for retrieving an article by it’s Id in a Clean Architecture project using the MediatR pattern.

// GetArticleById Query
namespace BlazorCleanArchitecture.Application.Articles.GetArticleById
{
public class GetArticleByIdQuery : IQuery<ArticleDto?>
{
public int Id { get; set; }
}
}

// GetArticleById QueryHandler
public class GetArticleByIdQueryHandler : IQueryHandler<GetArticleByIdQuery, ArticleDto?>
{
private readonly IArticleRepository _articleRepository;
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
public GetArticleByIdQueryHandler(IArticleRepository articleRepository, IUserRepository userRepository, IUserService userService)
{
_articleRepository = articleRepository;
_userRepository = userRepository;
_userService = userService;
}
public async Task<Result<ArticleDto?>> Handle(GetArticleByIdQuery request, CancellationToken cancellationToken)
{
const string _default = "Unknown";
var article = await _articleRepository.GetArticleByIdAsync(request.Id);
if (article is null)
{
return Result.Fail<ArticleDto?>("Failed to get article.");
}
var articleDto = article.Adapt<ArticleDto>();
if (article.UserId is not null)
{
var author = await _userRepository.GetUserByIdAsync(article.UserId);
articleDto.UserName = author?.UserName ?? _default;
articleDto.UserId = article.UserId;
articleDto.CanEdit = await _userService.CurrentUserCanEditArticleAsync(article.Id);
}
return articleDto;
}
}

Conclusion

In conclusion, implementing CQRS with MediatR and Mapster in .NET 8 and C# 13 provides a clean, scalable approach to handling complex application logic. By decoupling commands and queries, you achieve a more maintainable and testable architecture, while leveraging DTOs ensures efficient data transfer between different layers or services. This combination of patterns and tools promotes clarity, separation of concerns, and optimal performance in modern .NET applications, making it an excellent choice for developers building scalable solutions.