.NET 5 原始碼生成器——MediatR——CQRS

碼農譯站發表於2020-12-30

在這篇文章中,我們將探索如何使用.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

 

相關文章