ASP.Net Core C#

ASP.NET Core使用Mediator & CQRS

Jack Hung 2020/12/23 00:33:38
6971

 

CQRS

CQRS(Command Query Responsibility Segregation )又稱命令查詢職責分離模式,該設計模式將增、刪、修(Command)與讀取(Query)的行為拆分成讀寫分離,明確區分我們的每個請求,並且降低系統的複雜性。

 

      Mediator 

 

              根據Wiki的解釋,在中介者模式中,對象間的通信過程被封裝在一個中介者(調解人)對象之中。 對象之間不再直接交互,

     而是通過調解人進行交互。 這麼做可以減少可交互對象間的依賴,從而降低耦合

 

 

 

    實做說明

        實做範例時將以 Asp.Net Core Web API 為例,建立的Class都會以QueriesCommand做區隔,並以Handle來處理邏輯事務

        再進行handle的中介調用,實做範例時會以MediatR這個套件來實現CQRS及Mediator模式

    套件

 

 

Nuget下載安裝以下三個套件

 

  1.MediatR

  2.MediatR.Extensions.Microsoft.DependencyInjection

  3.Microsoft.Extensions.DependencyInjection

註冊MediatR

         在StartupConfigureServices底下註冊MediatR

services.AddMediatR(Assembly.GetExecutingAssembly());

    實做方法

       剛開始先建立所需的 Request 及Response Model 

       而RequestModel 必須得繼承 IRequest,這個 IRequest 是引用了MediatR套件,該泛型介面指定了要回傳的型別,

       若有不需要回傳的情境,則指定為Unit或不填入參數。

  public class GetProductByIdRequest : IRequest<GetProductByIdResponse>
    {
        public int Id { get; set; }
    }
   public class GetProductByIdResponse
    {
        public int ProductID { get; set; }
        public string ProductName { get; set; }
        public string QuantityPerUnit { get; set; }
        public decimal? UnitPrice { get; set; }
        public short? UnitsInStock { get; set; }
        public short? UnitsOnOrder { get; set; }
        public short? ReorderLevel { get; set; }
        public bool Discontinued { get; set; }
    }
    public class AddProductRequest :  IRequest<int>
    {
        public string ProductName { get; set; }
        public int? SupplierID { get; set; }
        public int? CategoryID { get; set; }

        public string QuantityPerUnit { get; set; }

        public decimal? UnitPrice { get; set; }
        public short? UnitsInStock { get; set; }
        public short? UnitsOnOrder { get; set; }
        public short? ReorderLevel { get; set; }
        public bool Discontinued { get; set; }

    }

   Handler

       現在Request已經建立,就要有程式來處理這些請求

       分別建立專責的Handle,且繼承 IRequestHandler

       該泛型介面可以接受兩個參數,分別是TRequest跟TResponse,這裡就放上剛建立的Request及Response

       這邊要注意的是,一個request只能有一個專責的handle

       PS.實務上沒有一定要將DBContext使用在Handle裡,依專案設計需求而定

public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdRequest, GetProductByIdResponse>
    {
        private readonly NorthwindContext _db;

        public GetProductByIdQueryHandler(NorthwindContext db)
        {
            _db = db;
        }

        public async Task<GetProductByIdResponse> Handle(GetProductByIdRequest request, CancellationToken cancellationToken)
        {
            var data = await _db.Products.SingleOrDefaultAsync(x => x.ProductID == request.Id, cancellationToken);

            if (data is null)
            {
                return null;
            }

            return new GetProductByIdResponse
            {
                ProductID = data.ProductID,
                ProductName = data.ProductName,
                QuantityPerUnit = data.QuantityPerUnit,
                UnitPrice = data.UnitPrice,
                UnitsInStock = data.UnitsInStock,
                UnitsOnOrder = data.UnitsOnOrder,
                ReorderLevel = data.ReorderLevel,
                Discontinued = data.Discontinued
            };

        }
    }

       

 public class AddProductCommandHandler : IRequestHandler<AddProductRequest, int>
    {
        private readonly NorthwindContext _dbContext;

        public AddProductCommandHandler(NorthwindContext dbContext)
        {
            _dbContext = dbContext;
        }


        public async Task<int> Handle(AddProductRequest request, CancellationToken cancellationToken)
        {
            var model = new Products
            {
                ProductName = request.ProductName,
                SupplierID = request.SupplierID,
                CategoryID = request.CategoryID,
                QuantityPerUnit = request.QuantityPerUnit,
                UnitPrice = request.UnitPrice,
                UnitsInStock = request.UnitsInStock,
                UnitsOnOrder = request.UnitsOnOrder,
                ReorderLevel = request.ReorderLevel,
                Discontinued = request.Discontinued
            };

            await _dbContext.AddAsync(model, cancellationToken);
            return await _dbContext.SaveChangesAsync(cancellationToken);
        }
    }

 Controller

      在Controller調用寫好的Handle前,要注入IMediator

  [Route("api/[controller]")]
    [ApiController]
    public class ProductController : ControllerBase
    {
        private readonly IMediator _mediator;

        public ProductController(IMediator mediator)
        {
            _mediator = mediator;
        }

          在Action中使用Send進行handle的調用

 [HttpGet]
        public async Task<IActionResult> GetById([FromQuery] GetProductByIdRequest query)
        {
            var result = await _mediator.Send(query);

            if (result is null) return NotFound();
            
            return Ok(result);
        }

 [HttpPost]

        public async Task<IActionResult> Post([FromBody] AddProductRequest command)
        {
            var result = await _mediator.Send(command);

            return Ok(result);
        }

       測試

             為了確保API是否在輸入參數時可以正常調用並回傳正確的Status Code,以下使用Nunit進行單元測試

public class ProductControllerTest
    {
        private readonly Mock<IMediator> _mediator;

        public ProductControllerTest()
        {
            _mediator = new Mock<IMediator>();

        }

        [Test]
        public async Task AddProduct_Success_Result()
        {
            //Arrange
            var request = new AddProductRequest();
    
            _mediator.Setup(x => x.Send(It.IsAny<AddProductRequest>(), new CancellationToken()))
                .ReturnsAsync(1);

            var productController = new ProductController(_mediator.Object);

            //Action

            var result = await productController.Post(request);
            var okResult = result as OkObjectResult;

            //Assert

            Assert.IsNotNull(okResult);
            Assert.AreEqual(200, okResult.StatusCode);

        }
        [Test]
        public async Task GetProductById_Success_Result()
        {
            //Arrange
            var request = new GetProductByIdRequest();
            _mediator.Setup(x => x.Send(It.IsAny<GetProductByIdRequest>(), new CancellationToken()))
                .ReturnsAsync(new GetProductByIdResponse());

            var productController = new ProductController(_mediator.Object);

            //Action

            var result = await productController.GetById(request);
            var okResult = result as OkObjectResult;

            //Assert

            Assert.IsNotNull(okResult);
            Assert.AreEqual(200, okResult.StatusCode);

        }

    }

        驗證結果為綠燈,單元測試通過

       

             使用PostMan 進行測試也能拿到正確回傳

            
            

       結語

           每個設計模式都有其優缺點,雖說這樣透過mediatr去調用handle確實可以精簡controller,讀寫工作的分層也很明確

           但每寫一個query或command,都得要寫一個handle去對應,對一些人來說可能還是會略嫌麻煩,

           當然實務上也不是全部的Controller都得要透過mediator調用,這都要視專案需求而設計。

           

 

           

Jack Hung