在这篇文章中,咱们将探索如何使用.NET 5中的新source generator特性,使用MediatR库和CQRS模式自动为系统生成API。前端
中介者模式
中介模式是在应用程序中解耦模块的一种方式。在基于web的应用程序中,它一般用于将前端与业务逻辑的解耦。web
在.NET平台上,MediatR库是该模式最流行的实现之一。以下图所示,中介器充当所发送命令的发送方和接收方之间的中间人。发送者不知道也不关心谁在处理命令。json
使用MediatR,咱们定义了一个command,它实现IRequest<T>接口,其中T表示返回类型。在这个例子中,咱们有一个CreateUser类,它将返回一个字符串给调用者:api
public class CreateUser : IRequest<string> { public string id { get; set; } public string Name { get; set; } }
从ASP.NET Core API发送命令到MediatR,咱们可使用如下代码:架构
[Route("api/[controller]")] [ApiController] public class CommandController : ControllerBase { private readonly IMediator _mediator; public CommandController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task<string> Get(CreateUser command) { return await _mediator.Send(command); } }
在接收端,实现也很是简单:建立一个实现IRequestHandler<T,U>接口的类。在本例中,咱们有一个处理程序,它处理CreateUser并向调用者返回一个字符串:app
public class CommandHandlers : IRequestHandler<CreateUser, string=""> { public Task<string> Handle(CreateUser request, CancellationToken cancellationToken) { return Task.FromResult("User Created"); } }
每一个处理程序类能够处理多个命令。处理规则是对于一个特定的命令,应该老是只有一个处理程序。若是但愿将消息发送给许多订阅者,则应该使用MediatR中的内置通知功能,但在本例中咱们将不使用该功能。async
CQRS
Command Query Responsibility Segregation(CQRS)是一个很是简单的模式。它要求咱们应该将系统中的命令(写)的实现与查询(读)分离开来。ui
有了CQRS,咱们会从这样作:this
改成这样作:spa
CQRS一般与event sourcing相关联,可是使用CQRS并不须要使用event sourcing,而仅仅使用CQRS自己就会给咱们带来不少架构上的优点。这是为何呢?由于读写的需求一般是不一样的,因此它们须要单独的实现。
Mediator + CQRS
在示例应用程序中结合这两种模式,咱们能够建立以下的架构:
Command和Query
使用MediatR,Command和Query之间没有明显的分离,由于二者都将实现IRequest<T>接口。为了更好地分离它们,咱们将引入如下接口:
public interface ICommand<T> : IRequest<T> {} public interface IQuery<T> : IRequest<T> {}
下面是使用这两个接口的示例:
public record CreateOrder : ICommand<string> { public int Id { get; set; } public int CustomerId { get; set; } } public record GetOrder : IQuery<order> { public int OrderId { get; set; } }
为了进一步改进咱们的代码,咱们可使用新的C# 9 record特性。在内部,它仍然是一个类,可是咱们为咱们生成了不少样板代码,包括equality, GetHashCode, ToString……
前端Command和Query
要真正从外部接收Command和Query,咱们须要建立一个ASP.NET Core API。这些action方法将接收传入的HTTP命令,并将它们传递给MediatR以进行进一步处理。控制器多是这样的:
[Route("api/[controller]")] [ApiController] public class CommandController : ControllerBase { private readonly IMediator _mediator; public CommandController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task<string> CreateOrder([FromBody] CreateOrder command) { return await _mediator.Send(command); } [HttpPost] public async Task<order> GetOrder([FromBody] GetOrder command) { return await _mediator.Send(command); } }
而后,MediatR将把Command和Query传递给各类处理程序,这些处理程序将处理它们并返回响应。应用CQRS模式,咱们将为Command和Query处理程序使用单独的类。
public class CommandHandlers : IRequestHandler<CreateOrder, string=""> { public Task<string> Handle(CreateOrder request, CancellationToken ct) { return Task.FromResult("Order created"); } } public class QueryHandlers : IRequestHandler<GetOrder, Order=""> { public Task<Order> Handle(GetOrder request, CancellationToken ct) { return Task.FromResult(new Order() { Id = 2201, CustomerId = 1234, OrderTotal = 9.95m, OrderLines = new List<OrderLine>() }); } }
源代码生成器
这是Roslyn编译器中的一个新特性,它容许咱们hook到编译器,并在编译过程当中生成额外的代码。
在一个很是高的层次上,你能够看到它以下:
重要的是要知道源代码生成器永远不能修改现有的代码,它只能向应用程序添加新代码。
源代码生成器+MediatR+CQRS
对于咱们实现的每一个Command和Query,咱们必须编写相应的ASP.NET Core action方法。
这意味着若是咱们的系统中有50个Command和Query,咱们须要建立50个action方法。固然,这将是至关乏味的、重复的和容易出错的。
可是,若是仅仅基于Command/Query,咱们就能够生成API代码做为编译的一部分,这不是很酷吗?就像这样:
意思是,若是我建立这个Command类:
/// <summary> /// Create a new order /// </summary> /// <remarks> /// Send this command to create a new order in the system for a given customer /// </remarks> public record CreateOrder : ICommand<string> { /// <summary> /// OrderId /// </summary> /// <remarks>This is the customers internal ID of the order.</remarks> /// <example>123</example> [Required] public int Id { get; set; } /// <summary> /// CustomerID /// </summary> /// <example>1234</example> [Required] public int CustomerId { get; set; } }
而后,源生成器将生成如下类,做为编译的一部分:
/// <summary> /// This is the controller for all the commands in the system /// </summary> [Route("api/[controller]/[Action]")] [ApiController] public class CommandController : ControllerBase { private readonly IMediator _mediator; public CommandController(IMediator mediator) { _mediator = mediator; } /// <summary> /// Create a new order /// </summary> /// <remarks> /// Send this command to create a new order in the system for a given customer /// </remarks> /// <param name="command">An instance of the CreateOrder /// <returns>The status of the operation</returns> /// <response code="201">Returns the newly created item</response> /// <response code="400">If the item is null</response> [HttpPost] [Produces("application/json")] [ProducesResponseType(typeof(string), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task<string> CreateOrder([FromBody] CreateOrder command) { return await _mediator.Send(command); } }
使用OpenAPI生成API文档
幸运的是是Swashbuckle包含在ASP.NET Core 5的API模板默认状况下,会看到这些类并为咱们生成漂亮的OpenAPI (Swagger)文档!
看看个人代码
他是这样组成的:
这个项目包含实际的源生成器,它将生成API控制器action方法。
查看生成的代码
咱们如何看到生成的源代码?经过将这些行添加到API项目文件中,咱们能够告诉编译器将生成的源代码写到咱们选择的文件夹中:
<EmitCompilerGeneratedFiles> True </EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath> $(BaseIntermediateOutputPath)GeneratedFiles </CompilerGeneratedFilesOutputPath>
这意味着你能够在这个目录中找到生成的代码:
objGeneratedFilesSourceGeneratorSourceGenerator.MySourceGenerator
在这个文件夹里你会找到如下两个文件:
结论
经过引入源代码生成器的概念,咱们能够删除大量必须编写和维护的样板代码。我不是编译器工程师,我在源代码生成器方面的方法可能不是100%最优的(甚至不是100%正确的),但它仍然代表任何人均可以建立本身的源代码生成器,而没有太多麻烦。
欢迎关注个人公众号,若是你有喜欢的外文技术文章,能够经过公众号留言推荐给我。