Let's Build: CQRS and MediatR Pattern using ASP.Net WEB API

Njeri Muriithi - Feb 24 - - Dev Community

Introduction

In this article, we will be implementing the MediatR and CQRS (Command Query Responsibility Segregation) pattern in our ASP.NET Web API project

CQRS stands for Command Query Responsibility segregation:

This is a design pattern that separates read(Query) and Write(Command) operations in a system to improve performance and scalability.
The command side handles create, update and delete requests and the query side handles the Get request.

The MediatR:

Acts as a bridge between command and query operations, helping with coordination. It facilitates communication between components by introducing a central handler. Instead of components interacting directly, they send requests to the mediatR, which routes them to the appropriate handler.

The main goal of the MediatR pattern is to reduce dependencies between objects by restricting direct communication. This improves maintainability and scalability.

When a command or query is sent, the mediatR directs the request to the corresponding handler, which then processes either a command (write operation) or a query (read operation).

Contents

  1. Query Operations
  2. Get Transaction By Name
  3. Command Operation

Applying CQRS and MediatR in Our Project(Iwallet Api)

From our previous project, where we built an iWallet using ASP.NET Web API_ https://dev.to/njeri_muriithi/lets-build-with-aspnet-web-api-iwallet-4k2g_ we will now refactor it to utilize the CQRS pattern with the MediatR pattern.
Getting Started:

Install MediatR from the NuGet package in Microsoft Visual Studio or by using the .NET CLI in Visual Studio Code:

dotnet add package MediatR

Register or inject MediatR in the application's entry point (Program.cs) as follows:

builder.Services.AddMediatR(cfg=>cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly));

Query Operations:

This includes all the read operations

Step1:Create a new folder for Queries
Create a folder named “Queries”, where the read operations are defined.

  • GetAllTransactionsQuery

Create a class GetAllTransactionsQuery that inherit from the IRequest<Out TResponse>
The generic type Tresponse represents the expected response can be of any type in this case <IEnumerable<DailyExpense>>

using MediatR;
using Wallet_API.Models;
namespace Wallet_API.Queries
{
 public class GetAllTransactionsQuery:IRequest<IEnumerable<DailyExpense>>
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

Step1.2:Create Handler for the Query
Create a folder named "Handlers", where the implementation logic for queries and commands will be written.

Create class _GetAllTransactionHandler _that implements the IRequestHandler<in TRequest, TResponse>

TRequest -> GetAllTransactionsQuery
TResponse-> IEnumerable

Inject the Iwallet interface used to fetch the transactions and the Imapper for DTO Transformations
Then implement the IRequestHandler Interface handle method to call the GetTransactions method implemented by the Iwallet interface and return the list of expenses

namespace Wallet_API.Handlers
{
public class GetAllTransactionHandler : IRequestHandler<GetAllTransactionsQuery, IEnumerable<DailyExpense>>
    {
        protected readonly IWallet _wallet;
        protected readonly IMapper _mapper;
        public GetAllTransactionHandler(IWallet wallet,IMapper mapper)                                          
        {
           _wallet = wallet; 
            _mapper = mapper; 
        }
public async Task<IEnumerable<DailyExpense>>Handle
(GetAllTransactionsQuery request, CancellationToken cancellationToken)
         {
            var expenses = await _wallet.GetTransactions();
            return expenses;
        }                                       
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 1.3: Define the HTTP GET Endpoint in the Controller

  • Define the HTTP Get EndPoint in the controller.
  • Instantiate the query option (GetAllTransactionsQuery).
  • Use _mediatR.Send(query) to send the request

MediatR dispatches the request to the corresponding handler (GetAllTransactionHandler). The handler in this case _GetAllTransactionHandler _ then processes the request and returns a response

Note: MediatR has two methods:

_mediatR.Send() → Sends a request and may return a response and is one sender to only one handler.
_mediatR.Publish()-> publish has no return type and is One sender to many handlers.

Modify your controller to include MediatR as a dependency. Inject IMediator in the constructor to use it for handling requests.

public class DailyTransactionsController : ControllerBase
 {

   private readonly IMediator _mediatR ;
  public DailyTransactionsController(IMediator mediator)
 {

     _mediatR = mediator;
 }
  [HttpGet]
public async Task<ActionResult<IEnumerable<DailyExpenseDto>>> GetTransactions()
  {    
      var query = new GetAllTransactionsQuery();
      var result = await _mediatR.Send(query);   
      return Ok(result);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step2:Get Transaction By Name

This section implements the functionality of getting the transaction by name.

  • On the Queries folder Create a new class GetTransactionNameNameQuery and Inherit from the IRequest<DailyExpense>.
  • The query requires the _TransactionName _as a parameter
  • Create a constructor to accept and initialize the TransactionName.
namespace Wallet_API.Queries
{
    public class GetTransactionByNameQuery:IRequest<DailyExpense>
    {
        public string TransactionName {  get;}

        public GetTransactionByNameQuery(string transactionName)                                        

        {
            TransactionName = transactionName;
        }   
    }

}

Enter fullscreen mode Exit fullscreen mode

Step 2.1:Get Transaction By Name Handler

  • Create the _GetTransactionByNameHandler _class in the handler folder.
  • Implement IRequestHandler<GetTransactionByNameQuery, DailyExpense>.
  • Inject dependencies (IWallet and IMapper).
  • Call _wallet.GetExpenseByName(request.TransactionName). the TransactionName parameter comes from the GetTransactionByNameQuery request.
  • Return _null _if no transaction is found.
namespace Wallet_API.Handlers
{
    public class GetTransactionByNameHandler : IRequestHandler<GetTransactionByNameQuery, DailyExpense>
    {
        protected readonly IWallet _wallet;
        protected readonly IMapper _mapper;
        public GetTransactionByNameHandler(IWallet wallet,
                                          IMapper mapper)
        {
            _wallet = wallet;
            _mapper = mapper;
        }
  public  async Task <DailyExpense>Handle(GetTransactionByNameQuery request,CancellationToken cancellationToken)
       {
    var Expenses  await_wallet.GetExpenseByName(request.TransactionName);
            return  Expenses == null ? null: Expenses;          

        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 2.2: Define the HTTP GET by Name Endpoint in the Controller

  • Define the [HttpGet("TransactionByName/{TransactionName}")] route.
  • The route expects a string TransactionName.
  • Instantiate GetTransactionByNameQuery with the provided TransactionName.
  • Use _mediatR.Send(query) to dispatch the request. When the request is sent via MediatR, it carries the TransactionName, which is then used inside the handler to fetch the relevant data
  • If no result is found, return NotFound(). Otherwise, return Ok(result).
 [HttpGet("Transaction By Name/{TransactionName}")]
 public async Task<ActionResult<DailyExpense>> GetTransactionByName(string TransactionName)
 {     
    var query = new GetTransactionByNameQuery(TransactionName);
     var results = await _mediatR.Send(query);   

     return results ==null? NotFound() : results;
 } 
Enter fullscreen mode Exit fullscreen mode

Command Operation:

Step 3.1: Create a Folder for Commands
Create a folder named "Commands", where the write operations will be defined.

  • Create Transaction Request
    This section implements the functionality to add a new transaction.

  • Create a class CreateTransactionRequest that inherits from IRequest<TResponse>.

  • The generic type TResponse represents the expected response, which can be of any type. In this case, it will be DailyExpense.

  • Define the incoming request, which is a DailyExpenseDto, then create a constructor.

namespace Wallet_API.Commands
{
    public class CreateTransactionRequest:IRequest<DailyExpense>
    {
      public DailyExpenseDto ExpenseDto { get; }

        public CreateTransactionRequest(DailyExpenseDto expenseDto)
        {
            ExpenseDto = expenseDto;
        }   
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3.2: Create a Handler for the Command

  • Create a new handler CreateTransactionHandler that implements IRequestHandler<CreateTransactionRequest, DailyExpense>.
  • Like previous handlers, inject the required dependencies: IWallet and IMapper.
  • Implement the Handle method to add a new transaction.
  • Use AutoMapper to map DailyExpenseDto to DailyExpense.
  • Call the AddTransaction method to save the transaction.
  • Return the newly created transaction.
namespace Wallet_API.Handlers
{
    public class GetNewTransactionHandler : IRequestHandler<CreateTransactionRequest, DailyExpense>
    {
        protected readonly IWallet _wallet;
        protected readonly IMapper _mapper;
        public GetNewTransactionHandler(IWallet wallet, IMapper mapper)
        {
            _wallet = wallet;
            _mapper = mapper;
        }
        public async Task<DailyExpense> Handle(CreateTransactionRequest 
      request, CancellationToken cancellationToken)
        {         
            var expense = _mapper.Map<DailyExpense>(request.ExpenseDto);     
           await _wallet.AddTransaction(expense);
           return _mapper.Map<DailyExpense>(expense);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3.3: Define the HTTP POST Endpoint in the Controller

  • Define the [HttpPost] endpoint.
  • The route expects a DailyExpenseDto object.
  • Instantiate _CreateTransactionRequest _with the provided expenseDto.
  • Use _MediatR.Send() to send the command, which will be handled by CreateTransactionHandler.
  • Return the result.
public async Task<ActionResult<DailyExpenseDto>> AddTransaction([FromBody] DailyExpenseDto dailyExpenseDto)
{
   var command = new CreateTransactionRequest(dailyExpenseDto);
     var createdTransaction = await _mediatR.Send(command); 
    return Ok(createdTransaction);

}

Enter fullscreen mode Exit fullscreen mode
  • This format also works for both HTTP DELETE and UPDATE operations.
  • DELETE _would use a command like DeleteTransactionRequest, which takes an TransactionName.
  • UPDATE _would use UpdateTransactionRequest, which takes an updated DailyExpenseDto.

Conclusion

The CQRS and MediatR design patterns are great for scaling, especially in complex projects, as they enhance maintainability.

Thank you for reading ❤️ Stay awesome! 🚀

. . . . . . .