在这篇文章中,我们将探索如何使用.NET 5中的新source generator特性,使用MediatR库和CQRS模式自动为系统生成API。

中介者模式

中介模式是在应用程序中解耦模块的一种方式。在基于web的应用程序中,它通常用于将前端与业务逻辑的解耦。

在.NET平台上,MediatR库是该模式最流行的实现之一。如下图所示,中介器充当所发送命令的发送方和接收方之间的中间人。发送者不知道也不关心谁在处理命令。

使用MediatR,我们定义了一个command,它实现IRequest<T>接口,其中T表示返回类型。在这个例子中,我们有一个CreateUser类,它将返回一个字符串给调用者:

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并向调用者返回一个字符串:

public class CommandHandlers : IRequestHandler<CreateUser, string="">
{
public Task<string> Handle(CreateUser request,
CancellationToken cancellationToken)
{
return Task.FromResult("User Created");
}
}

每个处理程序类可以处理多个命令。处理规则是对于一个特定的命令,应该总是只有一个处理程序。如果希望将消息发送给许多订阅者,则应该使用MediatR中的内置通知功能,但在本例中我们将不使用该功能。

CQRS

Command Query Responsibility Segregation(CQRS)是一个非常简单的模式。它要求我们应该将系统中的命令(写)的实现与查询(读)分离开来。

有了CQRS,我们会从这样做:

改为这样做:

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到编译器,并在编译过程中生成额外的代码。

在一个非常高的层次上,你可以看到它如下:

  1. 首先,编译器编译你的C#源代码并生成语法树。
  2. 然后,源代码生成器可以检查这个语法树并生成新的C#源代码。
  3. 然后,这个新的源代码被编译并添加到最终的输出中。

重要的是要知道源代码生成器永远不能修改现有的代码,它只能向应用程序添加新代码。

源代码生成器+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)文档!

看看我的代码

他是这样组成的:

  • SourceGenerator

这个项目包含实际的源生成器,它将生成API控制器action方法。

  • SourceGenerator-MediatR-CQRS
  1. 这是一个使用源代码生成器的示例应用程序。查看项目文件,以了解该项目如何引用源生成器。
  2. Templates这个文件夹包含Command和Query类的模板。源代码生成器将把生成的代码插入到这些模板中。
  3. CommandAndQueries基于此文件夹中定义的Command和Query,生成器将生成相应的ASP.NET终结点。

查看生成的代码

我们如何看到生成的源代码?通过将这些行添加到API项目文件中,我们可以告诉编译器将生成的源代码写到我们选择的文件夹中:

<EmitCompilerGeneratedFiles>
True
</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>
$(BaseIntermediateOutputPath)\GeneratedFiles
</CompilerGeneratedFilesOutputPath>

这意味着你可以在这个目录中找到生成的代码:

\obj\GeneratedFiles\SourceGenerator\SourceGenerator.MySourceGenerator

在这个文件夹里你会找到以下两个文件:

结论

通过引入源代码生成器的概念,我们可以删除大量必须编写和维护的样板代码。我不是编译器工程师,我在源代码生成器方面的方法可能不是100%最优的(甚至不是100%正确的),但它仍然表明任何人都可以创建自己的源代码生成器,而没有太多麻烦。

欢迎关注我的公众号,如果你有喜欢的外文技术文章,可以通过公众号留言推荐给我。

原文链接:https://www.edument.se/en/blog/post/net-5-source-generators-mediatr-cqrs

最新文章

  1. 编译原理-词法分析05-正则表达式到DFA-01
  2. BiliBili 第三方 Android 客户端应用源码
  3. dedecms /include/filter.inc.php Local Variable Overriding
  4. [转] Memcached参数设置 命令
  5. iPad apple-touch-startup-image实现portrait和landscape
  6. C#中dategridview数据导出为excel文件
  7. Xampp 添加 SSL
  8. Oracle bbed 实用示例-----修改Data内容、恢复delete的rows
  9. Android:从程序员到架构师之路Ⅰ
  10. vsvim _vsvimrc 设置(转)
  11. js 切换图片
  12. 10大支持移动“触摸操作”的JavaScript框架
  13. [蓝桥杯]2015蓝桥省赛B组题目及详解
  14. django信号浅谈
  15. jQuery实现回车绑定Tab事件
  16. Spark中master与worker的进程RPC通信实现
  17. Alpha冲刺随笔五:第五天
  18. linux Tar 命令参数详解
  19. svn更新产生的异常
  20. 删除k8s中一直处于Terminating的资源

热门文章

  1. 2. 三数之和(数组、hashset)
  2. CenOS下监控工具尝试
  3. 「刷题笔记」AC自动机
  4. python 爬取喜马拉雅节目生成RSS Feed
  5. Spring Boot 中使用 Spring Security, OAuth2 跨域问题 (自己挖的坑)
  6. html基础标签用法
  7. sitespeedio前端性能测试工具介绍
  8. Python函数的关键字参数
  9. jenkins+git部署环境,出现Failed to connect to repository : Command &quot;git ls-remote -h http://gitlab.xxxxx.git HEAD&quot; returned status code 128stdout: stderr: fatal: repository &#39;http://gitlab.xxxxx.git&#39; not fou
  10. pandas LabelEncoder 测试集出现了训练集中未出现过的值怎么解决(y contains previously unseen labels 解决方法)