ASP.NET Core使用Mediator & CQRS
CQRS
CQRS(Command Query Responsibility Segregation )又稱命令查詢職責分離模式,該設計模式將增、刪、修(Command)與讀取(Query)的行為拆分成讀寫分離,明確區分我們的每個請求,並且降低系統的複雜性。
Mediator
根據Wiki的解釋,在中介者模式中,對象間的通信過程被封裝在一個中介者(調解人)對象之中。 對象之間不再直接交互,
而是通過調解人進行交互。 這麼做可以減少可交互對象間的依賴,從而降低耦合。
實做說明
實做範例時將以 Asp.Net Core Web API 為例,建立的Class都會以Queries及Command做區隔,並以Handle來處理邏輯事務
再進行handle的中介調用,實做範例時會以MediatR這個套件來實現CQRS及Mediator模式
套件
在 Nuget下載安裝以下三個套件
1.MediatR
2.MediatR.Extensions.Microsoft.DependencyInjection
3.Microsoft.Extensions.DependencyInjection
註冊MediatR
在Startup的ConfigureServices底下註冊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調用,這都要視專案需求而設計。