七、CQRS 的作用
了解了 CQRS 模式、Mediator 模式和MediatR
NuGet 包是什么之后,现在是时候将它们应用到 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
包:
-
Navigate to your
Travel.Application
project using thedotnet
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 应用中创建记录器的包。 -
While in the
Travel.Application
project, create a directory and name itCommon
. Then, create a directory in theCommon
folder, and name itBehaviors
.现在,让我们创建四个 c#文件,并将它们命名为
LoggingBehavior.cs
、PerformanceBehavior.cs
、UnhandledExeptionBehavior.cs
和ValidationBehavior.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.Logging
和MediatR.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
让完全控制创建数据验证。 因此,它对所有验证场景都非常有用:
-
Let's add the
FluentValidation
package to our application:dotnet add package FluentValidation
前面的命令安装。net 的流行验证库。 该包使用一个连贯的接口来构建强类型规则。
-
下面的命令安装
FluentValidation
的依赖注入扩展:
现在我们可以创建另一个行为,这将用于验证。 下面的代码通过使用IValidator
和ValidationContext
验证PipeLineBehavior
中的请求,IValidator
定义了一个特定类型的验证器,ValidationContext
从FluentValidation
命名空间创建一个新的验证上下文实例:
// 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();
}
}
}
现在创建另一个目录并命名为Exceptions
在Common``Travel.Application
项目,因为这个文件夹的目录将的位置没有找到和验证异常。
在创建Exceptions
文件夹后,创建两个 c#文件——NotFoundException.cs
和ValidationException.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.cs
和IEmailService.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.Application
的Common
文件夹中为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 让您无需编写大量代码就可以映射对象,稍后您将看到这一点。
现在我们需要使用AutoMapper
NuGet 包自动设置对象到对象的映射,该包是由编写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.cs
和MappingProfile.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
来创建处理程序。 如果您注意到了,我们在这里使用DbContext
和AutoMapper
来处理请求并为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; }
}
}
前面的代码是我们将要创建的GetToursQuery
的Tours
视图模型:
// 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
以实现此功能。
现在让我们进入下一个有趣的部分,编写命令。
编写命令
现在我们将创建第一个命令,这是一个保存、更新或删除数据的请求,以及一个命令处理程序,它将解析该命令需要什么。 同样,控制器负责将命令发送或分派给相关的处理程序。
现在我们到了为TourLists
和TourPackages
创建命令的部分。
让我们在Tour.Application
项目的TourLists
目录中创建一个文件夹,并将其命名为Commands
。 然后,让我们在该文件夹中创建三个文件夹,并将其命名为CreateTourList
、DeleteTourList
和UpdateTourList
。
现在是时候创建一些命令和命令验证器了。 在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
。 然后,让我们在该文件夹中创建四个文件夹,并将它们命名为CreateTourPackage
、DeleteTourPackage
、UpdateTourPackage
和UpdateTourPackageDetail
。
现在,在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
目录下创建两个新文件夹Files
和Services
。
在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
项目中的TourPackagesController
和TourListController
文件:
// 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 通过捕获错误并以一致的方式处理它们而生成的任何NotFoundException
、ValidationException
和UnknownException
。
最后要做的是更新 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
请求:
图 7.1 -使用 Swagger UI 测试 TourLists 控制器
测试此处TourLists
的GET
请求的响应可以在图 7.1 中看到。 可以看到控制器响应200
与TourList
对象。
现在让我们总结一下所学的一切来结束这一章。
总结
这里的全部内容相当于整整一章。 让我们总结一下重要的部分。
您终于看到了如何应用CQRS
、MediatR
和Pipeline Behavior
。 MediatR
包使得 CQRS 模式在 ASP 中很容易实现。 净的核心。 Pipeline Behavior
包允许您在处理程序处理命令之前和之后在命令中运行许多方法,例如验证或日志记录。
您学习了如何使用FluentValidation
包,这是一个用于验证模型的强大库。
您还学习了如何使用AutoMapper
包,这个库允许您通过编写几行代码将一个对象映射到另一个对象。
最后,您看到了如何使用IServiceCollection
在Startup.cs
文件中创建一个干净的依赖项注入。
在此基础上,我们制作了 ASP.NET Core 5 应用更具可测试性和可扩展性。 在下一章中,我们将使用 Serilog 来登录 ASP.NET Core 5,我们还将实现 API 版本控制。
版权属于:月萌API www.moonapi.com,转载请注明出处