C# MediatR 介紹

筆者在研讀 GitHub 上的各大 ASP.NET Core 開源示範專案,包含: jasontaylordev/CleanArchitecture, dotnet-architecture/eShopOnWeb, fullstackhero/dotnet-webapi-boilerplate 等,發現都有使用到 MediatR 套件,MediatR 在 NuGet 上總下載量為 154.1M ,平均每天有 43.7 下載,在 GitHub 上有 10k 的 Star,看來應該要來了解一下這個套件。

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

留言