CQRS Abstraction Layer on MediatR in .NET 8 Microservices

Mehmet Ozkaya
5 min readMar 23, 2024

--

We will create a clear separation between commands and queries using MediatR, enhancing the clarity and maintainability of our code.

To implement CQRS pattern effectively, we’ll create specialized interfaces for commands and queries.

I have just published course — .NET 8 Microservices: C# 12, DDD, CQRS, Vertical/Clean Architecture.

🛠️ Building the Foundation

To kick things off, we establish our groundwork by introducing specialized interfaces for commands and queries within our microservices architecture. This involves creating a BuildingBlocks class library, which will house our CQRS abstractions and be shared across all microservices.

public interface ICommand<out TResponse> : IRequest<TResponse> { }
public interface IQuery<out T> : IRequest<T> { }

These are generic and normal methods with inherited from IRequest — MediatR. By adhering to these interfaces, we clearly distinguish between operations that alter the system’s state (commands) and those that retrieve data (queries), reinforcing the CQRS principle.

🎨 Crafting the BuildingBlocks Project

Our BuildingBlocks project is more than just a repository of interfaces; it's the cornerstone of our microservices ecosystem. It includes common functionalities, libraries, and, most importantly, our CQRS abstractions. This strategic move not only promotes reusability but also ensures consistency across our microservices landscape.

🔄 Separating Commands and Queries

In the spirit of CQRS, we meticulously design our system to handle commands and queries distinctly:

  • Commands: Represent actions that change the state of the system. They are processed by command handlers, which encapsulate the business logic required to execute the command.
  • Queries: Solely responsible for fetching data. Query handlers, dedicated to servicing these requests, ensure efficient data retrieval without side effects.
public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse> { }
public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse> { }

This interface will extend MediatR’s IRequestHandler but will be specific to handling command requests.

🌐 Integrating with Microservices

With our CQRS abstractions ready, we seamlessly integrate them into our microservices, starting with the Catalog microservice. This involves refactoring existing handlers to align with our ICommand and IQuery interfaces, thereby reinforcing the separation of commands and queries.

We remove the MediatR package from our Catalog.API project to avoid conflicts and ensure that all MediatR-related functionality is comes through our BuildingBlocks library.
Next, we add a project reference to the BuildingBlocks library in our Catalog.API project. This step provides that our microservice has access to the newly created CQRS abstractions.

🚀 Kickstarting with ICommand in Catalog Microservice

Transitioning from MediatR’s IRequest to a more CQRS-aligned ICommand sets the stage. This explicit separation underlines our commitment to distinguishing between actions that alter state (commands) and those that retrieve state (queries).

public record CreateProductCommand(string Name, List<string> Category, string Description, string ImageFile, decimal Price)
: ICommand<CreateProductResult>;

Here, CreateProductCommand encapsulates all the necessary data for creating a new product, adhering to the CQRS principle by clearly defining a command.

🛠️ The Command Handler

Our CreateProductCommandHandler takes center stage, bearing the responsibility of handling the CreateProductCommand. Implementing ICommandHandler, it's primed to process the command and orchestrate the creation of a new product.

internal class CreateProductCommandHandler : ICommandHandler<CreateProductCommand, CreateProductResult>
{
public Task<CreateProductResult> Handle(CreateProductCommand command, CancellationToken cancellationToken)
{
// Logic to handle command execution
throw new NotImplementedException();
}
}

💡 Breathing Life into Logic

With the scaffolding in place, we delve into the heart of our handler — the Handle method. This method is where the business logic to create a new product resides, from constructing a new Product entity to persisting it in the database.

public Task<CreateProductResult> Handle(CreateProductCommand command, CancellationToken cancellationToken)
{
var product = new Product
{
Name = command.Name,
Category = command.Category,
Description = command.Description,
ImageFile = command.ImageFile,
Price = command.Price
};
// Placeholder for database persistence logic
throw new NotImplementedException();
}

🌐 Exposing Through Minimal APIs

The final piece of the puzzle lies in exposing this capability through our API. Leveraging ASP.NET Core’s Minimal APIs, we’ll define an endpoint that listens for POST requests to create new products, seamlessly integrating with MediatR to dispatch our CreateProductCommand.

This endpoint will receive POST requests to create new products and use MediatR to send CreateProductCommand to our handler.

According to image, we have completed to internal part of request which is considered application layer cqrs implementation with command and handler classes. So now we will expose endpoints with request and response objects using minimal apis and carter library.

🎨 Develop POST Endpoint with Carter implements ICarterModule for Minimal Apis

Let’s begin by implementing the ICarterModule interface in our CreateProductEndpoint class. This interface allows us to define routes in a structured way.

using Carter;
using Mapster;
using MediatR;

namespace Catalog.API.Products.CreateProduct;

public record CreateProductRequest(string Name, List<string> Category, string Description, string ImageFile, decimal Price);

public record CreateProductResponse(Guid Id);

public class CreateProductEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapPost("/products", async (CreateProductRequest request, ISender sender) =>
{
var command = request.Adapt<CreateProductCommand>();

var result = await sender.Send(command);

var response = result.Adapt<CreateProductResponse>();

return Results.Created($"/products/{response.Id}", response);
})
.WithName("CreateProduct")
.Produces<CreateProductResponse>(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Create Product")
.WithDescription("Create Product");
}
}

In this implementation, we map the incoming request to a CreateProductCommand, send it via MediatR, and adapt the result back to a CreateProductResponse.

🚀 Conclusion

The journey to implementing CQRS with MediatR is both enlightening and transformative. By abstracting commands and queries, we foster a more organized, scalable, and maintainable microservices architecture. This separation not only aligns with the principles of clean architecture but also enhances our system’s ability to evolve and adapt in the ever-changing landscape of software development.

I have just published course — .NET 8 Microservices: C# 12, DDD, CQRS, Vertical/Clean Architecture.

This is step-by-step development of reference microservices architecture that include microservices on .NET platforms which used ASP.NET Web API, Docker, RabbitMQ, MassTransit, Grpc, Yarp API Gateway, PostgreSQL, Redis, SQLite, SqlServer, Marten, Entity Framework Core, CQRS, MediatR, DDD, Vertical and Clean Architecture implementation with using latest features of .NET 8 and C# 12.

--

--

Mehmet Ozkaya
Mehmet Ozkaya

Written by Mehmet Ozkaya

Software Architect | Udemy Instructor | AWS Community Builder | Cloud-Native and Serverless Event-driven Microservices https://github.com/mehmetozkaya

No responses yet