七、CQRS 的作用

了解了 CQRS 模式、Mediator 模式和MediatRNuGet 包是什么之后,现在是时候将它们应用到 ASP 中了.NET Core 5 的 Web API。 上面提到的模式和包将为我们的应用带来价值,使其具有很强的可伸缩性、可测试性和可读性。

在本章中,我们将在应用中编写代码时涵盖以下主题:

  • 实现 CQRS
  • 添加MediatR
  • 创建MediatR管道行为
  • 使用FluentValidation
  • 使用AutoMapper
  • 编写查询
  • 写命令
  • IServiceCollection

技术要求

以下是你完成本章所需要的东西:

  • Visual Studio 2019, Visual Studio for Mac,或 Rider
  • . net CLI

你可以到以下链接查看本章已完成的源代码:https://github.com/PacktPublishing/ASP.NET-Core-and-Vue.js/tree/master/Chapter07

实施 CQRS

下面是如何在 ASP 中使用MediatR包的步骤.NET Core 应用。 MediatR包的任务是帮助您轻松实现 CQRS 和 Mediator 模式。

让我们首先通过删除应用中可以找到的所有Class1.cs文件来清理我们的解决方案。 Class1.cs文件是在我们创建项目时生成的。

添加 MediatR 包

我们现在要安装MediatR包:

  1. Navigate to your Travel.Application project using the dotnet CLI. Then we need to install some NuGet packages by running the following commands:

    dotnet add package MediatR

    前面的命令将. net 中的中介实现安装到Travel.Application项目中。

    下面的命令安装 ASP 的MediatR扩展.NET Core:

    dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

    下面的命令安装Microsoft.Extensions.Logging的日志抽象:

    dotnet add package Microsoft.Extensions.Logging.Abstractions

    前面的命令安装Microsoft.Extensions.Logging.Abstractions,这是一个用于在。net 应用中创建记录器的包。

  2. While in the Travel.Application project, create a directory and name it Common. Then, create a directory in the Common folder, and name it Behaviors.

    现在,让我们创建四个 c#文件,并将它们命名为LoggingBehavior.csPerformanceBehavior.csUnhandledExeptionBehavior.csValidationBehavior.cs。 所有的都将在Behaviors文件夹中,该文件夹将是一个管道,用于对进入处理程序的任何请求进行预处理和后处理。

    请注意

    我将在这里截断任何不必要的代码。 请到本章的 GitHub 存储库查看每个文件的完整源代码。

创建 MediatR 管道行为

MediatR中的管道行为类似于请求和处理程序之间的中间件。 验证就是一个很好的例子,因此处理程序只处理必要且有效的请求。 您还可以在这里进行日志记录和其他操作,这取决于您正在解决的问题。

我们将在这里编写我们的第一个MediatR管道行为:

// LoggingBehavior.cs

using MediatR.Pipeline;

namespace Travel.Application.Common.Behaviors
{
  public class LoggingBehavior<TRequest> :
   IRequestPreProcessor<TRequest>
  {
    
    public async Task Process(TRequest request,
      CancellationToken cancellationToken)
    {
      var requestName = typeof(TRequest).Name;
      _logger.LogInformation("Travel Request: {@Request}",
       requestName, request);
    }
  }
}

LoggingBehavior.cs的代码用于使用Microsoft.Extensions.LoggingMediatR.Pipeline名称空间记录请求。

IRequestPreProcessor是一个接口,用于为处理程序预处理已定义的请求,而IRequest是一个标记,用于表示带有响应的请求,您也将在命令和以后的映射查询中找到该响应。

下面的代码记录响应的运行时间(以毫秒为单位PipelineBehavior)。 PipelineBehavior包装内部处理程序,并在请求中添加一个额外行为的实现:

// PerformanceBehavior.cs

using MediatR;

namespace Travel.Application.Common.Behaviors
{
  internal class PerformanceBehavior<TRequest, TResponse> :
    IPipelineBehavior<TRequest, TResponse>
  {
    
    public async Task<TResponse> Handle(TRequest request,
      CancellationToken cancellationToken,
        RequestHandlerDelegate<TResponse> next)
    {
      
      _logger.LogWarning("Travel Long Running Request:
        {Name} ({ElapsedMilliseconds} milliseconds)
          {@Request}",
        requestName, elapsedMilliseconds, request);
      return response;
    }
  }
}

您还将在这里找到,它调用管道中的下一个东西。 关键字next在中间件实现中很常见,这意味着转到修改请求的下一个函数:

// UnhandledExceptionBehavior.cs

using MediatR;

namespace Travel.Application.Common.Behaviors
{
  public class UnhandledExceptionBehavior<TRequest,
    TResponse> : IPipelineBehavior<TRequest, TResponse>
  {
    private readonly ILogger<TRequest> _logger;
    
    public async Task<TResponse> Handle(TRequest request,
      CancellationToken cancellationToken, 
        RequestHandlerDelegate<TResponse> next)
    {
      Try { return await next(); }
      catch (Exception ex)
      {
        var requestName = typeof(TRequest).Name;
        _logger.LogError(ex, "Travel Request: Unhandled
          Exception for Request {Name} {@Request}", 
            requestName, request);
        throw;
      }
    }
  }
}

还有一件事在PipelineBehavior中缺失了。 是一种验证机制ValidationBehavior.cs,我们接下来将添加该机制。

我们还在Travel.Application项目中。 我们再添加两个 NuGet 包,其中包含FluentValidation

使用 FluentValidation

FluentValidation让完全控制创建数据验证。 因此,它对所有验证场景都非常有用:

  1. Let's add the FluentValidation package to our application:

    dotnet add package FluentValidation

    前面的命令安装。net 的流行验证库。 该包使用一个连贯的接口来构建强类型规则。

  2. 下面的命令安装FluentValidation的依赖注入扩展:

现在我们可以创建另一个行为,这将用于验证。 下面的代码通过使用IValidatorValidationContext验证PipeLineBehavior中的请求,IValidator定义了一个特定类型的验证器,ValidationContextFluentValidation命名空间创建一个新的验证上下文实例:

// ValidationBehavior.cs

using FluentValidation;
using MediatR;
using ValidationException = Travel.Application.Common.Exceptions.ValidationException;

namespace Travel.Application.Common.Behaviors
{
  public class ValidationBehavior<TRequest, TResponse> :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
  {
    private readonly IEnumerable<IValidator<TRequest>>
      _validators;
    public ValidationBehavior
      (IEnumerable<IValidator<TRequest>> validators)
    { _validators = validators; }
    public async Task<TResponse> Handle(TRequest request,
      CancellationToken cancellationToken,
        RequestHandlerDelegate<TResponse> next)
    {
      if (!_validators.Any()) return await next();
      var context = new 
        ValidationContext<TRequest>(request);
      var validationResults = await Task.WhenAll
       (_validators.Select(v => v.ValidateAsync(context,
          cancellationToken)));
      var failures = validationResults.SelectMany(r =>
        r.Errors).Where(f => f != null).ToList();
      
      return await next();
    }
  }
}

现在创建另一个目录并命名为ExceptionsCommon``Travel.Application项目,因为这个文件夹的目录将的位置没有找到验证异常。

在创建Exceptions文件夹后,创建两个 c#文件——NotFoundException.csValidationException.cs:

// NotFoundException.cs

using System;
namespace Travel.Application.Common.Exceptions
{
  public class NotFoundException : Exception
  {
    public NotFoundException()
      : base() { }
    public NotFoundException(string message)
      : base(message) { }
    public NotFoundException(string message, Exception
      innerException)
      : base(message, innerException) { }
    public NotFoundException(string name, object key)
      : base($"Entity \"{name}\" ({key}) was not found.") { }
  }
}

前面的代码是一个重载方法,用于抛出带有命令内部含义的NotFoundException。 您很快就会完成这项工作。

下面的代码是针对一个或多个验证失败发生的异常:

// ValidationException.cs

using FluentValidation.Results;

namespace Travel.Application.Common.Exceptions
{
  public class ValidationException : Exception
  {
    public ValidationException()
      : base("One or more validation failures have
         occurred.")
    {
      Errors = new Dictionary<string, string[]>();
    }
    public ValidationException
      (IEnumerable<ValidationFailure> failures)
      : this()
    {
      var failureGroups = failures
        .GroupBy(e => e.PropertyName, e => e.ErrorMessage);
      foreach (var failureGroup in failureGroups)
      {  }
    }
    public IDictionary<string, string[]> Errors { get; }
  }
}

让我们在Common文件夹中创建一个Interfaces目录。 这个目录将是两个简单服务的接口的位置。

创建完Interfaces文件夹后,让我们创建两个 c#文件:IDateTime.csIEmailService.cs:

// IDateTime.cs

using System;
namespace Travel.Application.Common.Interfaces
{
  public interface IDateTime
  {
    DateTime NowUtc { get; }
  }
}

前面的代码是我们稍后将创建的DateTime服务的契约:

// IEmailService.cs

…
namespace Travel.Application.Common.Interfaces
{
  public interface IEmailService
  {
    Task SendAsync(EmailDto emailRequest);
  }
}

前面的代码是我们稍后将创建的EmailService服务的契约。

现在,让我们在Travel.ApplicationCommon文件夹中为DbContext创建一个接口,但首先我们需要安装EntityFrameworkCore包:

dotnet add package Microsoft.EntityFrameworkCore

上述dotnet命令将安装 Entity Framework Core NuGet 包。

现在让我们在IapplicationDbContext中创建接口:

…
namespace Travel.Application.Common.Interfaces
{
  public interface IApplicationDbContext
  {
    DbSet<TourList> TourLists { get; set; }
    DbSet<TourPackage> TourPackages { get; set; }
    Task<int> SaveChangesAsync(CancellationToken
      cancellationToken);
  }
}

前面的代码是我们在第 5 章设置 DbContext 和控制器中创建的TravelDbContext的契约。 一旦合同即将实施,我们将稍后更新TravelDbContext

我们刚刚使用 Fluent API 完成了验证的设置,现在让我们继续将数据传输对象映射到实体,反之亦然。

使用 AutoMapper

AutoMapper 是一个流行的库,它使用基于约定的对象到对象映射器。 因此,AutoMapper 让您无需编写大量代码就可以映射对象,稍后您将看到这一点。

现在我们需要使用AutoMapperNuGet 包自动设置对象到对象的映射,该包是由编写MediatR库的同一个人编写的。 我喜欢AutoMapper,因为它简化了映射和投影。

下面的命令安装AutoMapper:

dotnet add package AutoMapper

下面的命令安装 ASP 的AutoMapper扩展.NET Core:

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

现在让我们为映射器创建文件。 在Travel.Application项目的Common中创建一个新目录并将其命名为Mappings

在创建Mappings文件夹后,创建两个 c#文件:ImapFrom.csMappingProfile.cs:

// IMapFrom.cs

using AutoMapper;
namespace Travel.Application.Common.Mappings
{
  public interface IMapFrom<T>
  {
    void Mapping(Profile profile) =>
      profile.CreateMap(typeof(T), GetType());
  }
}

前面的代码是用于从程序集应用映射的接口。 您将注意到有一个从AutoMapper传递过来的Profile类型。 Profile是一个配置,它将根据命名约定为您执行映射。

前面的代码允许我们对映射配置进行分组:

// MappingProfile.cs

using AutoMapper;

namespace Travel.Application.Common.Mappings
{
  public class MappingProfile : Profile
  {
    public MappingProfile()
    {
      ApplyMappingsFromAssembly
       (Assembly.GetExecutingAssembly());
    }
    private void ApplyMappingsFromAssembly(Assembly
      assembly)
    {
      var types = assembly.GetExportedTypes()
        .Where(t => t.GetInterfaces().Any(i =>
          i.IsGenericType && i.GetGenericTypeDefinition()
            == typeof(IMapFrom<>)))
        .ToList();
      foreach (var type in types)
      {
        var instance = Activator.CreateInstance(type);
        var methodInfo = type.GetMethod("Mapping")
                         ?? 
          type.GetInterface("IMapFrom`1").GetMethod
            ("Mapping");
        methodInfo?.Invoke(instance, new object[] { this
          });
      }
    }
  }
}

接下来,我们在Travel.Application项目中创建一个名为TourLists的目录。 然后,在TourLists文件夹中创建名为的另一个目录。 最后,我们在Queries文件夹中创建一个名为ExportTours的目录。

在创建三个嵌套目录之后,在Queries文件夹中创建一个名为TourItemFileRecord.cs的 c#文件:

using Travel.Application.Common.Mappings;
…
namespace Travel.Application.TourLists.Queries.ExportTours
{
  public class TourPackageRecord : IMapFrom<TourPackage>
  {
    public string Name { get; set; }
    public string MapLocation { get; set; }
  }
}

我们将要创建的CsvFileBuilder文件将需要前面的代码来创建参数的类型。

让我们在Common目录的Interfaces文件夹中创建另一个 c#接口文件ICsvFileBuilder.cs:

…
namespace Travel.Application.Common.Interfaces
{
  public interface ICsvFileBuilder
  {
    byte[] BuildTourPackagesFile
      (IEnumerable<TourPackageRecord> records);
  }
}

前面的代码是CsvFileBuilder的契约,我们稍后将创建它。

现在让我们在ExportTours文件夹中再创建两个 c#文件。 一个是ExportToursVm.cs文件,另一个是ExportToursQuery.cs文件,我们将在下一节中看到:

// ExportToursVm.cs

namespace Travel.Application.TourLists.Queries.ExportTours
{
  public class ExportToursVm
  {
    public string FileName { get; set; }
    public string ContentType { get; set; }
    public byte[] Content { get; set; }
  }
}

前面的代码是一个视图模型,我们将在后面的文件构建器中使用它。

现在,在编写了ExportToursVm类之后,我们可以继续下一节,即如何编写查询。

编写查询

现在,我们将创建第一个查询,这是一个读取数据的请求,以及一个查询处理程序,它将解析查询所需的内容。 同样,控制器负责将查询发送或分派给相关的处理程序。

下面是ExportToursQuery及其处理程序的代码块。 稍后您将看到控制器如何在中介器的Send方法中使用ExportToursQuery作为参数:

// ExportToursQuery.cs

using AutoMapper;
using AutoMapper.QueryableExtensions;
using MediatR;

namespace Travel.Application.TourLists.Queries.ExportTours
{
  public class ExportToursQuery : IRequest<ExportToursVm>
  {
    public int ListId { get; set; }
  }
  public class ExportToursQueryHandler :
    IRequestHandler<ExportToursQuery, ExportToursVm>
  {
    
    public async Task<ExportToursVm> 
      Handle(ExportToursQuery request, CancellationToken
        cancellationToken)
    {
      var vm = new ExportToursVm();
      
      vm.ContentType = "text/csv";
      vm.FileName = "TourPackages.csv";
      return await Task.FromResult(vm);
    }
  }
}

您可以通过派生MediatR包的IRequestsHandler来创建处理程序。 如果您注意到了,我们在这里使用DbContextAutoMapper来处理请求并为MediatR包创建响应。

接下来,在Travel.Application项目的root目录中创建一个Dtos文件夹。 Dtos文件夹将具有与Common目录相同的级别。

创建完Dtos文件夹后,让我们在里面创建两个 c#文件:

// TourListDto.cs

…
namespace Travel.Application.Dtos.Tour
{
  public class TourListDto : IMapFrom<TourList>
  {
    public TourListDto()
    {
      Items = new List<TourPackageDto>();
    }
    public IList<TourPackageDto> Items { get; set; }
    public int Id { get; set; }
    public string City { get; set; }
    public string About { get; set; }
  }
}

前面的代码是一个数据传输对象,或DTO,用于TourList。 下面的代码是TourPackage的 DTO:

// TourPackageDto.cs

using AutoMapper;
…
namespace Travel.Application.Dtos.Tour
{
  public class TourPackageDto : IMapFrom<TourPackage>
  {
    public int Id { get; set; }
    …
    public void Mapping(Profile profile)
    {
      profile.CreateMap<TourPackage, TourPackageDto>()
        .ForMember(d =>
          d.Currency, opt =>
          opt.MapFrom(s =>
            (int)s.Currency));
    }
  }
}

使用与Dtos目录相同的方式,让我们在Travel.Application项目中创建另一个目录,并将其命名为TourLists。 然后,在TourLists目录中创建一个名为GetTours的文件夹。

创建完GetTours文件夹后,在里面创建两个 c#文件:

// ToursVm.cs

using System.Collections.Generic;
using Travel.Application.Dtos.Tour;
namespace Travel.Application.TourLists.Queries.GetTours
{
  public class ToursVm
  {
    public IList<TourListDto> Lists { get; set; }
  }
}

前面的代码是我们将要创建的GetToursQueryTours视图模型:

// GetToursQuery.cs

namespace Travel.Application.TourLists.Queries.GetTours
{
  public class GetToursQuery : IRequest<ToursVm> { }
  public class GetToursQueryHandler :
    IRequestHandler<GetToursQuery, ToursVm>
  {
    …
    public async Task<ToursVm> Handle(GetToursQuery 
      request, CancellationToken cancellationToken)
    {
      return new ToursVm
      {
        Lists = await _context.TourLists
          .ProjectTo<TourListDto>
            (_mapper.ConfigurationProvider)
          .OrderBy(t => t.City)
          .ToListAsync(cancellationToken)
      };
    }
  }
}

前面的代码是GetToursQuery的处理程序,中介从控制器发送该处理程序。 稍后我们将更新TourListsController以实现此功能。

现在让我们进入下一个有趣的部分,编写命令

编写命令

现在我们将创建第一个命令,这是一个保存、更新或删除数据的请求,以及一个命令处理程序,它将解析该命令需要什么。 同样,控制器负责将命令发送或分派给相关的处理程序。

现在我们到了为TourListsTourPackages创建命令的部分。

让我们在Tour.Application项目的TourLists目录中创建一个文件夹,并将其命名为Commands。 然后,让我们在该文件夹中创建三个文件夹,并将其命名为CreateTourListDeleteTourListUpdateTourList

现在是时候创建一些命令和命令验证器了。 在CreateTourList文件夹中创建两个 c#文件:

// CreateTourListCommand.cs

using MediatR;

namespace Travel.Application.TourLists.Commands.CreateTourList
{
  public partial class CreateTourListCommand :
    IRequest<int>
  {

  }
  public class CreateTourListCommandHandler :
    IRequestHandler<CreateTourListCommand, int>
  {
    private readonly IApplicationDbContext _context;
    public CreateTourListCommandHandler
      (IApplicationDbContext context)
    { _context = context; }
    public async Task<int> Handle(CreateTourListCommand
      request, CancellationToken cancellationToken)
    {
      var entity = new TourList { City = request.City };
      _context.TourLists.Add(entity);
      await _context.SaveChangesAsync(cancellationToken);
      return entity.Id;
    }
  }
}

前面的代码是创建新的TourList的命令,我们在这里使用的是MediatR包:

// CreateTourListCommandValidator.cs

using FluentValidation;
…
namespace Travel.Application.TourLists.Commands.CreateTourList
{
  public class CreateTourListCommandValidator :
    AbstractValidator<CreateTourListCommand>
  {
    private readonly IApplicationDbContext _context;
    public CreateTourListCommandValidator
      (IApplicationDbContext context)
    {
      _context = context;
      RuleFor(v => v.City)
        …
        .NotEmpty().WithMessage("About is required");
   }
  }
}

前面的代码是CreateTourListCommand的验证器,我们这里使用的是FluentValidation包中的RuleFor

RuleFor 是针对特定属性的验证规则的构建器。 也就是说,FluentValidation是一个替代数据注解的验证库。 您应该使用它而不是数据注释,因为它帮助您编写干净和可维护的代码。

现在,在DeleteTourList文件夹中创建一个 c#文件:

// DeleteTourListCommand.cs

using MediatR;

namespace Travel.Application.TourLists.Commands.DeleteTourList
{
  public class DeleteTourListCommand : IRequest
  {
    public int Id { get; set; }
  }
  public class DeleteTourListCommandHandler :
    IRequestHandler<DeleteTourListCommand>
  {
    private readonly IApplicationDbContext _context;
    public DeleteTourListCommandHandler
      (IApplicationDbContext context)
    {
      _context = context;
    }
    public async Task<Unit> Handle(DeleteTourListCommand
      request, CancellationToken cancellationToken)
    {
      var entity = await _context.TourLists
        .Where(l => l.Id == request.Id)
        .SingleOrDefaultAsync(cancellationToken);
      
      _context.TourLists.Remove(entity);
      await _context.SaveChangesAsync(cancellationToken);
      return Unit.Value;
    }
  }
}

前面的代码是用于删除TourList的命令,我们在这里使用的是MediatR包。 这里的Unit类型来自MediatR包,它表示没有返回值。 因为void不是有效的返回类型,所以Unit类型代表一个 void 类型。

现在,在UpdateTourList文件夹中创建两个 c#文件:

// UpdateTourListCommand.cs

using MediatR;

namespace Travel.Application.TourLists.Commands.UpdateTourList
{
  public class UpdateTourListCommand : IRequest
  {
    
  }
  public class UpdateTourListCommandHandler :
    IRequestHandler<UpdateTourListCommand>
  {
    private readonly IApplicationDbContext _context;
    public UpdateTourListCommandHandler
      (IApplicationDbContext context)
    { _context = context; }
    public async Task<Unit> Handle(UpdateTourListCommand 
      request, CancellationToken cancellationToken)
    {
      var entity = await
        _context.TourLists.FindAsync(request.Id);
      
      entity.City = request.City;
      await _context.SaveChangesAsync(cancellationToken);
      return Unit.Value;
    }
  }
}

前面的代码是更新数据库中现有TourList数据的命令。 同样,这里使用的是Unit类型:

// UpdateTourListCommandValidator.cs

using FluentValidation;
…
namespace Travel.Application.TourLists.Commands.UpdateTourList
{
  public class UpdateTourListCommandValidator :
    AbstractValidator<UpdateTourListCommand>
  {
    private readonly IApplicationDbContext _context;
    public UpdateTourListCommandValidator
      (IApplicationDbContext context)
    {
      _context = context;
      RuleFor(v => v.City)
        .NotEmpty().WithMessage("City is required.")
        …
    }
  }
}

前面的代码是UpdateTourListCommand的验证器,我们在这里使用的是FluentValidation包。

现在是TourPackage。 让我们在Travel.Application项目的根文件夹中创建一个文件夹,就像我们对TourLists所做的那样,并将其命名为TourPackages

在创建了TourPackages目录之后,让我们在TourPackages目录中创建一个文件夹并将其命名为Commands。 然后,让我们在该文件夹中创建四个文件夹,并将它们命名为CreateTourPackageDeleteTourPackageUpdateTourPackageUpdateTourPackageDetail

现在,在CreateTourPackage文件夹中创建两个 c#文件:

// CreateTourPackageCommand.cs

using MediatR;

namespace Travel.Application.TourPackages.Commands.CreateTourPackage
{
  public class CreateTourPackageCommand : IRequest<int>
  {
    
  }
  public class CreateTourPackageCommandHandler :
    IRequestHandler<CreateTourPackageCommand, int>
  {
    private readonly IApplicationDbContext _context;
    public CreateTourPackageCommandHandler
      (IApplicationDbContext context)
    { _context = context;}
    public async Task<int> Handle(CreateTourPackageCommand 
      request, CancellationToken cancellationToken)
    {
      var entity = new TourPackage {  };
      _context.TourPackages.Add(entity);
      await _context.SaveChangesAsync(cancellationToken);
      return entity.Id;
    }
  }
}

前面的代码是创建新的TourPackage的命令,我们在这里使用的是MediatR包:

// CreateTourPackageCommandValidator.cs

using FluentValidation;namespace Travel.Application.TourPackages.Commands.CreateTourPackage
{
  public class CreateTourPackageCommandValidator :
    AbstractValidator<CreateTourPackageCommand>
  {
    private readonly IApplicationDbContext _context;
    public CreateTourPackageCommandValidator
      (IApplicationDbContext context)
    {
      _context = context;
      RuleFor(v => v.Name)
        .NotEmpty().WithMessage("Name is required.")
        …
    }
    public async Task<bool> BeUniqueName(string name,
      CancellationToken cancellationToken)
    {
      return await _context.TourPackages
        .AllAsync(l => l.Name != name);
    }
  }
}

前面的代码是CreateTourPackageCommand的验证器,我们在这里使用的是FluentValidation包。

现在在DeleteTourList文件夹中创建一个 c#文件:

// DeleteTourPackageCommand.cs

using MediatR;

namespace Travel.Application.TourPackages.Commands.DeleteTourPackage
{
  public class DeleteTourPackageCommand : IRequest
  {
    public int Id { get; set; }
  }
  public class DeleteTourPackageCommandHandler :
    IRequestHandler<DeleteTourPackageCommand>
  {
    private readonly IApplicationDbContext _context;
    public DeleteTourPackageCommandHandler
      (IApplicationDbContext context)
    {
      _context = context;
    }
    public async Task<Unit> Handle(DeleteTourPackageCommand
      request, CancellationToken cancellationToken)
    {
      var entity = await
        _context.TourPackages.FindAsync(request.Id);
      
      _context.TourPackages.Remove(entity);
      await _context.SaveChangesAsync(cancellationToken);
      return Unit.Value;
    }
  }
}

前面的代码是用于删除TourPackage的命令,我们在这里使用的是MediatR包。

现在在UpdateTourPackage文件夹中创建两个 c#文件:

// UpdateTourPackageCommand.cs

using MediatR;

namespace Travel.Application.TourPackages.Commands.UpdateTourPackage
{
  public partial class UpdateTourPackageCommand : IRequest
  {
    
  }
  public class UpdateTourPackageCommandHandler :
    IRequestHandler<UpdateTourPackageCommand>
  {
    private readonly IApplicationDbContext _context;
    public UpdateTourPackageCommandHandler
      (IApplicationDbContext context)
    { _context = context; }
    public async Task<Unit> Handle(UpdateTourPackageCommand
      request, CancellationToken cancellationToken)
    {
      var entity = await
        _context.TourPackages.FindAsync(request.Id);
      
      entity.Name = request.Name;
      await _context.SaveChangesAsync(cancellationToken);
      return Unit.Value;
    }
  }
}

前面的代码是用于更新TourPackage的命令,我们在这里使用的是MediatR包。

// UpdateTourPackageCommandValidator.cs

using FluentValidation;namespace Travel.Application.TourPackages.Commands.UpdateTourPackage
{
  public class UpdateTourPackageCommandValidator :
     AbstractValidator<UpdateTourPackageCommand>
  {
    private readonly IApplicationDbContext _context;
    public UpdateTourPackageCommandValidator
      (IApplicationDbContext context)
    {
      _context = context;
      RuleFor(v => v.Name)
        …
        .MustAsync(BeUniqueName).WithMessage("The specified
           name already exists.");
    }
    public async Task<bool> BeUniqueName(string name,
      CancellationToken cancellationToken)
    {
      return await _context.TourPackages
        .AllAsync(l => l.Name != name);
    }
  }
}

前面的代码是UpdateTourPackageCommand的验证器,我们在这里使用的是FluentValidation包。

现在在UpdateTourPackageDetail文件夹中创建两个 c#文件:

// UpdateTourPackageDetail.cs

using MediatR;

namespace Travel.Application.TourPackages.Commands.UpdateTourPackageDetail
{
  public class UpdateTourPackageDetailCommand : IRequest
  {
    
  }
  public class UpdateTourPackageDetailCommandHandler :
    IRequestHandler<UpdateTourPackageDetailCommand>
  {
    private readonly IApplicationDbContext _context;
    public UpdateTourPackageDetailCommandHandler
      (IApplicationDbContext context)
    {
      _context = context;
    }
    public async Task<Unit> Handle
      (UpdateTourPackageDetailCommand request, 
        CancellationToken cancellationToken)
    {
      var entity = await
        _context.TourPackages.FindAsync(request.Id);
      
      await _context.SaveChangesAsync(cancellationToken);
      return Unit.Value;
    }
  }
}

前面的代码是另一个用于更新TourPackage的命令,我们在这里使用的是MediatR包。

// UpdateTourPackageDetailCommandValidator.cs

using FluentValidation;namespace Travel.Application.TourPackages.Commands.UpdateTourPackageDetail
{
  public class UpdateTourPackageDetailCommandValidator :
    AbstractValidator<UpdateTourPackageDetailCommand>
  {
    private readonly IApplicationDbContext _context;
    public UpdateTourPackageDetailCommandValidator
      (IApplicationDbContext context)
    {
      _context = context;RuleFor(v => v.Currency)
        .NotEmpty().WithMessage("Currency is required");
    }
    public async Task<bool> BeUniqueName(string name,
      CancellationToken cancellationToken)
    {
      return await _context.TourPackages
        .AllAsync(l => l.Name != name);
    }
  }
}

前面的代码是UpdateTourPackageDetailCommand的验证器,我们在这里使用的是FluentValidation包。

现在让我们继续下一节,它是关于编写 iserviccollection 的。

编写 iserviccollection

IServiceCollection是来自DependencyInjection名称空间的接口。 我们将使用IServiceCollection进行依赖注入。

最后,有一个针对Travel.Application项目的依赖注入。 在Travel.Application项目的root文件夹中创建一个 c#文件:

// DependencyInjection.cs

namespace Travel.Application
{
  public static class DependencyInjection
  {
    public static IServiceCollection AddApplication(this
     IServiceCollection services)
    {
      services.AddAutoMapper
        (Assembly.GetExecutingAssembly());
      services.AddValidatorsFromAssembly
        (Assembly.GetExecutingAssembly());
      services.AddMediatR(Assembly.GetExecutingAssembly());
      services.AddTransient(typeof(IPipelineBehavior<,>),
        typeof(PerformanceBehavior<,>));
      services.AddTransient(typeof(IPipelineBehavior<,>),
        typeof(ValidationBehavior<,>));
      services.AddTransient(typeof(IPipelineBehavior<,>),
        typeof(UnhandledExceptionBehavior<,>));
      return services;
    }
  }
}

前面的代码是一个依赖注入容器方法。 您将在这里看到,IServiceCollection正在向服务描述符集合添加不同种类的服务。

我们将注入静态方法,AddApplication,在Startup文件的 Web API 项目,尤其是Startup文件,不需要声明任何对第三方库的依赖性等 AutoMapper 因为他们已经宣布在这个文件中。

现在让我们转到Travel.Domain项目,为Mail服务添加一个settings文件。 在Travel.Domain项目中创建一个文件夹并将其命名为Settings

在创建Settings目录之后,在其中创建一个 c#文件:

// MailSettings.cs

namespace Travel.Domain.Settings
{
  public class MailSettings
  {
    public string EmailFrom { get; set; }
    …
    public string DisplayName { get; set; }
  }
}

前面的代码用于我们稍后将创建的电子邮件服务的设置。

现在让我们向Travel.Application项目中再添加一个异常文件。 进入Travel.Application项目的Common目录,创建一个 c#文件:

// ApiException.cs

…
namespace Travel.Application.Common.Exceptions
{
  public class ApiException : Exception
  {
    public ApiException() : base() { }
    public ApiException(string message) : base(message) { }
    public ApiException(string message, params object[]
      args)
      : base(String.Format(CultureInfo.CurrentCulture,
        message, args)) { }
  }
}

前面的代码是用于我们稍后将在电子邮件服务中使用的一个异常。

让我们进入Travel.Shared项目,为这个项目添加一些 NuGet 包:

dotnet add package MailKit

上述 CLI 命令安装。net 邮件客户端库MailKit。 下面的命令行安装一个包,该包在 ASP. net 中提供额外的配置.NET Core 应用:

dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions

下面的 CLI 命令安装Mimekit,一个用于创建和解析 S/MIME、MIME 和 PGP 消息的包:

dotnet add package MimeKit

安装 CSV 文件读写包CsvHelper:

dotnet add package CsvHelper

安装完 NuGet 包后,让我们在Travel.Shared项目的root目录下创建两个新文件夹FilesServices

Files文件夹中,我们创建一个 c#文件:

// CsvFileBuilder.cs

using CsvHelper;
using System.IO;

namespace Travel.Shared.Files
{
  public class CsvFileBuilder : ICsvFileBuilder
  {
    public byte[] BuildTourPackagesFile
      (IEnumerable<TourPackageRecord> records)
    {
      using var memoryStream = new MemoryStream();
      
      return memoryStream.ToArray();
    }
  }
}

前面的代码是CsvFileBuilder的实现。

现在让我们在Services文件夹中创建两个 c#文件:

// EmailService.cs

using MimeKit;
using MailKit.Net.Smtp;

namespace Travel.Shared.Services
{
  public class EmailService : IEmailService
  {
    
    public async Task SendAsync(EmailDto request)
    {
      try
      { var email = new MimeMessage { Sender =
        MailboxAddress.Parse(request.From ??
         MailSettings.EmailFrom) };
        email.To.Add(MailboxAddress.Parse(request.To));
        
        await smtp.DisconnectAsync(true); }
      catch (System.Exception ex)
      { Logger.LogError(ex.Message, ex);
        throw new ApiException(ex.Message); }
    }
  }
}

前面的代码是IEmailService的实现。

// DateTimeService.cs

…
namespace Travel.Shared.Services
{
  public class DateTimeService : IDateTime
  {
    public DateTime NowUtc => DateTime.UtcNow;
  }
}

前面的代码是IDateTime的实现。 它只返回DateTime.UtcNow

对于Travel.Shared项目的依赖注入,让我们在Travel.Shared项目的root目录下创建一个 c#文件:

// DependencyInjection.cs

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;namespace Travel.Shared
{
  public static class DependencyInjection
  {
    public static IServiceCollection 
      AddInfrastructureShared(this IServiceCollection 
        services, IConfiguration config)
    {
      services.Configure<MailSettings>
        (config.GetSection("MailSettings"));
      services.AddTransient<IDateTime, DateTimeService>();
      services.AddTransient<IEmailService, EmailService>();
      services.AddTransient<ICsvFileBuilder,
        CsvFileBuilder>();
      return services;
    }
  }
}

前面的代码是另一个依赖注入容器方法,我们稍后将在Startup文件中添加该方法。

现在是最后一步了。 转到Travel.WebApi项目并更新appSettings.json文件:

// appSettings.json

{
  "Logging": {
    "LogLevel": {
      …
    }
  },
  "MailSettings": {
    "EmailFrom": "",
     …
    "DisplayName": ""
  },
  "AllowedHosts": "*"
}

该代码在appsettings.json中添加了MailSettings

接下来,我们在Travel.WebApi项目的Controllers文件夹中创建一个 c#文件:

// ApiController.cs

using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace Travel.WebApi.Controllers
{
  [ApiController]
  [Route("api/[controller]")]
  public abstract class ApiController : ControllerBase
  {
    private IMediator _mediator;
    protected IMediator Mediator => _mediator ??= 
      HttpContext.RequestServices.GetService<IMediator>();
  }
}

前面的代码是允许ApiContoller使用 Mediator 的属性注入。 与构造函数注入相比,我更喜欢这种方法,因为它简单。 属性注入使不再需要使用构造函数注入来维护所有控制器的参数和签名。

接下来,我们更新Travel.WebApi项目中的TourPackagesControllerTourListController文件:

// TourPackagesController.cs


namespace Travel.WebApi.Controllers
{
  [ApiController]
  [Route("api/[controller]")]
  public class TourPackagesController : ApiController
  {
    [HttpPost]
    public async Task<ActionResult<int>>
      Create(CreateTourPackageCommand command)
    { return await Mediator.Send(command); }
    [HttpPut("{id}")]
    public async Task<ActionResult> Update(int id,
      UpdateTourPackageCommand command)
    {

      await Mediator.Send(command);
    }
    [HttpPut("[action]")]
    public async Task<ActionResult> UpdateItemDetails(int
      id, UpdateTourPackageDetailCommand command)
    {

      await Mediator.Send(command);
    }
    [HttpDelete("{id}")]
    public async Task<ActionResult> Delete(int id)
    {
      await Mediator.Send(new DeleteTourPackageCommand { Id
        = id });
    }
  }
}

这个更新的TourPackagesController的代码使用 Mediator 发送命令,并从ApiController派生而来。

// TourListsController.cs


namespace Travel.WebApi.Controllers
{
  [ApiController]
  [Route("api/[controller]")]
  public class TourListsController : ApiController
  {
    [HttpGet]
    public async Task<ActionResult<ToursVm>> Get()
    {
      return await Mediator.Send(new GetToursQuery());
    }
    [HttpGet("{id}")]
    public async Task<FileResult> Get(int id)
    {
      var vm = await Mediator.Send(new ExportToursQuery {
        ListId = id });
      return File(vm.Content, vm.ContentType, vm.FileName);
    }
    [HttpPost]
    public async Task<ActionResult<int>>
      Create(CreateTourListCommand command)
    {
      return await Mediator.Send(command);
    }
    [HttpPut("{id}")]
    public async Task<ActionResult> Update(int id,
      UpdateTourListCommand command)
    { 
    await Mediator.Send(command);
    }
    [HttpDelete("{id}")]
    public async Task<ActionResult> Delete(int id)
    {
      await Mediator.Send(new DeleteTourListCommand { Id =
        id });
    }
  }
}

此更新的TourListsController代码使用 Mediator 发送命令,并派生自ApiController

在更新TourListsController之后,我们将为 API 添加一个过滤器,方法是在Travel.WebApi项目的root目录中创建一个新文件夹,并将其命名为Filter。 然后,在新创建的文件夹中创建一个 c#文件ApiExceptionFilter.cs,并为ApiExceptionFilter添加以下代码:

using System;
…
using Microsoft.AspNetCore.Mvc.Filters;
using Travel.Application.Common.Exceptions;
namespace Travel.WebApi.Filters
{
    public class ApiExceptionFilter :
      ExceptionFilterAttribute
    {
        private readonly IDictionary<Type,
          Action<ExceptionContext>> _exceptionHandlers;
        public ApiExceptionFilter()
        {
            …
        }
        public override void OnException(ExceptionContext
          context)
        {
            HandleException(context);
            base.OnException(context);
        }
        private void HandleException(ExceptionContext
          context)
        {
            …
        }
        private void HandleUnknownException
          (ExceptionContext context)
        {
            …
        }
        private void HandleValidationException
          (ExceptionContext context)
        {
            …
        }
        private void HandleNotFoundException
          (ExceptionContext context)
        {
            …
        }
    }
}

前一个类的任何一个方法都没有什么特别之处。 请到前面代码的 GitHub 库查看这些方法的实现。 我们创建的 API 异常过滤器将处理 Web API 通过捕获错误并以一致的方式处理它们而生成的任何NotFoundExceptionValidationExceptionUnknownException

最后要做的是更新 Web API 项目的Startup.cs文件:

// Startup.cs

using Microsoft.Extensions.DependencyInjection;
using Travel.Application;
using Travel.Data;
using Travel.Shared;
using Microsoft.AspNetCore.Mvc;
using Travel.WebApi.Filters;
…
namespace Travel.WebApi
{
  public class Startup
  {
    public Startup(IConfiguration configuration)
    { Configuration = configuration; }
    public IConfiguration Configuration { get; } 
    public void ConfigureServices(IServiceCollection
      services)
    {
      services.AddApplication();
      services.AddInfrastructureData();
      services.AddInfrastructureShared(Configuration);
      services.AddHttpContextAccessor();
      services.AddControllersWithViews(options =>
          options.Filters.Add(new ApiExceptionFilter()));
      services.Configure<ApiBehaviorOptions>(options =>
          options.SuppressModelStateInvalidFilter = true
      );
      …
    }
    // This method gets called by the runtime. Use this
      method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app,
      IWebHostEnvironment env)
    { … }
  }
}

Startup.cs文件的更新代码通过依赖注入注册应用、数据和共享项目的服务。 更新后的代码是在启动项目中保持容器引导的一种优雅的方式,而不会违反面向对象的设计原则。

现在我们可以检查控制器是否连接正确。 运行应用,然后转到 Swagger UI。 尝试TourLists控制器的GET请求:

Figure 7.1 – Using Swagger UI to test the TourLists controller

图 7.1 -使用 Swagger UI 测试 TourLists 控制器

测试此处TourListsGET请求的响应可以在图 7.1 中看到。 可以看到控制器响应200TourList对象。

现在让我们总结一下所学的一切来结束这一章。

总结

这里的全部内容相当于整整一章。 让我们总结一下重要的部分。

您终于看到了如何应用CQRSMediatRPipeline BehaviorMediatR包使得 CQRS 模式在 ASP 中很容易实现。 净的核心。 Pipeline Behavior包允许您在处理程序处理命令之前和之后在命令中运行许多方法,例如验证或日志记录。

您学习了如何使用FluentValidation包,这是一个用于验证模型的强大库。

您还学习了如何使用AutoMapper包,这个库允许您通过编写几行代码将一个对象映射到另一个对象。

最后,您看到了如何使用IServiceCollectionStartup.cs文件中创建一个干净的依赖项注入。

在此基础上,我们制作了 ASP.NET Core 5 应用更具可测试性和可扩展性。 在下一章中,我们将使用 Serilog 来登录 ASP.NET Core 5,我们还将实现 API 版本控制。