使用 MediatR 和 FluentValidator
1. 创建示例文件夹 Sample
首先,创建示例文件夹 Sample。
2. 创建表示层项目 Web
在示例文件夹 Sample 中,使用标准的 dotnet 命令,基于 .NET 6 下 minimal WebAPI 项目创建示例项目。
dotnet new webapi -n Web
新项目将默认包含一个 WeatherForecastController 控制器。
3. 为 Web 项目增加 Autofac 支持
为 Web 项目增加 Autofac 依赖注入容器的支持。
我们将不使用微软默认的依赖注入容器,而使用 Autofac 依赖注入容器,它可以支持更多的特性,例如,可以以程序集为单位直接注入其中定义的服务,极大简化服务的注册工作。
Autofac
是 Autofac 的核心库,而 Autofac.Extensions.DependencyInjection
则提供了各种便于使用的扩展方法。
添加 NuGet 包
进入 Web 项目的目录,使用下面的命令添加 Autofac 库。
dotnet add package Autofac
dotnet add package Autofac.Extensions.DependencyInjection
添加针对 Autofac 依赖注入容器的支持。
以后,既可以继续使用基于微软的服务注册形式来注册服务,这样,原有的服务注册还可以继续使用。又可以使用 Autofac 提供的方式注册服务。修改 Program.cs 中的代码,下面代码中的 builder 即来自 Autofac 的类型为 Autofac.ContainerBuilder。
using Autofac;
using Autofac.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// 配置使用 Autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// 配置 Autofac 容器
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
{
});
以后,我们可以在这个 ConfigureContainer() 方法中,使用 Autofac 的方式来注册服务了。
4. 创建业务表示层项目 Application
回到 Sample 文件夹,在其中创建类库项目 Application,我们用来表示业务处理领域逻辑。
dotnet new classlib -n Application
这将会在 Sample 目录下创建名为 Application 的第二个文件夹,其中包含新创建的项目。
4. Add MediatR
在本例中,我们将使用基于 MediatR 的中介者模式来实现业务逻辑与表示层的解耦。
进入新创建的 Application 文件夹,为项目增加 MediatR 的支持。
dotnet add package MediatR
而 MediatR.Contracts 包中仅仅包含如下类型:
IRequest
(including generic variants andUnit
)INotification
IStreamRequest
5. 在 Application 中定义业务处理
首先,删除默认生成的 Class1.cs
文件。
然后,创建 Models 文件夹,保存我们使用的数据模型。在我们的示例中,这个模型就是 Web 项目中的 WeatherForecast ,我们将它从 Web 项目的根目录,移动到 Models 文件夹中,另外,将它的命名空间也修改为 Application。修改之后如下所示:
namespace Application;
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
然后,在 Application 项目中创建名为 Commands 的文件夹来保存我们定义的业务处理。我们定义的所有命令和对应的处理器都将保存在这个文件夹中。
我们定义查询天气的命令对象 QueryWeatherForecastCommand,它需要实现 MediatR 的接口 IRequest<T>,所以,记得使用它的命名空间 MediatR。这个命令处理之后的返回结果的类型是 WeatherForecast[] 数组类型,我们再增加一个 Name 属性,来表示天气预报所对应的地区名称,全部代码如下所示:
namespace Application;
using MediatR;
public class QueryWeatherForecastCommand: IRequest<WeatherForecast[]>
{
public string Name { get; set; }
}
在这个文件夹中,继续定义这个操作所对应的处理器。
处理器需要实现的接口是 IRequestHandler<in TRequest, TResponse>
,
namespace Application;
using MediatR;
public class QueryWeatherForecastCommandHandler
: IRequestHandler<QueryWeatherForecastCommand, WeatherForecast[]>
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public Task<WeatherForecast[]> Handle(QueryWeatherForecastCommand command, CancellationToken cancellationToken)
{
var result = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
return Task.FromResult(result);
}
}
6. 在 Web 项目中使用 Application 定义的处理
回到 Web 文件夹中,为 Web 项目添加 MediatR 的支持,同时还需要添加 MediatR 对 Autofac 的支持库。
dotnet add package MediatR
dotnet add package MediatR.Extensions.Autofac.DependencyInjection;
为 Web 项目添加对 Application 项目的引用。
dotnet add reference ../Application/Application.csproj
在 WeatherForecastController.cs 中间顶部,添加对 Application 项目的引用,以支持 WeatherForecast 类型,它现在已经被转移到了 Application 项目中。
为了便于使用 Autofac 的模块注册功能,在 Web 项目中创建文件夹 AutofacModules,在其中创建 MediatorModule.cs 代码文件。文件内容可以先为空。
namespace Web;
using Autofac;
using Autofac.Extensions.DependencyInjection;
public class MediatorModule : Autofac.Module {
protected override void Load (ContainerBuilder builder) {
}
}
而在控制器 WeatherForecastController 中,由于原来的处理逻辑已经被转移到命令处理器中,所以,可以删除这里的处理逻辑代码,现在它应该如下所示:
using Application;
using Microsoft.AspNetCore.Mvc;
namespace Web.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return null;
}
}
然后,修改 Program.cs 中的代码,使用 Autofac 注册 MediatR。
需要注意的是,我们的命令和处理器定义在 Application 程序集中。
// 配置使用 Autofac
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// 配置 Autofac 容器
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
{
// 注册 MediatR
// 来自 NuGet package: MediatR.Extensions.Autofac.DependencyInjection
builder.RegisterMediatR(typeof(Application.QueryWeatherForecastCommand).Assembly);
// 注册模块
builder.RegisterModule<Web.MediatorModule>();
});
重新回到 WeatherForecastController 文件,使用新定义的 MediatR 方式。
using Application;
using Microsoft.AspNetCore.Mvc;
using MediatR;
namespace Web.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IMediator _mediator;
public WeatherForecastController(
ILogger<WeatherForecastController> logger,
IMediator mediator)
{
_logger = logger;
_mediator = mediator;
}
[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> Get()
{
var result = await _mediator.Send(
new QueryWeatherForecastCommand { Name="Hello" }
);
return result;
}
}
重新编译并运行程序,现在它应该和以前一样可以访问,并获得天气预报数据。
7 . 为 QueryWeatherForecastCommand 增加验证支持
ASP.NET Core 是原生支持模型验证的,现在我们使用 FluentValidation 来重新实现。
在 Application 项目中,添加 FluentValidation 包。同时,为了能够使用日志,我们还需要添加 Microsoft.Extensions.Logging.Abstractions 包。
dotnet add package FluentValidation
dotnet add package Microsoft.Extensions.Logging.Abstractions
在 Application 项目中,增加文件夹 Validatiors。并添加针对 QueryWeatherForecastCommand 的验证器。
代码实现如下:
using Application;
using FluentValidation;
using Microsoft.Extensions.Logging;
public class QueryWeatherForecastCommandValidator : AbstractValidator<QueryWeatherForecastCommand>
{
public QueryWeatherForecastCommandValidator(
ILogger<QueryWeatherForecastCommandValidator> logger
)
{
RuleFor(c => c.Name).NotEmpty();
logger.LogInformation("----- INSTANCE CREATED - {ClassName}", GetType().Name);
}
}
重新编译项目,通过编译。
下面,我们使用 MediatR 的命令处理管道来支持验证。
在 Application 项目中,增加文件夹 Behaviors,在其中创建验证处理。
namespace Application;
using MediatR;
using Microsoft.Extensions.Logging;
using FluentValidation;
public class ValidatorBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly ILogger<ValidatorBehavior<TRequest, TResponse>> _logger;
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidatorBehavior(IEnumerable<IValidator<TRequest>> validators, ILogger<ValidatorBehavior<TRequest, TResponse>> logger)
{
_validators = validators;
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var typeName = request.GetGenericTypeName();
_logger.LogInformation("----- Validating command {CommandType}", typeName);
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(result => result.Errors)
.Where(error => error != null)
.ToList();
if (failures.Any())
{
_logger.LogWarning("Validation errors - {CommandType} - Command: {@Command} - Errors: {@ValidationErrors}", typeName, request, failures);
throw new Exception(
$"Command Validation Errors for type {typeof(TRequest).Name}", new ValidationException("Validation exception", failures));
}
return await next();
}
}
其中的 GetGenericTypeName() 是一个扩展方法 ,定义在 Extensions 文件夹中的 GenericTypeExtensions 类中。
该扩展方法定义如下:
namespace Application;
/*
* 扩展方法,用于获取对象实例或者类型的字符串名称
*/
public static class GenericTypeExtensions
{
public static string GetGenericTypeName(this Type type)
{
string typeName;
if (type.IsGenericType)
{
var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray());
typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>";
}
else
{
typeName = type.Name;
}
return typeName;
}
public static string GetGenericTypeName(this object @object)
{
return @object.GetType().GetGenericTypeName();
}
}
回到 Web 项目中,我们注册定义的验证器,并定义 MediatR 的处理管道。将 MediatorModule 代码修改为如下所示:
namespace Web;
using System.Reflection;
using Application;
using Autofac;
using FluentValidation;
using MediatR;
public class MediatorModule : Autofac.Module {
protected override void Load (ContainerBuilder builder) {
// Register the Command's Validators (Validators based on FluentValidation library)
builder
.RegisterAssemblyTypes(
typeof(QueryWeatherForecastCommandValidator).GetTypeInfo().Assembly)
.Where(t => t.IsClosedTypeOf(typeof(IValidator<>)))
.AsImplementedInterfaces();
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>));
}
}
重新编译并运行,可以在控制台,看到如下输出:
info: QueryWeatherForecastCommandValidator[0]
----- INSTANCE CREATED - QueryWeatherForecastCommandValidator
info: Application.ValidatorBehavior[0]
----- Validating command QueryWeatherForecastCommand
8. 增加 MediatR 处理管道日志支持
在 Application 的 Behaviors 文件夹下,增加 LoggingBehavior.cs 文件。它将会在调用实际的处理之前和之后记录日志。
namespace Application;
using MediatR;
using Microsoft.Extensions.Logging;
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) => _logger = logger;
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
_logger.LogInformation("----- Handling command {CommandName} ({@Command})", request.GetGenericTypeName(), request);
var response = await next();
_logger.LogInformation("----- Command {CommandName} handled - response: {@Response}", request.GetGenericTypeName(), response);
return response;
}
}
回到 Web 项目中的 MediatorModule 文件,在验证处理之前增加日志支持,特别需要注意注册的顺序。
namespace Web;
using System.Reflection;
using Application;
using Autofac;
using FluentValidation;
using MediatR;
public class MediatorModule : Autofac.Module {
protected override void Load (ContainerBuilder builder) {
// Register the Command's Validators (Validators based on FluentValidation library)
builder
.RegisterAssemblyTypes(
typeof(QueryWeatherForecastCommandValidator).GetTypeInfo().Assembly)
.Where(t => t.IsClosedTypeOf(typeof(IValidator<>)))
.AsImplementedInterfaces();
builder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>));
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>));
}
}
重新编译运行,访问 WeatherForecase API,可以在控制台输出中看到:
info: QueryWeatherForecastCommandValidator[0]
----- INSTANCE CREATED - QueryWeatherForecastCommandValidator
info: Application.LoggingBehavior[0]
----- Handling command QueryWeatherForecastCommand (Application.QueryWeatherForecastCommand)
info: Application.ValidatorBehavior[0]
----- Validating command QueryWeatherForecastCommand
info: Application.LoggingBehavior[0]
----- Command QueryWeatherForecastCommand handled - response: Application.WeatherForecast, Application.WeatherForecast, Application.WeatherForecast, Application.WeatherForecast, Application.WeatherForecast