In this article we discuss about CQRS pattern with MediatR in ASP.Net Core. We discuss about more details on CQRS design pattern, and how to implement this pattern in a real world example with MediatR at .NET Core. CQRS is one of the most commonly used patterns that helps architect the Solution to accommodate the Onion Architecture in .NET Core. Please follow my previous article on ASP.NET Core Web API with MongoDB CRUD- Beginner’s Guide.
What are the issue in Traditional Architecture?
- In traditional architectures, the same data model is used to query and update a database. That’s simple and works well for basic CRUD operations.
- In complex applications this approach can become unwieldy. For example, on the read side, the application may perform many different queries, returning data transfer objects (DTOs) with different shapes. Object mapping can become complicated. On the write side, the model may implement complex validation and business logic. As a result, you can end up with an overly complex model that does too much.
- Read and write workloads are often asymmetrical, with very different performance and scale requirements.
- The traditional approach can have a negative effect on performance due to the load on the data store and data access layer, and the complexity of queries required to retrieve information.
- Managing security and permissions can become complex because each entity is subject to both read and write operations, which might expose data in the wrong context.
What is CQRS Pattern ?
CQRS(Command Query Responsibility Segregation) is a design pattern that separated the read and write operations of a data source, hence decoupling the solution to a great extend. Command refers to a Database Command, which can be either an Insert / Update or Delete Operation, whereas Query stands for Querying data from a source.
CQRS allows an application to work with different models, One model that has data needed to update a record, another model to insert a record, yet another to query a record. This gives us flexibility with varying and complex scenarios. You don’t have to rely on just one DTO for the entire CRUD Operations by implementing CQRS.
Pros of CQRS
- Optimized Data Transfer Objects: One model per data operation that gives us all the flexibility to get rid of the complex model class.
- Highly Scalable:Â As full control over the model classes in CQRS the data operations makes application highly scalable in the long run.
- Improved Performance: In all enterprises application there are always Read operations is more compared to write operation. using this pattern we should speed up the performance on the read operations by introducing a cache or NOSQLDB.
- Secure Parallel Operations:Â Since we have dedicated models per operation, there is no possibility of data loss while doing parallel operations.
Cons of CQRS
- Code complexation: As we are introducing a new design pattern so we may write more code lines to integrate CQRS.
What is MediatR Pattern ?
In software engineering, the mediator pattern defines an object that encapsulates how a set of objects interact. This pattern is considered to be a behavioral pattern due to the way it can alter the program’s running behavior. In object-oriented programming, programs often consist of many classes.
In simply we can see Mediator pattern is another design pattern that dramatically reduces the coupling between various components of an application by making them communicate indirectly, usually via a special mediator object. We will dive deep into Mediator in another post. Essentially, the Mediator pattern is well suited for CQRS implementation.
Creating ASP.NET Core project and Implement CQRS Pattern
Let’ s build an ASP.NET Core WebAPI 5.0 project to implementation on CQRS Pattern. We build an ASP.NET Core Web API endpoint that does CRUD operations for a Transaction Entity, i.e Create/ Update/ Delete product record from the Database.
- Open Visual studio 2019 and create ASP.NET Core Web API project.
- The target framework is .NET 5.0 and tick on Enable OpenAPI support to enable swagger.
Install Packages to implement CQRS
Install-Package Microsoft.EntityFrameworkCore Install-Package Microsoft.EntityFrameworkCore.Relational Install-Package Microsoft.EntityFrameworkCore.SqlServer Install-Package MediatR Install-Package MediatR.Extensions.Microsoft.DependencyInjection Install-Package Microsoft.EntityFrameworkCore.Tools
Adding the Transaction Model
Let’s add the transaction model inside Entity folder.
public class Transaction { public int TransactionId { get; set; } public string AccountNumber { get; set; } public string BeneficiaryName { get; set; } public string BankName { get; set; } public string SWIFTCode { get; set; } public int Amount { get; set; } public DateTime Date { get; set; } }
Adding the Context Class and Interface
- Create a new Folder called Context and add a class named as TransactionDBContext. This class enables to access the data using EF core ORM. Also create the interface ITransactionDbContext.
public interface ITransactionDbContext { DbSet<Transaction> Transactions { get; set; } Task<int> SaveChanges(); }
public class TransactionDbContext : DbContext, ITransactionDbContext { public TransactionDbContext(DbContextOptions<TransactionDbContext> options) : base(options) { } public DbSet<Transaction> Transactions { get; set; } public async Task<int> SaveChanges() { return await base.SaveChangesAsync(); } }
Define the Connection String in appsettings.json
We define a connection string in the appsettings.json found within the API Project.
"ConnectionStrings": { "connectionstring": "Server=SEREVERNAME;Database=paymentDB;Trusted_Connection=True;MultipleActiveResultSets=true" }
Configuring the API Services in Startup
Navigate to your API Project’s Startup class and add these lines to your Startup Class ConfigureServices method. This will register the EF Core with the application.
services.AddDbContext<TransactionDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("connectionstring"), b => b.MigrationsAssembly(typeof(TransactionDbContext).Assembly.FullName)));
Generating the Database using EF Core
To generate the database use the Package Manager Console of Visual Studio. You can open this by going to Tools -> Nuget Package Manager -> Package Manager Console.
add-migration "initial"
After adding the above command you can find the new folder with Migrations is created on below.
Then update database using below command.
update-database
After executing the command you can see now it create database with migrations.
Configuring MediatR in ASP.NET Core Web API Project
Mediator pattern is another design pattern that dramatically reduces the coupling between various components of an application by making them communicate indirectly. MediatR is a library that helps implements Mediator Pattern in .NET.
To register the library, we need to add at the end of our API startup class in the ConfigureService Method.
services.AddMediatR(Assembly.GetExecutingAssembly());
Implement the CRUD Operations
Let’s implement CRUD(Create, Read, Update, and Delete) operations using MediatR pattern.
Let’s Create a Folder name as Features and under this folder can create another folder name as TranscationFeatures and then create subfolders named as the Queries and Command.
Commands
The commands folder only consist of methods those are use on transaction related to database. Let’s add the classes to TranscationFeatures Commands folder.
- CreateTransactionCommand
- UpdateTransactionCommand
- DeleteTranscationByIdCommand
Create Transaction Command
public class CreateTransactionCommand : IRequest<int> { public int TransactionId { get; set; } public string AccountNumber { get; set; } public string BeneficiaryName { get; set; } public string BankName { get; set; } public string SWIFTCode { get; set; } public int Amount { get; set; } public DateTime Date { get; set; } public class CreateProductCommandHandler : IRequestHandler<CreateTransactionCommand, int> { private readonly ITransactionDbContext _context; public CreateProductCommandHandler(ITransactionDbContext context) { _context = context; } public async Task<int> Handle(CreateTransactionCommand command, CancellationToken cancellationToken) { var tran = new Transaction(); tran.AccountNumber = command.AccountNumber; tran.BeneficiaryName = command.BeneficiaryName; tran.BankName = command.BankName; tran.SWIFTCode = command.SWIFTCode; tran.Amount = command.Amount; tran.Date = command.Date; _context.Transactions.Add(tran); await _context.SaveChanges(); return tran.TransactionId; } } }
- #3-9 – We create model property to create transaction command.
- #12-16 – Constructor Inject the database context.
- #17-29 – Assign the model that push into the transactions table.
Update Transaction Command
public class UpdateTransactionCommand : IRequest<int> { public int Id { get; set; } public string AccountNumber { get; set; } public string BeneficiaryName { get; set; } public string BankName { get; set; } public string SWIFTCode { get; set; } public int Amount { get; set; } public DateTime Date { get; set; } public class UpdateTransactionCommandHandler : IRequestHandler<UpdateTransactionCommand, int> { private readonly ITransactionDbContext _context; public UpdateTransactionCommandHandler(ITransactionDbContext context) { _context = context; } public async Task<int> Handle(UpdateTransactionCommand command, CancellationToken cancellationToken) { var tran = _context.Transactions.Where(a => a.TransactionId == command.Id).FirstOrDefault(); if (tran == null) { return default; } else { tran.AccountNumber = command.AccountNumber; tran.BeneficiaryName = command.BeneficiaryName; tran.BankName = command.BankName; tran.SWIFTCode = command.SWIFTCode; tran.Amount = command.Amount; tran.Date = command.Date; await _context.SaveChanges(); return tran.TransactionId; } } } }
Delete Transaction Command
public class DeleteTranscationByIdCommand : IRequest<int> { public int Id { get; set; } public class DeleteProductByIdCommandHandler : IRequestHandler<DeleteTranscationByIdCommand, int> { private readonly ITransactionDbContext _context; public DeleteProductByIdCommandHandler(ITransactionDbContext context) { _context = context; } public async Task<int> Handle(DeleteTranscationByIdCommand command, CancellationToken cancellationToken) { var transcation = _context.Transactions.Where(a => a.TransactionId == command.Id).FirstOrDefault(); if (transcation == null) return default; _context.Transactions.Remove(transcation); await _context.SaveChanges(); return transcation.TransactionId; } } }
Delete transaction takes place considering the transaction by ID.
Queries
The Queries folder only consist of methods that are fetched from database only means not any READ/WRITE operation to database.
Get All Transaction Query
public class GetAllTranscationsQuery : IRequest<IEnumerable<Transaction>> { public class GetAllTranscationsQueryHandler : IRequestHandler<GetAllTranscationsQuery, IEnumerable<Transaction>> { private readonly ITransactionDbContext _context; public GetAllTranscationsQueryHandler(ITransactionDbContext context) { _context = context; } public async Task<IEnumerable<Transaction>> Handle(GetAllTranscationsQuery query, CancellationToken cancellationToken) { var transcationList = await _context.Transactions.ToListAsync(); if (transcationList == null) { return null; } return transcationList.AsReadOnly(); } } }
- Line #5-9: Constructor Inject the DB Context interface.
- Line #10-18 : Here we make all the transaction details that are available in DB.
Get Transaction By Id
public class GetTranscationByIdQuery : IRequest<Transaction> { public int Id { get; set; } public class GetTranscationByIdQueryHandler : IRequestHandler<GetTranscationByIdQuery, Transaction> { private readonly ITransactionDbContext _context; public GetTranscationByIdQueryHandler(ITransactionDbContext context) { _context = context; } public async Task<Transaction> Handle(GetTranscationByIdQuery query, CancellationToken cancellationToken) { var transcation = _context.Transactions.Where(a => a.TransactionId == query.Id).FirstOrDefault(); if (transcation == null) return null; return transcation; } } }
This return values by transaction Id.
Create the Transaction API Controller
Let’s create a transaction API controller that make the CRUD operation using the Feature service of Commands and Queries.
[Route("api/[controller]")] [ApiController] public class TranscationController : ControllerBase { private IMediator _mediator; protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>(); [HttpGet] public async Task<IActionResult> GetAll() { return Ok(await Mediator.Send(new GetAllTranscationsQuery())); } [HttpGet("{id}")] public async Task<IActionResult> GetById(int id) { return Ok(await Mediator.Send(new GetTranscationByIdQuery { Id = id })); } [HttpPost] public async Task<IActionResult> Create(CreateTransactionCommand command) { return Ok(await Mediator.Send(command)); } [HttpPut("{id}")] public async Task<IActionResult> Update(int id, UpdateTransactionCommand command) { if (id != command.Id) { return BadRequest(); } return Ok(await Mediator.Send(command)); } [HttpDelete("{id}")] public async Task<IActionResult> Delete(int id) { return Ok(await Mediator.Send(new DeleteTranscationByIdCommand { Id = id })); } }
- Line #5-6: MediatR interface is injected.
- Line #7-35 : We create the CRUD Operation call. Here Create, Update, Delete is call to commands and for Read the transaction call from Queries.
Configure Service method in Startup
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<TransactionDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("connectionstring"), b => b.MigrationsAssembly(typeof(TransactionDbContext).Assembly.FullName))); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "CQRS_MediatR", Version = "v1" }); }); services.AddControllers(); services.AddScoped<ITransactionDbContext>(provider => provider.GetService<TransactionDbContext>()); services.AddMediatR(Assembly.GetExecutingAssembly()); }
Test the CQRS with MediatR using Swagger
Let’s test our CRUD application using Swagger. Build & Run the application
POST method test using swagger
Let’s test the POST method using swagger with required inputs like below and then hit execute button.
Now when you navigate to database and we can see the transaction is added in the table.
GET method test using swagger
Try to execute the GET method we can see the result
You can test the CRUD using Swagger using this way.
Conclusion
Leave behind your valuable queries and suggestions in the comment section below. Also, if you think this article helps you, do not forget to share this with your developer community. Happy Coding 🙂
Related Articles
- .NET 8 Authentication with Identity in a Web API using Bearer Tokens and Cookies
- How to convert Text To Speech With Azure Cognitive Services using Angular and .Net Core
- CRUD operation using the repository pattern with .Net 8, Ef-Core, and MySQL
- How to use Response Compression in .NET Core
- How to Integrate GraphQL in .Net Core
Jayant Tripathy
Coder, Blogger, YouTuberA passionate developer keep focus on learning and working on new technology.