ASP.NET Core: Implementing CQRS and MediatR pattern in ASP.NET Core API

In this article, we will see an implementation of the Command Query Responsibility Segregation (CQRS) pattern in ASP.NET Core API with MediatR.  In my earlier article, I have already explained the CQRS. In this article, we will focus on Mediator Pattern. 

Mediator Pattern

Mediator is a behavioral design pattern that reduces coupling between components of the program. This is achieved by using a special mediator object. This object decouples components by managing indirect communication across them. This pattern makes the application easy to extend, reuse, and modify individual components because they do not have any direct dependency across them. Figure 1, will provide a good idea of the Mediator pattern.



Figure 1: The Mediator Pattern

As shown in Figure 1, the Mediator object is responsible for listening to requests from each service object and passing them to the actual Handler object hence implementing the decoupling across the Services and Handlers. This helps to further maintain the usability of the application even if the handlers are extended, changed, etc.    

Using MediatR in ASP.NET Core API for Implementing CQRS 

The MediatR is a simple Mediator implementation in .NET.  This supports request/response, Commands, Queries, Notifications, and events, Synchronous and Asynchronous intelligent dispatching via C#. Following are some of the types exposed by MediatR

  • IMediator, this defines a Mediator for encapsulation of request/response and publishing interaction pattern. 
  • IRequest, a Marker interface that represents a request with a response.
  • IRequetHandler, this is used to define a handler for the request

Figure 2 shows the implementation of the ASP.NET Core API Application



Figure 2: The CQRS with the MidaitR

As shown in Figure 2, we will be creating separate APIs for Read and Write operations. These APIs will use Query and Command to make data Read and Write requests and the MedtatR object will handle these requests using Query and Command Handlers to perform database operations.


The Implementation

We will be implementing this application using entity Framework Core (EF Core), MediatR, AutoMapper, and Custom Middleware.  I have already explained the Auto MApper on this link. The Listing 1 shows the Database query


Create Database Company;
GO
USE [Company]
GO

 
CREATE TABLE [dbo].[ProductInfo](
	[ProductId] [char](20) Primary Key,
	[ProductName] [varchar](200) NOT NULL,
	[Manufacturer] [varchar](200) NOT NULL,
	[Description] [varchar](400) NOT NULL,
	[BasePrice] [decimal](18, 0) NOT NULL,
	[Tax] as ([BasePrice] * 0.2),
	[TotalPrice]  AS ([BasePrice]+([BasePrice] * 0.2)),
)
select * from ProductInfo

Insert into [ProductInfo] values('Prd-0001','Laptop','MS-IT','Gaming Laptop',120000)

Insert into [ProductInfo] values('Prd-0002','Iron','MS-Ect','Household Cotton Iron',2000)
Insert into [ProductInfo] values('Prd-0003','Water Bottle','MS-Home Appliances','2 Lts. Cold Storage',200)
Insert into [ProductInfo] values('Prd-0004','RAM','MS-IT','32 GB DDR 6 Fast Memory',20000)
Insert into [ProductInfo] values('Prd-0005','Mixer','MS-Ect','Kitchen Appliances',16000)
Insert into [ProductInfo] values('Prd-0006','Mouse','MS-IT','Optical USB Gaming Mouse',1000)
Insert into [ProductInfo] values('Prd-0007','Keyboard','MS-IT','Gaming Lights 110 Keys Stroke Kayboard',2000)

Listing 1: The Database Query

 Step 1: Open Visual Studio 2022 and create a new ASP.NET Core API Project Targetted to .NET 7 r .NET 8. Name this project as Core_CQRS_Mediatr. In this project, add the following NuGet Packages

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworCore.Relational 
  • Microsoft.EntityFrameworCore.SqlServer 
  • Microsoft.EntityFrameworCore.Tools
  • Microsoft.EntityFrameworCore.Design
  • AutoMapper
  • MediatR

Step 2: Using Entity Framework Core Database-First Approach for the Company database generate Models. Run the command shown in Listing 2 to Use the EF Core Database-First approach

dotnet ef dbcontext scaffold "Data Source=.;Initial Catalog=Company;Integrated Security=SSPI" Microsoft.EntityFrameworkCore.SqlServer -o Models


Listing 2: The EF Core Database-First Approach

The Models folder will be created in the project with the CompanyContext and ProductInfo classes in it.

Step 3:  Since we will be creating Read and Write APIs we need to manage the responses in encapsulated class. To do this, in the Models folder, add a class file named ResponseBase.cs. In this file, we will class the ReasponseBase class that contains properties for responses generated from the APIs. Listing 3 shows the code for the ResponseBase class


 public class ResponseBase
 {
     public string? Message { get; set; }
     public int StatusCode { get; set; }
     public bool IsSuccess { get; set; } = false;
 }
Listing 3: ResponseBase class
 
From the API, we will be responding single record or collection of records in the API response, to define the response object for a single record, in the Models folder we will add a class file named ResponseRecord.cs with class in it as shown in Listing 4

 public class ResponseRecord<TEntity>: ResponseBase where TEntity : class
 {
    public TEntity? Record { get; set; }
 }
  
Listing 4: A Single Record Response

To return a collection of records in response, in the Models folder let's add a new class file named ResponseRecords.cs with the code in it as shown in Listing 5

public class ResponseRecords<TEntity> : ResponseBase where TEntity : class
{
    public IEnumerable<TEntity>? Records { get; set; }
}

Listing 5: The Collection Response class
 
Step 4: The ProductInfo model class has various properties for showing Product details, Tax, and TotalPrice, but in response, we do not want to send Tax and TotalPrice property values. To remove these values from the response we will use the Auto Mapper. The need for Auto Mapper is already posted by me in another article which can be read from this link. In the project, let's add a new folder named ViewModel. In this folder, we will add a class named ProductInfoViewModel in the ProductInfoViewModel.cs file as shown in Listing 6


public class ProductInfoViewModel
{
    [Display(Name ="Product Id")]
    [Required(ErrorMessage = "Product Id is required.")]
    public string ProductId { get; set; } = null!;
    [Display(Name = "Product Name")]
    [Required(ErrorMessage = "Product Name is required.")]
    public string ProductName { get; set; } = null!;
    [Display(Name = "Manufacturer")]
    [Required(ErrorMessage = "Manufacturer is required.")]
    public string Manufacturer { get; set; } = null!;
    [Display(Name = "Description")]
    [Required(ErrorMessage = "Description is required.")]
    public string Description { get; set; } = null!;
    [Display(Name = "Base Price")]
    [Required(ErrorMessage = "Base Price is required.")]
    public decimal BasePrice { get; set; }
}
Listing 6:  The ProductInfoViewModel class

This is one of the important steps that will help to filter the important information that we should not respond to the request received.  

Step 5: In this project, add a new folder named Mapper. In this folder, add a new class file named ProductInfoProfile.cs. In this class file, we will add code for the Mapper profile class as shown in Listing 7


public class ProductInfoProfile : Profile
{
    public ProductInfoProfile()
    {
        CreateMap<ProductInfo, ProductInfoViewModel>().ReverseMap();
    }
}
Listing 7: ProductInfoProfile class
 
Step 6: While executing the API application, if an exception occurs then we need to handle it and send the response to the request. To handle exceptions globally, we can use ASP.NET Core Middleware. I have already published an article on Middleware which you can read from this link. In this project, add a new folder named CustomMiddlewares. In this folder, add a new class file named ExceptionHandlerMiddleware.cs. In this class, let's write a logic or exception middleware as shown in Listing 8

using System.Security.Cryptography;

namespace Core_CQRS_Mediatr.CustomMIddlewares
{
    public class ErrorResponse
    {
        public string? ErrorMessage { get; set; }
        public int ErrorCode { get; set; }
    }

    /// <summary>
    /// The Custom Middeware Logic class
    /// </summary>
    public class ExceptinHandlerMidleware
    {
        RequestDelegate _next;

        public ExceptinHandlerMidleware(RequestDelegate next)
        {
            _next = next;
        }

        /// <summary>
        /// When this Middeware is applied in HttpPipeline, teh RequestDelegate will
        /// Auto-Invoke this method
        /// THis method will contain the Logic for the MIddleware
        /// </summary>
        /// <param name="ctx"></param>
        /// <returns></returns>
        public async Task InvokeAsync(HttpContext ctx)
        {
            try
            {
                // If no Error Then Continue with the HTTP Pipeline Execution
                // by invoking the next middleware in the Pipeline
                await _next(ctx);
            }
            catch (Exception ex)
            {
                // If error occures, handle the exception by 
                // listeng to the exception and send the error response
                // This will directly start the Http Response Process

                // Set the Response Error Code
                // Either Hard-Code it or therwise read it from
                // THe database or other configuration respures

                ctx.Response.StatusCode = 500;
                
                string message = ex.Message;

                var errorResponse = new ErrorResponse()
                { 
                   ErrorCode = ctx.Response.StatusCode,
                   ErrorMessage = message
                };

                // Generate the error response
                
                await ctx.Response.WriteAsJsonAsync(errorResponse);

            }
        }
    }

    // Create an Extension Method class that will register the ExceptinHandlerMidleware class
    // as custom Middlware

    public static class ErrorHandlerMiddlewareExtension
    {
        public static void UseErrorExtender(this IApplicationBuilder builder)
        {
            // Register the ExceptinHandlerMidleware class as custome middleware
            builder.UseMiddleware<ExceptinHandlerMidleware>();
        }
    }
}


Listing 8: The Custom Exception Middleware
 
Step 7: Now let's create queries for reading all records and a single record. So in this project, add a new folder named Queries. In this folder, add a new class file named GetProductInfoQuery.cs. In this class file, add the code for GetProductInfoQuery as shown in Listing 9

public class GetProductInfoQuery : IRequest<ResponseRecords<ProductInfo>>
{
}
Listing 9: The GetProductInfoQuery class
 
Similarly, in this folder, add a new class file named GetProductInfoByIdQuery.cs. In this class file, add code for the GetProductInfoByIdQuery class as shown in Listing 10

 public class GetProductInfoByIdQuery: IRequest<ResponseRecord<ProductInfo>>
 {
     public string? Id { get; set; }
 }

Listing 10: The GetProductInfoByIdQuery code

Listing 9 and Listing 10 show the code for reading all Products and a Single Product by Id respectively.
 
Step 8: In the project, add a new folder named Commands. In this folder, we will add classes for Command requests to make write requests to create, update, and delete records. In this folder, add a class file named RegisterProductInfoCommand.cs. In this class file, we will add code for the RegisterProductInfoCommand class. This class defines a property named ProductInfo of the type ProductInfo to write a new Product. The code for this class is shown in Listing 11.

 public class RegisterProductInfoCommand : IRequest<ResponseRecord<ProductInfo>>
 {
     public ProductInfo ProductInfo { get; set; }
     public RegisterProductInfoCommand(ProductInfo product)
     {
         ProductInfo = product;
     }
 }

Listing 11: The code for RegisterProductInfoCommand class

In the Commands folder, add a new class file named UpdateProductInfoCommand.cs. In this class file, we will add code for the UpdateProductInfoCommand class to make the command request for updating the Product. This class defines a ProductInfo property of the type ProductInfo. This data will be used to update an existing product. The code for this class is  shown in Listing 12

 public class UpdateProductInfoCommand : IRequest<ResponseRecord<ProductInfo>>
 {
     public ProductInfo ProductInfo { get; set; }

     public UpdateProductInfoCommand(ProductInfo product)
     {
         ProductInfo = product;
     }
 }
Listing 12: The UpdateProductInfoCommand class

In the Commands folder, we will add code for the Delete command to delete the product based on the Id property. In this folder, add a new class file named DeleteProductInfoCommand.cs. In this class file, we will add the code for the DeleteProductInfoCommand class that declares the Id property typed string. The code for DeleteProductInfoCommand class is shown in Listing 13
public class DeleteProductInfoCommand : IRequest<ResponseRecord<ProductInfo>>
{
    public string Id { get; set; }

    public DeleteProductInfoCommand(string id)
    {
        Id = id;
    }
}


Listing 13: The DeleteProductInfoCommand.cs class
  
Step 9: In the project, add a new folder named Services. In this folder, we will add code for the Repository interface and the Repository class to decouple the data access code from the controller classes. In this folder, add the code for the IDataAccessService interface by adding a new interface file named IDataAccessService.cs. The code for the IDataAccessService interface is shown in Listing 14
public interface IDataAccessService<TEntity, in TPk> where TEntity : class
{
    Task<ResponseRecords<TEntity>> GetAsync();
    Task<ResponseRecord<TEntity>> GetByIdAsync(TPk id);
    Task<ResponseRecord<TEntity>> CreateAsync(TEntity entity);
    Task<ResponseRecord<TEntity>> UpdateAsync(TPk id,TEntity entity);
    Task<ResponseRecord<TEntity>> DeleteAsync(TPk id);
}


Listing 14: The IDataAccessService interface code
 
In this folder, add a new class file named ProductInfoDataAccessService.cs. This class file will contain code for the ProductInfoDataAccessService class. This class implements the IDataAccessService interface and the ProductInfoDataAccessService class contains code for performing CRUD Operations using Entity Framework Core. The code for ProductInfoDataAccessService class is shown in Listing 15


 public class ProductInfoDataAccessService : IDataAccessService<ProductInfo, string>
 {

     CompanyContext Ctx;
     ResponseRecords<ProductInfo> ResponseRecords;
     ResponseRecord<ProductInfo> ResponseRecord;

     public ProductInfoDataAccessService(CompanyContext Ctx)
     {
         this.Ctx = Ctx;
         ResponseRecords = new ResponseRecords<ProductInfo>();
         ResponseRecord = new ResponseRecord<ProductInfo>();
     }

     async Task<ResponseRecord<ProductInfo>> IDataAccessService<ProductInfo, string>.CreateAsync(ProductInfo entity)
     {
          
             var result = await Ctx.ProductInfos.AddAsync(entity);
             await Ctx.SaveChangesAsync();
             ResponseRecord.Record = result.Entity;
             ResponseRecord.Message = "Record is Added Successfully.";
             ResponseRecord.IsSuccess = true;
             return ResponseRecord;
        
     }

     async Task<ResponseRecord<ProductInfo>> IDataAccessService<ProductInfo, string>.DeleteAsync(string id)
     {
         
             ResponseRecord.Record = await Ctx.ProductInfos.FindAsync(id);
             if (ResponseRecord.Record is null)
                 throw new Exception($"Recortd with ProductId : {id} is not found.");


             int recordDeleted = Ctx.ProductInfos.Where(prd => prd.ProductId == id).ExecuteDelete();

             if (recordDeleted == 0)
             {
                 ResponseRecord.Message = "Record Delete Failed";
                 ResponseRecord.IsSuccess = false; 
             }
             else
             {
                 ResponseRecord.Message = "Record is Deleted Successfully.";
                 ResponseRecord.IsSuccess = true;
             }
             return ResponseRecord;
          
     }

     async Task<ResponseRecords<ProductInfo>> IDataAccessService<ProductInfo, string>.GetAsync()
     {
          
             ResponseRecords.Records = await Ctx.ProductInfos.ToListAsync();
             ResponseRecords.Message = "Record is Read Successfully.";
             ResponseRecords.IsSuccess = true; 
             return ResponseRecords;
          
     }

     async Task<ResponseRecord<ProductInfo>> IDataAccessService<ProductInfo, string>.GetByIdAsync(string id)
     {
         
             ResponseRecord.Record = await Ctx.ProductInfos.FindAsync(id);
             ResponseRecord.Message = "Record is Read Successfully.";
             ResponseRecord.IsSuccess = true;
             return ResponseRecord;
         
     }

     async Task<ResponseRecord<ProductInfo>> IDataAccessService<ProductInfo, string>.UpdateAsync(string id, ProductInfo entity)
     {
        
             ResponseRecord.Record = await Ctx.ProductInfos.FindAsync(id);
             if (ResponseRecord.Record is null)
                 throw new Exception($"Record with ProductId : {id} is not found.");


             int recordUpdated = Ctx.ProductInfos
                 .Where(prd => prd.ProductId == id)
                 .ExecuteUpdate(setters => setters
                     .SetProperty(prd => prd.ProductName, entity.ProductName)
                     .SetProperty(prd => prd.Manufacturer, entity.Manufacturer)
                     .SetProperty(prd => prd.Description, entity.Description)
                     .SetProperty(prd => prd.BasePrice, entity.BasePrice)
               );
             if (recordUpdated == 0)
             {
                 ResponseRecord.Message = "Record Update Failed";
                 ResponseRecord.IsSuccess = false;
             }
             else
             {
                 ResponseRecord.Record = await Ctx.ProductInfos.FindAsync(id);
                 ResponseRecord.Message = "Record is Updated Successfully.";
                 ResponseRecord.IsSuccess = true;
             }
             return ResponseRecord;
          
     }
 }
Listing 15: The ProductInfoDataAccessService class
 

Step 10: Now the most important part of this implementation is to write Handlers. These handlers classes will implement the IRequestHandler interface. This is a generic interface that is typed to the Query class as well as the Command class to handle Query and Command requests so that Read and Write operations using the ProductInfoDataAccessService injected using the IDataAccessService interface.  The IRequestHanler interface contains the Handle() method. Each handler class must implement the Handle() method to perform Read and Write Operations. In the Handler folder, add a new class file named GetProductInfoListHandler.cs. In this class file, we will add code for the GetProductInfoListHandler class. This class implements the Handle() method of the IRequestHandler interface to read all products. The code for the GetProductInfoListHandler class is shown in Listing 16

public class GetProductInfoListHandler : IRequestHandler<GetProductInfoQuery, ResponseRecords<ProductInfo>>
{
    IDataAccessService<ProductInfo,string> ProductInfoService;

    public GetProductInfoListHandler(IDataAccessService<ProductInfo, string> productInfoService)
    {
        ProductInfoService = productInfoService;
    }

    public async  Task<ResponseRecords<ProductInfo>> Handle(GetProductInfoQuery request, CancellationToken cancellationToken)
    {
        return await ProductInfoService.GetAsync();
    }
}

Listing 16: The GetProductInfoListHandler class
 
Similarly, we will add the class files for Reading and Write records as shown in the following listings

The GetProductInfoByIdHandler.cs file contains GetProductInfoByIdHandler class


public class GetProductInfoByIdHandler : IRequestHandler<GetProductInfoByIdQuery, ResponseRecord<ProductInfo>>
{

    IDataAccessService<ProductInfo, string> ProductInfoService;

    public GetProductInfoByIdHandler(IDataAccessService<ProductInfo, string> productInfoService)
    {
        ProductInfoService = productInfoService;
    }

    public async Task<ResponseRecord<ProductInfo>> Handle(GetProductInfoByIdQuery request, CancellationToken cancellationToken)
    {
        return await ProductInfoService.GetByIdAsync(request.Id);
    }
}
Listing 17: The GetProductInfoByIdHandler class

The CreateProductInfoHandler.cs file contains code for the CreateProductInfoHandler class.

public class CreateProductInfoHandler : IRequestHandler<RegisterProductInfoCommand, ResponseRecord<ProductInfo>>
{
    IDataAccessService<ProductInfo,string> ProductInfoService;

    public CreateProductInfoHandler(IDataAccessService<ProductInfo, string> productInfoService)
    {
        this.ProductInfoService = productInfoService;  
    }


    public async Task<ResponseRecord<ProductInfo>> Handle(RegisterProductInfoCommand request, CancellationToken cancellationToken)
    {
        return await ProductInfoService.CreateAsync(request.ProductInfo);
    }
}

Listing 18: The CreateProductInfoHandler class

The UpdateProductInfoHandler.cs file contains code for the UpdateProductInfoHandler class
public class UpdateProductInfoHandler : IRequestHandler<UpdateProductInfoCommand, ResponseRecord<ProductInfo>>
{
    IDataAccessService<ProductInfo,string> ProductInfoService;

    public UpdateProductInfoHandler(IDataAccessService<ProductInfo, string> productInfoService)
    {
        ProductInfoService = productInfoService;
    }

    public async Task<ResponseRecord<ProductInfo>> Handle(UpdateProductInfoCommand request, CancellationToken cancellationToken)
    {
        return await ProductInfoService.UpdateAsync(request.ProductInfo.ProductId, request.ProductInfo);
    }
}

Listing 19: The UpdateProductInfoHandler class

The DeleteProductInfoHandler.cs file contains code for the DeleteProductInfoHandler class


public class DeleteProductInfoHandler : IRequestHandler<DeleteProductInfoCommand, ResponseRecord<ProductInfo>>
{

    IDataAccessService<ProductInfo, string> ProductInfoService;

    public DeleteProductInfoHandler(IDataAccessService<ProductInfo, string> productInfoService)
    {
        ProductInfoService = productInfoService;
    }

    public async Task<ResponseRecord<ProductInfo>> Handle(DeleteProductInfoCommand request, CancellationToken cancellationToken)
    {
        return await ProductInfoService.DeleteAsync(request.Id);
    }
}
 Listing 20: The DeleteProductInfoHandler class
   
Code from Listing 16 to 20 shows Handler code for classes to perform Read /  Write operations. Once these steps are completed, it is time to complete the application by adding the controllers.   

Step 11: Modify the appsettings.json file to define the connection string as shown in Listing 21

"ConnectionStrings": {
  "AppConnStr": "Data Source=.;Initial Catalog=Company;Integrated Security=SSPI;TrustServerCertificate=True"
}

Listing 21: The connection string in appsettings.json

Step 12: Let's modify the Program.cs to register the AutoMapper, MediatR, DbContext, and DataAccessService in the Dependency Container as shown in Listing 22

// Add services to the container.
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddMediatR(Assembly.GetExecutingAssembly());
builder.Services.AddDbContext<CompanyContext>(
      options=> options.UseSqlServer(builder.Configuration.GetConnectionString("AppConnStr")));

builder.Services.AddScoped<IDataAccessService<ProductInfo,string>, ProductInfoDataAccessService>();


Listing 22: The Dependencies Registration
Register the Custom Exception Middleware in Program.cs in Listing 23

.....
app.UseErrorExtender();
.....
Listing 23: Registration of Custom Exception Handler

Step 13: In the Controllers folder, let's add a new empty API controller named ReadProductInfoController.cs. In this file, we will add code for the ReadProductInfoController class. This class is a constructor injected with the IMediator interface. This interface has the Send() method to send the query request to read data. The code for the ReadProductInfoController class is shown in Liting 24


using Core_CQRS_Mediatr.Commands;
using Core_CQRS_Mediatr.Models;
using Core_CQRS_Mediatr.Queries;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Core_CQRS_Mediatr.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ReadProductInfoController : ControllerBase
    {
        IMediator mediator;

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

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var response = await mediator.Send(new GetProductInfoQuery());
            if (!response.IsSuccess)
                throw new Exception($"Error occurred while reading data");
            response.StatusCode = 200;
            return Ok(response);
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> Get(string id)
        {
            var response = await mediator.Send(new GetProductInfoByIdQuery() { Id = id});
            if (!response.IsSuccess)
                throw new Exception($"Error occurred while reading data base don Id= {id}");
            response.StatusCode = 200;
            return Ok(response);
        }

        
    }
}

Listing 24: The ReadProductInfoController API class
 
As shown in Listing 24, the API class contains Http Get methods. These methods contain code for making the query request to read the data. Carefully see the code, that the API controller uses the MediatR to make calls without directly accessing the Data Access Services. The MediatR abstracts the call to the Data Access Services using the Handlers. This is an advantage of using the Mediator pattern.  In the same Controllers folder, add a new empty API named WriteProductInfoController.cs. In this file, we will add code for the WriteProductInfoController API. This controller injects IMapper and IMediator interfaces. We need the IMapper to map the ProductInfoViewModel the data received from the HTTP POST, and PUT requests with the ProductInfo object so that it can be further used for Creare and Update operations. The WriteProductInfoController class contains HTTP Post, Put, and Delete methods. All these methods use the command objects to perform write operations. The code for WriteProductInfoController class is shown in Listing 25


using AutoMapper;
using Core_CQRS_Mediatr.Commands;
using Core_CQRS_Mediatr.Models;
using Core_CQRS_Mediatr.ViewModels;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Core_CQRS_Mediatr.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class WriteProductInfoController : ControllerBase
    {
        IMediator Mediator;
        IMapper Mapper;

        public WriteProductInfoController(IMapper mapper, IMediator mediator)
        {
            Mediator = mediator;
            Mapper = mapper;
        }
        [HttpPost]
        public async Task<IActionResult> Post(ProductInfoViewModel product)
        {
            if (!ModelState.IsValid)
                throw new Exception(GetModelErrorMessagesHelper(ModelState));
            ProductInfo prd = Mapper.Map<ProductInfo>(product);
            var response = await Mediator.Send(new RegisterProductInfoCommand(prd));
            if(!response.IsSuccess)
                throw new Exception($"The create request faild");
            response.StatusCode = 200;
            return Ok(response);
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> Put(string id, ProductInfoViewModel product)
        {
         
            if (String.IsNullOrEmpty(id))
                throw new Exception($"The Id={id} value is invalid");
            if (!ModelState.IsValid)
                throw new Exception(GetModelErrorMessagesHelper(ModelState));
            if (!id.Equals(product.ProductId))
                throw new Exception($"The update request cannot be processed because the provided data is mismatched");
            ProductInfo prd = Mapper.Map<ProductInfo>(product);
            var response = await Mediator.Send(new UpdateProductInfoCommand(prd));
            if (!response.IsSuccess)
                throw new Exception($"The update request faild");
            response.StatusCode = 200;
            return Ok(response);
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(string id)
        {
            if(String.IsNullOrEmpty(id))
                throw new Exception($"The Id={id} value is invalid");
            var response = await Mediator.Send(new DeleteProductInfoCommand(id));
            if (!response.IsSuccess)
                throw new Exception($"The delete request faild for Id= {id}");
            response.StatusCode = 200;
            return Ok(response);
        }


        private string GetModelErrorMessagesHelper(ModelStateDictionary errors)
        {
            string messages = "";
            foreach (var item in errors)
            {
                for (int j = 0; j < item.Value.Errors.Count; j++)
                {
                    messages += $"{item.Key} \t {item.Value.Errors[j].ErrorMessage} \n";
                }
            }
            return messages;
        }
    }
}

Listing 25: The WriteProductInfoController code
 
In the WriteProductInfoController class, the Mediator objects send requests for Command to perform write operations.  These commands are further handled by the Handlers to access the ProductIndoDataService to perform database operations.

Run the Application, and the Swagger UI will be loaded in the browser where HTTP Requests can be tested. Figure 3, shows the HTTP POST Request.



Figure 3: The HTTP POST Request
  

The code for this article can be downloaded from this link.
Conclusion: The Mediator Pattern is useful when the AS.NET Core application is big and complex enough that it can be accessed by various client applications and these clients expect separate Read and Write operations with separate APIs.  The biggest advantage of the Mediator pattern is that if we want to add a new data access service in the applications, it can be added to the application without updating the API Object.   

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach