筆者在研讀 GitHub 上的各大 ASP.NET Core 開源示範專案,包含:
jasontaylordev/CleanArchitecture,
dotnet-architecture/eShopOnWeb,
fullstackhero/dotnet-webapi-boilerplate
等,發現都有使用到 MediatR 套件,MediatR 在 NuGet 上總下載量為 154.1M ,平均每天有 43.7 下載,在 GitHub 上有 10k 的 Star,看來應該要來了解一下這個套件。
在一般的開發過程中可能會依照三層架構(three-tier architecture)將程式碼分層,區分 Controller, Service, Repository 等,Controller 依賴於 Service ,Service 又依賴於 Repository ,各層緊密相連:
Controller → Service → Repository
使用 MediatR 後會拆分為:
Controller → Mediator
Mediator → Request Handlers
Request Handlers → Repository (需要存取資料庫時才要)
讓 Controller 不會再直接依賴於處理邏輯,達成解耦合(Decoupling)
註: 從 MediatR 12 以後設定方式有所不同,本次示範的版本為 12.1.1
建立一個自訂類別,用來儲存請求回應的內容,這裡就是簡單的 UserDto:
建立一個用於傳遞請求參數的自訂類別,若沒有需要請求參數可以是空的類別,需要繼承 IRequest,IRequest 的泛型放入回傳的資料型態,這裡假設是上面建立的 UserDto 集合:
建立處理程式 Handler 類別,繼承 IRequestHandler ,需要實作 Handle 方法,用來處理請求並回傳結果,這裡直接回傳一個寫死的清單做示範:
最後建立一個 Controller ,用來接收請求,並且注入 IMediator ,將請求發送到 IMediator 中,讓他自動去找該由哪個處理程序處理:
執行程式,測試 API ,就會發現確實能夠取得 Handle 的內容,並不用和以前一樣需要註冊 Service 服務,只要繼承 IRequestHandler 就會自動被找到並分配任務了!
通常使用 Command 來表示執行會變更狀態的動作,例如新增、更新、刪除等,這裡建立一個 AddUserCommand 類別,繼承 IRequest ,因為沒有回傳值所以只要 IRequest 就好,不用填入泛型類別:
建立新增使用者的處理程序 AddUserHandler
回到 UserController ,加上新增的 API 入口:
可以,只要使用 IPipelineBehavior 即可!下面就示範使用 LoggingBehavior 攔截紀錄
然後在 Program.cs 中註冊即可:
註: LoggingBehavior 的泛型無法限制繼承 IRequest 或是 IRequest<TResponse>,假設加上以下限制的話沒有回傳值的 IRequest 就無法被攔截
解決方式是把所有沒有回傳值的 IRequest 都指定為 Unit,參見下方示範:
修改 AddUserCommand.cs:
修改 AddUserHandler.cs:
UserController 就不需要修改了
使用 MediatR 的一個缺點就是在 Controller 和 Handler 中會更難跳轉,會再比抽出介面的更難跳轉一點,或許這就是解耦合的代價吧。
參考資料:
GitHub - jbogard/MediatR
Microsoft.Learn - Implement the microservice application layer using the Web API
IBM - What is three-tier architecture?
Why use MediatR? 3 reasons why and 1 reason not
Microsoft.Learn - CQRS pattern
CodeMaze - CQRS and MediatR in ASP.NET Core
MediatR 是什麼?
MediatR 是一個簡單的中介軟體(Middleware)實作,主要用來簡化應用程式中的請求接收和處理邏輯,讓程式碼更為整齊,將業務流程抽離,降低耦合。在一般的開發過程中可能會依照三層架構(three-tier architecture)將程式碼分層,區分 Controller, Service, Repository 等,Controller 依賴於 Service ,Service 又依賴於 Repository ,各層緊密相連:
Controller → Service → Repository
使用 MediatR 後會拆分為:
- Mediator: MediatR 套件中的中介層,負責將 Controller 與 Request Handlers 間的溝通
- Controller: 負責接收請求,但依賴於 MediatR 中的 Mediator
- Request Handlers: 取代上面提到的 Service ,每個請求都會有一個 Request Handlers ,負責處理業務邏輯,依賴於 Repository
- Repository: 資料存取
Controller → Mediator
Mediator → Request Handlers
Request Handlers → Repository (需要存取資料庫時才要)
讓 Controller 不會再直接依賴於處理邏輯,達成解耦合(Decoupling)
安裝
先使用 NuGet 安裝 MediatR 套件,或是使用 .NET CLI 執行以下指令安裝
dotnet add package MediatR
註: 從 MediatR 12 以後設定方式有所不同,本次示範的版本為 12.1.1
基礎示範
先在 Program.cs 中加入以下程式碼,註冊服務:
builder.Services.AddMediatR(configuration => configuration.RegisterServicesFromAssembly(typeof(Program).Assembly));
var app = builder.Build();
建立一個自訂類別,用來儲存請求回應的內容,這裡就是簡單的 UserDto:
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
建立一個用於傳遞請求參數的自訂類別,若沒有需要請求參數可以是空的類別,需要繼承 IRequest,IRequest 的泛型放入回傳的資料型態,這裡假設是上面建立的 UserDto 集合:
public class UserQueryDto : IRequest<List<UserDto>>
{
}
建立處理程式 Handler 類別,繼承 IRequestHandler ,需要實作 Handle 方法,用來處理請求並回傳結果,這裡直接回傳一個寫死的清單做示範:
public class GetUsersHandler : IRequestHandler<UserQueryDto, List<UserDto>>
{
public Task<List<UserDto>> Handle(UserQueryDto request, CancellationToken cancellationToken)
{
var result = new List<UserDto>
{
new()
{
Id = 1,
Name = "User 1"
},
new()
{
Id = 2,
Name = "User 2"
}
};
return Task.FromResult(result);
}
}
最後建立一個 Controller ,用來接收請求,並且注入 IMediator ,將請求發送到 IMediator 中,讓他自動去找該由哪個處理程序處理:
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
private readonly IMediator _mediator;
public UserController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<ActionResult> Get()
{
var users = await _mediator.Send(new UserQueryDto());
return Ok(users);
}
}
執行程式,測試 API ,就會發現確實能夠取得 Handle 的內容,並不用和以前一樣需要註冊 Service 服務,只要繼承 IRequestHandler 就會自動被找到並分配任務了!
沒有回傳值的示範
上面示範的是查詢,會回傳一個集合,這裡使用新增做示範,不回傳內容。通常使用 Command 來表示執行會變更狀態的動作,例如新增、更新、刪除等,這裡建立一個 AddUserCommand 類別,繼承 IRequest ,因為沒有回傳值所以只要 IRequest 就好,不用填入泛型類別:
public class AddUserCommand : IRequest
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
建立新增使用者的處理程序 AddUserHandler
public class AddUserHandler : IRequestHandler<AddUserCommand>
{
public async Task Handle(AddUserCommand request, CancellationToken cancellationToken)
{
// todo: 新增的邏輯
return;
}
}
回到 UserController ,加上新增的 API 入口:
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
private readonly IMediator _mediator;
public UserController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<ActionResult> Get()
{
var users = await _mediator.Send(new UserQueryDto());
return Ok(users);
}
[HttpPost]
public async Task<ActionResult> Post()
{
await _mediator.Send(new AddUserCommand());
return StatusCode(201);
}
}
攔截請求
在 ASP.NET Core 中我們可以使用 Middleware 攔截 Request 和 Response ,不過既然現在所有 API 請求都進入到 MediatR 中管理,那 MediatR 能不能做到同樣的事情呢?可以,只要使用 IPipelineBehavior 即可!下面就示範使用 LoggingBehavior 攔截紀錄
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
_logger.LogInformation($"Handling {typeof(TRequest).Name}");
var response = await next();
_logger.LogInformation($"Handled {typeof(TResponse).Name}");
return response;
}
}
然後在 Program.cs 中註冊即可:
builder.Services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
註: LoggingBehavior 的泛型無法限制繼承 IRequest 或是 IRequest<TResponse>,假設加上以下限制的話沒有回傳值的 IRequest 就無法被攔截
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
解決方式是把所有沒有回傳值的 IRequest 都指定為 Unit,參見下方示範:
回傳「空」的示範
上面提到沒有回傳值時可以繼承 IRequest 而不是 IRequest<>,不過其實在沒有回傳值時可以使用 Unit :修改 AddUserCommand.cs:
public class AddUserCommand : IRequest<Unit>
{
public int Id { get; set; }
public string Name { get; set; } = null!;
}
修改 AddUserHandler.cs:
public class AddUserHandler : IRequestHandler<AddUserCommand, Unit>
{
public Task<Unit> Handle(AddUserCommand request, CancellationToken cancellationToken)
{
// todo: 新增的邏輯
return;
}
}
UserController 就不需要修改了
使用 MediatR 的一個缺點就是在 Controller 和 Handler 中會更難跳轉,會再比抽出介面的更難跳轉一點,或許這就是解耦合的代價吧。
參考資料:
GitHub - jbogard/MediatR
Microsoft.Learn - Implement the microservice application layer using the Web API
IBM - What is three-tier architecture?
Why use MediatR? 3 reasons why and 1 reason not
Microsoft.Learn - CQRS pattern
CodeMaze - CQRS and MediatR in ASP.NET Core
留言
張貼留言
如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com