Menu Close

CQRS pattern with MediatR in ASP.NET Core 5.0

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.

#Find Source Code

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.

CQRS_Architecture

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.

In this example we consider a Payment account Transaction CRUD operation and see how to segregate the CRUD using CQRS in MediatR.

  • 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.
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.

CQRS-mediatR

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

CQRS-mediatR-swagger

POST method test using swagger

Let’s test the POST method using swagger with required inputs like below and then hit execute button.

CQRS-mediatR-swagger-post

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

CQRS-mediatR-swagger-test

You can test the CRUD using Swagger using this way.

#Find Source Code

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

Jayant Tripathy
Coder, Blogger, YouTuber

A passionate developer keep focus on learning and working on new technology.

Leave a Reply

Your email address will not be published.