.NET 6 Web API, CQRS and MediatR Library

Hello, in this article, we will work on the Command Query Responsibility Segregation (CQRS) and Mediator design principles together.

The MediatR library is based on two main principles: CQRS and Mediator.

You can access the codes of the project from the following link: https://github.com/onurkarakus/CqrsMediatorPattern

First of all, what is CQRS? I will try to explain it a bit.

As the name suggests, CQRS is a principle of separating commands and queries. In the structures we usually use, we try to manage CRUD operations through a single repository. CQRS brings a design logic that is built on this separation.

Command objects are defined as objects that make changes on data (CUD), while queries are objects that perform reading operations (R) on data.

The working logic of the CQRS principle

As you can see, this principle supports the complete separation of read and command operations within the application. Here, a specific boundary or method for the separation process is not explicitly defined. The separation process can be done based on a model, application, or database if desired. Of course, these rules can change depending on the size and complexity of the application.

The short definition and basic principle of this principle can be defined as follows:

“A method should change the state of an object or return a value. It should not do both.”

Advantages:

  • The segregation of commands and queries prevents complexity in the creation of ViewModel objects used to present data to users from data models (mapping).
  • If you are designing an application that includes a large number of database operations, you must prioritize performance, which means that reading and writing operations must be separate objects, allowing for separate control, management, and performance optimization.
  • There will be fewer operation lock-ups (e.g. db lock) because reading and writing operations can be scaled separately.
  • It ensures that the application codes are clear and readable.
  • It can speed up the adaptation of new programmers to the project.

Disadvantages:

  • If you are developing a small-scale application, adopting the CQRS principle can be overwhelming.
  • Using only the CQRS principle creates a lot of code development needs, causing the development time of the project to extend.

What is Mediator?

The Mediator Design Pattern is a principle that provides us with the opportunity to reduce dependencies between objects. By starting from the idea of “the class with the least dependencies is the easiest to use”, this design pattern aims to make objects have the same interface by adapting the same interface. This principle aims to ensure that objects communicate not with each other, but through this interface.

With this pattern, the objects we use communicate with each other not through their dependencies, but through an intermediary.

A common example given when researching is the airport, tower, and plane example. Planes communicating with the tower and not with each other as long as it is not necessary can be a simple example of this principle.

MediatR library is a library that makes it easier for us to implement the Mediator Design Pattern.

Now, let’s try to examine the CQRS design pattern example on a project first. Then, let’s make our project more user-friendly with the MediatR library using the Mediator design pattern.

Create a new ASP.NET Core Web API project and name it.

Creation screen for the project named Cqrs Mediatr Pattern

When our project is first created, we should clean up the default files that come with it to keep it organized.

Our scenario is simple. We have an application containing book information and an enum value that holds the types of these books. Let’s perform CRUD operations on the application.

To do this, we create folders named Domain -> Entities, Domain -> Enum and define our object definitions.

We have three new object definitions.

The Book.cs that will store our book information.

using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.Domain.Entities;

public class Book
{
public long Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public BookGenre BookGenre { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
}

BookGenre.cs which is the enum type we will hold the book types.

namespace CqrsMediatorPattern.Domain.Enums;

public enum BookGenre
{
None = 0,
Horror = 1,
Fable = 2,
Fiction = 3,
Mystery = 4
}

For our database operations, we need to add Entity Framework Core packages to our project. For this, you can install the following packages as package manager (Nuget Packet Manager), CLI or package reference (PacketReference).

Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore
Adding packages via Nuget package manager

Now we can design our Dbcontext class. By opening a folder named Data -> Context, we design our Dbcontext class there. We are making an arrangement so that the name of our class is BookDbContext.

BookDbContext.cs
using CqrsMediatorPattern.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace CqrsMediatorPattern.Data.Context;

public class BookDbContext : DbContext
{
public BookDbContext(DbContextOptions<BookDbContext> options) : base(options)
{


}

public DbSet<Book> Books { get; set; }
}

We need to update the ConnectionString information in our appsettings.json file. We add the information as follows.

{your_username} : Username specified for Sql Server
{your password} : Your password for Sql Server

{
"ConnectionString": {
"SqlDbConnectionString": "Data Source=.\\SQLEXPRESS; Initial Catalog=CqrsMediatr_example;Persist Security Info=True;User Id={kullanici_adiniz};Password={şifreniz};MultipleActiveResultSets=true;TrustServerCertificate=True"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Then we need to register the DbContext object that we created in our Program.cs class. For this, we add as follows in the Program.cs class.

using CqrsMediatorPattern.Data.Context;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Then, we run the following commands on the Package Manager Console, respectively, to create our database and tables.

Add-Migration Init
Update-Database
Add-Migration
Update-Database

Then, when we check it over Sql Server, we can see that the database and tables have been created.

Şimdi CQRS için geliştirmelerimizi içeriyor. İlk olarak CQRS prensibini hayata geçireceğiz. İyi ve kötü yanlarını kod olarak gördükten sonra Mediator ilkelerinin yararları ve MediatR kütüphanesinin kullanımına bakacağız.

Öncelikle CQRS isminde bir klasör açarak Commands, Handlers ve Queries bilgilerimizi burada oluşturuyoruz.

CQRS folder and its contents

Commands: It will be the folder where all the command (CUD) operations to be made within the application are defined.

Queries: It will be the folder where all the query (R) operations to be made within the application are defined.

Handler : It will be the folder where our models are defined, which will process all commands and queries at a common point and return the answer that will be suitable for users.

Now let’s design our objects in the Commands folder.

CreateBookCommandRequest.cs and CreateBookCommandResponse.cs

using CqrsMediatorPattern.Domain.Entities;
using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class CreateBookCommandRequest
{
public string? Title { get; set; }
public string? Description { get; set; }
public BookGenre? BookGenre { get; set; }
}
namespace CqrsMediatorPattern.CQRS.Commands.Response;

public class CreateBookCommandResponse
{
public bool IsSuccess { get; set; }
public int CreatedBookId { get; set; }
}

DeleteBookCommandRequest.cs and DeleteBookCommandResponse.cs

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class DeleteBookCommandRequest
{
public int BookId { get; set; }
}
namespace CqrsMediatorPattern.CQRS.Commands.Response;

public class DeleteBookCommandResponse
{
public bool IsSuccess { get; set; }
}
Final state after adding our command objects

Now let’s design our objects in the Queries folder.

GetAllBooksQueryRequest.cs and GetAllBookQueryResponse.cs

namespace CqrsMediatorPattern.CQRS.Queries.Request;

public class GetAllBooksQueryRequest
{

}
using CqrsMediatorPattern.Domain.Enums;

namespace CqrsMediatorPattern.CQRS.Queries.Response;

public class GetAllBooksQueryResponse
{
public long Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public BookGenre BookGenre { get; set; }
public DateTime Created { get; set; }
public DateTime Updated { get; set; }
}

Now it’s time for the Handlers folder. Here we will develop structures that will process command (Command) and query (Query) requests.

CommandHandlers

CreateBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using CqrsMediatorPattern.Domain.Entities;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class CreateBookCommandHandler
{
private readonly BookDbContext _dbContext;

public CreateBookCommandHandler(BookDbContext dbContext)
{
this._dbContext = dbContext;
}

public CreateBookCommandResponse CreateBook(CreateBookCommandRequest request)
{
_ = _dbContext.Books.Add(new Book
{
BookGenre = request.BookGenre.Value,
Created = DateTime.Now,
Description = request.Description,
Title = request.Title
});

var idInformation = _dbContext.SaveChanges();

return new CreateBookCommandResponse { IsSuccess= true, CreatedBookId = idInformation };
}
}

DeleteBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class DeleteBookCommandHandler
{
private readonly BookDbContext _dbContext;

public DeleteBookCommandHandler(BookDbContext dbContext)
{
this._dbContext = dbContext;
}

public DeleteBookCommandResponse DeleteBook(DeleteBookCommandRequest request)
{
var findBookResult = _dbContext.Books.FirstOrDefault(p => p.Id == request.BookId);

if (findBookResult == null)
{
return new DeleteBookCommandResponse { IsSuccess = false };
}

_dbContext.Books.Remove(findBookResult);

_ = _dbContext.SaveChanges();

return new DeleteBookCommandResponse { IsSuccess = true };
}
}

QueryHandlers

GetAllBooksQueryHandler.cs

using CqrsMediatorPattern.CQRS.Queries.Request;
using CqrsMediatorPattern.CQRS.Queries.Response;
using CqrsMediatorPattern.Data.Context;

namespace CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;

public class GetAllBooksQueryHandler
{
private readonly BookDbContext _dbContext;

public GetAllBooksQueryHandler(BookDbContext dbContext)
{
this._dbContext = dbContext;
}

public List<GetAllBooksQueryResponse> GetAllBooks(GetAllBooksQueryRequest request)
{
return _dbContext.Books.Select(x => new GetAllBooksQueryResponse()
{
BookGenre = x.BookGenre,
Created = x.Created,
Description = x.Description,
Id = x.Id,
Title = x.Title,
Updated = x.Updated
}).ToList();
}
}

Basically, we have developed our classes with the CQRS principle. Now we need to add the classes we developed as Handler to our application as services and then develop our Controller class. The final version of our Program.cs class will be as follows.

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddTransient<CreateBookCommandHandler>();
builder.Services.AddTransient<DeleteBookCommandHandler>();
builder.Services.AddTransient<GetAllBooksQueryHandler>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Now let’s develop our Controller class. We create an API Controller named BookController inside our Controllers folder.

The final version of our BookController.cs class will be as follows.

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.CQRS.Queries.Request;
using Microsoft.AspNetCore.Mvc;

namespace CqrsMediatorPattern.Controllers;

[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
readonly CreateBookCommandHandler createBookCommandHandler;
readonly DeleteBookCommandHandler deleteBookCommandHandler;
readonly GetAllBooksQueryHandler getAllBooksQueryHandler;

public BookController(CreateBookCommandHandler createBookCommandHandler, DeleteBookCommandHandler deleteBookCommandHandler, GetAllBooksQueryHandler getAllBooksQueryHandler)
{
this.createBookCommandHandler = createBookCommandHandler;
this.deleteBookCommandHandler = deleteBookCommandHandler;
this.getAllBooksQueryHandler = getAllBooksQueryHandler;
}

[HttpGet]
public IActionResult Get([FromQuery] GetAllBooksQueryRequest request)
{
var allBooks = getAllBooksQueryHandler.GetAllBooks(request);

return Ok(allBooks);
}

[HttpPost]
public IActionResult Post([FromBody] CreateBookCommandRequest request)
{
CreateBookCommandResponse response = createBookCommandHandler.CreateBook(request);

return Ok(response);
}

[HttpDelete("{id}")]
public IActionResult Delete([FromQuery] DeleteBookCommandRequest request)
{
DeleteBookCommandResponse response = deleteBookCommandHandler.DeleteBook(request);
return Ok(response);
}
}

Then we can run our project and test it via Swagger.

We will first add a book and then take the books as a list.

Adding a New Book
New Book Added Result
Book List
Book List Result
Bookinformation in the database

As you can see, we were able to separate our Command and Query objects by sending our requests through the BookController and perform our operations with different flows.

We performed these operations for a single object and table. But as the project grows, the number of Command and Query objects will increase. It seems that the classes we add to our application as a service will increase and DI operations of feature Controller classes will begin to be a problem.

Another bad situation is that we determine the response information of the incoming request information. Jobs such as determining the Handler information that is appropriate according to the incoming request on the side of the software will also cause difficulties for us in the development stages of the project and increase the complexity.

In such a case, the Mediator principle will support us and by adding this principle with the MediatR library, we will be able to use our CQRS objects more easily and comfortably. In this way, we will be able to ensure that our Command, Query and Handler objects work and are managed in accordance with their behavior.

Let’s start preparing MediatR developments together.

First, we start by adding the MediatR library to our project.

To add the MediatR library to our project, you can install the following packages as package manager (Nuget Packet Manager), CLI or package reference (PacketReference).

MediatR
MediatR.Extensions.Microsoft.DependencyInjection
Installing MediatR with Nuger package manager

Then we perform the service registration process in our Program.cs class.

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddTransient<CreateBookCommandHandler>();
builder.Services.AddTransient<DeleteBookCommandHandler>();
builder.Services.AddTransient<GetAllBooksQueryHandler>();

builder.Services.AddMediatR(typeof(Program));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

The MediatR library offers us two interfaces. Thanks to these interfaces, we can determine our Request and RequestHanler objects and enable them to take their responsibilities in accordance with the Mediator principle.

These interfaces are as follows.

IRequest :

namespace MediatR
{
//
// Summary:
// Marker interface to represent a request with a void response
public interface IRequest : IRequest<Unit>, IBaseRequest
{
}
}

IRequestHandler :

namespace MediatR
{
//
// Summary:
// Defines a handler for a request
//
// Type parameters:
// TRequest:
// The type of request being handled
//
// TResponse:
// The type of response from the handler
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
{
//
// Summary:
// Handles a request
//
// Parameters:
// request:
// The request
//
// cancellationToken:
// Cancellation token
//
// Returns:
// Response from the request
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
}

Now let’s rearrange all our Command, Query and Handler classes that we created earlier for MediatR. What we will do here is to ensure that the CommandRequest objects adapt the IRequest interfaces, and our Handler objects adapt the IRequestHandler interfaces.

CreateBookCommandRequest.cs

using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Domain.Enums;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class CreateBookCommandRequest: IRequest<CreateBookCommandResponse>
{
public string? Title { get; set; }
public string? Description { get; set; }
public BookGenre? BookGenre { get; set; }
}

DeleteBookCommandRequest.cs

using CqrsMediatorPattern.CQRS.Commands.Response;
using MediatR;
namespace CqrsMediatorPattern.CQRS.Commands.Request;

public class DeleteBookCommandRequest: IRequest<DeleteBookCommandResponse>
{
public int BookId { get; set; }
}

GetAllBooksQueryRequest.cs

using CqrsMediatorPattern.CQRS.Queries.Response;
using MediatR;
namespace CqrsMediatorPattern.CQRS.Queries.Request;

public class GetAllBooksQueryRequest:IRequest<List<GetAllBooksQueryResponse>>
{
}

Now let’s make changes to our Handler objects. We will implement an interface like IRequestHandler<request,response> for Handler objects. Then we will add the gleen Handler method with the interface. The difference here is that the newly added Handle method will work as async.

CreateBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using CqrsMediatorPattern.Domain.Entities;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class CreateBookCommandHandler: IRequestHandler<CreateBookCommandRequest, CreateBookCommandResponse>
{
private readonly BookDbContext _dbContext;

public CreateBookCommandHandler(BookDbContext dbContext)
{
this._dbContext = dbContext;
}

public Task<CreateBookCommandResponse> Handle(CreateBookCommandRequest request, CancellationToken cancellationToken)
{
_ = _dbContext.Books.Add(new Book
{
BookGenre = request.BookGenre.Value,
Created = DateTime.Now,
Description = request.Description,
Title = request.Title
});

var idInformation = _dbContext.SaveChanges();

return Task.FromResult(new CreateBookCommandResponse { IsSuccess = true, CreatedBookId = idInformation });
}
}

DeleteBookCommandHandler.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.Data.Context;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;

public class DeleteBookCommandHandler : IRequestHandler<DeleteBookCommandRequest, DeleteBookCommandResponse>
{
private readonly BookDbContext _dbContext;

public DeleteBookCommandHandler(BookDbContext dbContext)
{
this._dbContext = dbContext;
}

public Task<DeleteBookCommandResponse> Handle(DeleteBookCommandRequest request, CancellationToken cancellationToken)
{
var findBookResult = _dbContext.Books.FirstOrDefault(p => p.Id == request.BookId);

if (findBookResult == null)
{
return Task.FromResult(new DeleteBookCommandResponse { IsSuccess = false });
}

_dbContext.Books.Remove(findBookResult);

_ = _dbContext.SaveChanges();

return Task.FromResult(new DeleteBookCommandResponse { IsSuccess = true });
}
}

GetAllBooksQueryHandler.cs

using CqrsMediatorPattern.CQRS.Queries.Request;
using CqrsMediatorPattern.CQRS.Queries.Response;
using CqrsMediatorPattern.Data.Context;
using MediatR;

namespace CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;

public class GetAllBooksQueryHandler: IRequestHandler<GetAllBooksQueryRequest, List<GetAllBooksQueryResponse>>
{
private readonly BookDbContext _dbContext;

public GetAllBooksQueryHandler(BookDbContext dbContext)
{
this._dbContext = dbContext;
}

public Task<List<GetAllBooksQueryResponse>> Handle(GetAllBooksQueryRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(_dbContext.Books.Select(x => new GetAllBooksQueryResponse()
{
BookGenre = x.BookGenre,
Created = x.Created,
Description = x.Description,
Id = x.Id,
Title = x.Title,
Updated = x.Updated
}).ToList());
}
}

Now we are making our BookController class suitable for MediatR. The change we will make here is that MediatR directs our classes for us, by injecting itself into the Controller class and leaving the routing process to the library.

BookController.cs

using CqrsMediatorPattern.CQRS.Commands.Request;
using CqrsMediatorPattern.CQRS.Commands.Response;
using CqrsMediatorPattern.CQRS.Queries.Request;
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace CqrsMediatorPattern.Controllers;

[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
readonly IMediator mediator;

public BookController(IMediator mediator)
{
this.mediator = mediator;
}

[HttpGet]
public async Task<IActionResult> Get([FromQuery] GetAllBooksQueryRequest request)
{
var allBooks = await mediator.Send(request);

return Ok(allBooks);
}

[HttpPost]
public async Task<IActionResult> Post([FromBody] CreateBookCommandRequest request)
{
CreateBookCommandResponse response = await mediator.Send(request);

return Ok(response);
}

[HttpDelete("{id}")]
public async Task<IActionResult> Delete([FromQuery] DeleteBookCommandRequest request)
{
DeleteBookCommandResponse response = await mediator.Send(request);

return Ok(response);
}
}

As you can see, we have removed the injection information of our Handler classes that we have added before. We rearranged our Controller methods to be async and left all the work to the object we added with the name of the mediator.

Finally, we can remove our Handler services that we previously registered in our Program.cs class.

Program.cs

using CqrsMediatorPattern.CQRS.Handlers.CommandHandlers;
using CqrsMediatorPattern.CQRS.Handlers.QueryHandlers;
using CqrsMediatorPattern.Data.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContextPool<BookDbContext>(
options => options.UseSqlServer(builder.Configuration["ConnectionString:SqlDbConnectionString"]));

builder.Services.AddMediatR(typeof(Program));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

When we run our application and list the books, we will see that the books are coming without any problems.

In this article, we examined the conveniences that the CQRS principle provides us in terms of performance, management and development while creating large-scale applications. We tried to examine how we can produce easy solutions with the MediatR library for CQRS, which comes with its good sides as well as bad sides, by examining the Mediator principle.

The CQRS structure can be used alone or together with Meditor when desired, providing us with convenience in development.

See you in another review…

You can reach the Github address of the project at the following address.

https://github.com/onurkarakus/CqrsMediatorPattern