• 使用 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. 参考资料

  • 相关阅读:
    全局函数和静态函数
    C语言变量总结
    #ifdef、#ifndef 与 #endif
    #include与#define的意义
    exit
    字符常量
    void *:万能指针
    算法(Algorithms)第4版 练习 链表类 1.3.19~1.3.29
    算法(Algorithms)第4版 练习 1.3.219
    算法(Algorithms)第4版 练习 1.3.20
  • 原文地址:https://www.cnblogs.com/haogj/p/16667750.html
Copyright © 2020-2023  润新知