冠军

导航

使用 Autofac, MediatR 和 FluentValidator 构建松耦合 ASP.NET Core API 应用

使用 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 and Unit)
  • 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

9. 参考资料

posted on 2022-09-08 16:19  冠军  阅读(614)  评论(0编辑  收藏  举报