有些時候,我們想在命令執行前後統一做額外的事情,例如每個命令執行前驗證資料欄位、紀錄請求者,或是命令執行後,統一執行資料庫交易等。若是使用自訂的Command,通常會使用裝飾者模式 (Decorator Pattern) 做處理,而 MediatR 則使用管線的概念協助。
MediatR管線:
IRequestPreProcessor<>
請求執行前的預處理IRequestPostProcessor<,>
請求執行後的再處理IPipelineBehavior<,>
自訂管線行為以下是我們目前的程式碼,每次執行命令前,我們都會使用 FluentValidation,協助我們驗證資料欄位是否正確,並使用方法注入,更好的顯示程式碼意圖。
然而隨著時間流逝,相似的程式碼,會逐步在每個請求中重複出現,所以我們希望能透過 MediatR 管線,來協助我們自動執行驗證。
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IMediator _mediator;
public WeatherForecastController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateWeatherForecast(
[FromBody] CreateWeatherForecastRequest request,
[FromServices] IValidator<CreateWeatherForecastCommand> validator)
{
var command = new CreateWeatherForecastCommand
{
Nation = request.Nation,
City = request.City,
Date = request.Date,
TemperatureC = request.TemperatureC,
Summary = request.Summary,
};
var validate = await validator.ValidateAsync(command);
if (validate.IsValid is false) return BadRequest(validate.ToDictionary());
var result = await _mediator.Send(command);
return result ? Ok() : BadRequest();
}
[HttpPut("{id:int}")]
public async Task<IActionResult> UpdateWeatherForecast(int id,
[FromBody] UpdateWeatherForecastRequest request,
[FromServices] IValidator<UpdateWeatherForecastCommand> validator)
{
if (id != request.WeatherForecastId) return BadRequest();
var command = new UpdateWeatherForecastCommand
{
WeatherForecastId = request.WeatherForecastId,
Nation = request.Nation,
City = request.City,
Date = request.Date,
TemperatureC = request.TemperatureC,
Summary = request.Summary,
};
var validate = await validator.ValidateAsync(command);
if (validate.IsValid is false) return BadRequest(validate.ToDictionary());
var result = await _mediator.Send(command);
return result ? Ok() : BadRequest();
}
}
ValidationBehavior
並實作IPipelineBehavior
// 1.只有當透過管道的請求是 Command 命令時,我們才允許執行此 IPipelineBehavior,所以我們使用where進行條件拘束。
public class ValidationBehavior<TRequest, TReponse> : IPipelineBehavior<TRequest, TReponse> where TRequest : ICommand<TReponse>
{
// 2.查詢所有實作AbstractValidator類別
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TReponse> Handle(
TRequest request,
RequestHandlerDelegate<TReponse> next,
CancellationToken cancellationToken)
{
if (!_validators.Any()) return await next();
var context = new ValidationContext<TRequest>(request);
// 3.執行驗證,並只取出驗證錯誤的部分
var errorsDictionary = _validators
.Select(validator => validator.Validate(context))
.Where(validationResult => !validationResult.IsValid)
.SelectMany(validationResult => validationResult.Errors)
.GroupBy(
x => x.PropertyName,
x => x.ErrorMessage,
(propertyName, errorMessages) => new
{
Key = propertyName,
Values = errorMessages.Distinct().ToArray()
})
.ToDictionary(x => x.Key, x => x.Values);
// 4.如果有任何驗證錯誤,拋出ValidationException,阻止進一步執行
if (errorsDictionary.Any())
{
throw new Exceptions.ValidationException(errorsDictionary);
}
return await next();
}
}
ValidationException
記錄錯誤public class ValidationException : Exception
{
public IDictionary<string, string[]> Errros { get; }
public ValidationException(IDictionary<string, string[]> errors)
{
Errros = errors;
}
}
public record ValidationError(string PropertyName, string ErrorMessage);
ExceptionHandlingMiddleware
處理驗證異常public class ExceptionHandlingMiddleware : IMiddleware
{
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
await HandleExceptionAsync(context, e);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var exceptionDetails = GetExceptionDetails(exception);
var problemDetails = new ProblemDetails
{
Status = exceptionDetails.StatusCode,
Type = exceptionDetails.Type,
Title = exceptionDetails.Title,
Detail = exceptionDetails.Details,
};
if (exceptionDetails.Errors != null)
{
problemDetails.Extensions["errors"] = exceptionDetails.Errors;
}
context.Response.ContentType = "application/json";
context.Response.StatusCode = exceptionDetails.StatusCode;
await context.Response.WriteAsJsonAsync(problemDetails);
}
private static ExceptionDetails GetExceptionDetails(Exception exception)
{
// 驗證錯誤統一使用400錯誤,其他則使用500,顯示伺服器錯誤
return exception switch
{
ValidationException validationException => new ExceptionDetails(
StatusCodes.Status400BadRequest,
"ValidationFailure",
"Validation error",
"One or more validation errors has occurred",
validationException.Errros),
_ => new ExceptionDetails(
StatusCodes.Status500InternalServerError,
"ServerError",
"Server error",
"An unexpected error has occured",
null)
};
}
}
internal record ExceptionDetails(int StatusCode, string Type, string Title, string Details, IDictionary<string, string[]> Errors);
IServiceCollection
註冊builder.Services.AddMediatR(cf =>
{
cf.RegisterServicesFromAssembly(applicationAssemblies);
cf.AddOpenBehavior(typeof(ValidationBehavior<,>));
});
builder.Services.AddTransient<ExceptionHandlingMiddleware>();
// ...
app.UseMiddleware<ExceptionHandlingMiddleware>();