原文:Building Microservices On .NET Core – Part 2 Shaping microservice internal architecture with CQRS and MediatR
时间:2019年1月21日
作者:Ewelina Polska-Brzostowska, Lead .NET Developer
在有关在.NET Core中构建微服务的系列文章的第一篇文章中,我们将重点介绍典型微服务的内部体系结构。 根据微服务类型,可以考虑许多选项。 系统中的某些服务将是典型的CRUD,因此无需讨论其设计(除非从性能和可伸缩性角度来看它们至关重要)。
在本文中,我们将设计非平凡微服务的内部体系结构,该体系结构既负责管理其数据状态,又将其公开给外部世界。 基本上,我们的微服务将负责其数据的创建和各种修改,还将公开API,该API将允许其他服务和应用程序查询此数据。
完整解决方案的源代码可以在我们的Github上找到。
CQRS概览
想象一下ProductService
类。它实现了我们可以对产品执行的所有操作。在我的示例中,它是保险产品,但是在此情况下,这并不重要。代码的每个更改都需要调查其工作方式以及可能产生的副作用。这会导致代码增长,变得难以处理并且启动很费时间。
在许多应用程序中,有许多巨型类实现逻辑并包含可以使用给定类型的对象完成的所有操作。如何重构它们以拆分代码并共享功能?简化一下,我们可以区分两个主要的数据操作。操作可以更改或读取数据。因此,自然的方法是通过此类将它们分开。可以将更改数据的操作(Commands)与仅读取数据的操作(queries)区分开。在大多数系统中,读取和写入之间的差异至关重要。进行读取时,您没有进行任何验证或业务逻辑。但是您经常使用缓存。读和写操作的模型也(或必须是)大不相同。
CQRS – 命令查询责任隔离是一种模式,需要将执行查询逻辑的代码和模型与执行命令的代码和模型分开。
回到我们的示例–根据上述规则共享的ProductService
现在变为:
- 返回
IEnumerable
的FindAllProductsQuery
(也可以实现为另一个模型–包含ProductDto
集合的FindAllProductsResult
) - 返回
ProductDto
的FindProductByCodeQuery
- 使用输入
ProductDraftDto
的CreateProductDraftHandler
并将产品添加到我们的系统中。
上面的查询共享了一个模型,但是如果需要在结果中使用不同的数据,则应将模型分开(通常是这样)。
因此,我们现在有两个部分:命令或查询类以及结果类。
如何连接它们?
如何知道每个查询/命令的输入/输出是什么类型?
现在该介绍中介者了。在这种情况下,调解员要做的工作是将这些片段捆绑到单个请求中。
.NET Core 2.x和MediatR
我认为我们现在可以看到一些代码。 我们使用MediatR库来帮助我们在ProductService中实现CQRS模式。 MediatR是某种“内存总线”,它是应用程序不同部分之间通信的接口。
我们可以使用Package Manager Console将MediatR添加到项目中,键入:
Install-Package MediatR
接下来,仅通过添加代码services.AddMediatR();
将其注册到DI容器中。 在Startup
类的ConfigureServices
方法中。
要使用MediatR创建查询消息,我们需要添加实现IRequest接口的类,并指定查询类期望的响应类型:
public class FindProductByCodeQuery : IRequest
{
public string ProductCode { get; set; }
}
如何定义输入模型? 它是控制器动作的参数:
// GET api/products/{code}
[HttpGet("{code}")]
public async Task GetByCode([FromRoute]string code)
{
var result = await mediator.Send(new FindProductByCodeQuery{ ProductCode = code });
return new JsonResult(result);
}
现在,我们可以使用MediatR发送消息。 控制器非常苗条。 这里没有逻辑。 它的唯一职责是发送客户端JSON响应。 为了准备响应,我们发送中介消息-调用IMediator对象的Send方法(从DI容器注入-参见下文)。 我们发送具有属性ProductCode设置的FindProductByCodeQuery
对象。
private readonly IMediator mediator;
public ProductsController(IMediator mediator)
{
this.mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
在这里,我们需要定义我们的CQRS解决方案的另一部分。 它可以处理请求。 将回答给定类型的每个消息的类。
另外,MediatR使它更容易实现:
public class FindProductByCodeHandler : IRequestHandler
{
private readonly IProductRepository productRepository;
public FindProductByCodeHandler(IProductRepository productRepository)
{
this.productRepository = productRepository ?? throw new ArgumentNullException(nameof(productRepository));
}
}
如我们所见,处理程序使用输入和输出类型的定义实现IRequestHandler接口:
public interface IRequestHandler where TRequest : IRequest
{
Task Handle(TRequest request, CancellationToken cancellationToken);
}
在我们的示例中,FindProductByCodeHandler
定义向我们(和中介者)提供了“知道”如何响应FindProductByCodeQuery
消息并返回ProductDto
对象的信息。 现在我们需要定义如何处理消息。 该接口定义了我们应该实现的Handle方法。 我们将转到IProductRepository
并检索请求的对象:
public async Task Handle(FindProductByCodeQuery request, CancellationToken cancellationToken)
{
var result = await productRepository.FindOne(request.ProductCode);
return result != null ? new ProductDto
{
Code = result.Code,
Name = result.Name,
Description = result.Description,
Image = result.Image,
MaxNumberOfInsured = result.MaxNumberOfInsured,
Questions = result.Questions != null ? ProductMapper.ToQuestionDtoList(result.Questions) : null,
Covers = result.Covers != null ? ProductMapper.ToCoverDtoList(result.Covers) : null
} : null;
}
到结果类型的映射也在处理程序类中执行。 如果需要,我们可以使用例如 AutoMapper或实现一些自定义映射器。 我们还可以在此处添加缓存以及准备响应所需的任何其他逻辑。
用xUnit测试
现在,我们具有准备按ProductCode产品的功能。 让我们测试一下。 我们使用xUnit测试我们的.NET Core 2.x应用程序。
使用MediatR和CQRS进行测试非常简单。 我们在ProductsControllerTest
中创建了一个简短方法,该方法使用总线来测试控制器:
[Fact]
public async Task GetAll_ReturnsJsonResult_WithListOfProducts()
{
var client = factory.CreateClient();
var response = await client.DoGetAsync("/api/Products");
True(response.Count > 1);
}
我们还应该测试我们的处理程序,这是FindProductsHandlersTest中的测试之一:
[Fact]
public async Task FindProductByCodeHandler_ReturnsOneProduct()
{
var findProductByCodeHandler = new FindProductByCodeHandler(productRepository.Object);
var result = await findProductByCodeHandler.Handle(new Api.Queries.FindProductByCodeQuery { ProductCode = TestProductFactory.Travel().Code}, new System.Threading.CancellationToken());
Assert.NotNull(result);
}
productRepository是IProductRepository
的模拟,它是通过以下方式定义的:
private Mock productRepository;
private List products = new List
{
TestProductFactory.Travel(),
TestProductFactory.House()
};
public FindProductsHandlersTest()
{
productRepository = new Mock();
productRepository.Setup(x => x.FindAll()).Returns(Task.FromResult(products));
productRepository.Setup(x => x.FindOne(It.Is(s => products.Select(p => p.Code).Contains(s)))).Returns(Task.FromResult(products.First()));
productRepository.Setup(x => x.FindOne(It.Is(s => !products.Select(p => p.Code).Contains(s)))).Returns(Task.FromResult(null));
}
命令的实现绝对相同。这里没有地方显示示例,但是转到GitHub上的完整源代码(这里是命令示例),您可以在其中查看所有代码,项目组织等。
总结
我强烈建议尝试使用MediatR库。它易于设置,因此使我们可以快速入门,并发现库,尤其是CQRS模式为我们提供了什么。我希望我的文字表明它可以使所有事物分离,每个类都有自己的责任,输入和输出模型很合适,控制器也尽可能整洁。
**如果我们创建不同的,单独的请求和处理程序而不是一个大接口,那么我们可以更改服务功能的任何部分而没有副作用。**我们可以轻松更改处理程序的行为(逻辑!),直到它仍返回正确类型的对象为止-不会对控制器产生影响。
我们可以通过添加新的请求处理程序对来创建新功能。或者删除其他删除它们。如果我们是长期开发的系统中的新手,那么我们只需要研究其中的一小部分,仅在需要维护的地方进行。
CQRS也可以在微服务体系结构中实现–查询命令和/或命令处理程序可以作为单独的微服务实现。我们也可以实现命令队列。可以使用不同的模型进行读写,而微服务可以使用不同的数据模型。可以通过运行不同数量的命令或查询类型的处理程序来扩展操作。
当然,CQRS并不能解决所有问题-我认为对数千个事件的定义不能使我们的系统易于维护。如果它无法应对您的开发挑战,请不要使用它。