Edument

.NET 5 Source Generator - MediatR - CQRS - OMG!

I den här bloggposten kommer vi att titta på hur vi kan använda Source Generators i .NET 5 för att automatiskt generera källkoden till ett webb API baserat på existerande klasser. Systemet är baserat på en kommando-driven arkitektur som använder MediatR-biblioteket och CQRS-mönstret.

Mediatormönstret

Mediatormönstret är ett etablerat sätt att koppla isär moduler inom en applikation. I en webbaserad applikation används det ofta till att koppla isär frontend från affärslogiken.  

På .NET-plattformen är MediatR -biblioteket en av de vanligaste implementeringarna av det här mönstret. Som bilden nedan visar agerar mediatorn som en mellanhand mellan avsändaren och mottagarna av skickade kommandon. Avsändaren vet inte vem som hanterar kommandona, och bryr sig inte om det heller.


Med MediatR implementerar vi ett kommando som en klass som i sin tur implementerar IRequest<T>-gränssnittet, där T representerar returtypen.

I det här exemplet har vi kommandot CreateUser som kommer att returnera en sträng:

public class CreateUser : IRequest<string>
    {
        public string id { get; set; }
        public string Name { get; set; }
    }   


För att skicka kommandot från ett ASP .NET Core-API till MediatR kan vi använda följande kod: 

[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);
    }
}
</string>


Även på mottagarsidan är implementationen relativt enkelt: skapa en klass som implementerar interface IRequestHandler<T,U>. I det här fallet har vi har vi en mottagare som hanterar kommandot CreateUser och returnerar en sträng:

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


Varje klass med mottagare kan hantera multipla kommandon. En bra tumregel är att du endast har en mottagare för ett specifikt kommando. Om du vill skicka ett meddelande till flera prenumeranter bör du använda den inbyggda noteringsfunktionen i MediatR, men vi kommer inte att använda den i just det här exemplet. 

CQRS

Command Query Responsibility Segregation är i grunden ett väldigt enkelt mönster. Det säger att vi bör separera implementeringen av kommandon (skrivningar) från frågor (läsningar) i vårt system. 

Med CQRS går vi från det här: 

Till det här:

CQRS associeras ofta med event sourcing, men det är inget krav att använda event sourcing för att använda CQRS, utan CQRS ger oss flera arkitektoniska fördelar helt på egen hand. Varför då? Eftersom behoven på skrivar- och frågesidorna ofta skiljer sig åt förtjänar de separata implementationer. 

Gå till vår sida CQRS.nu för att läsa mer om hur man använder CQRS med Event Sourcing. Där får du även lära dig hur man skriver vältestade system med hjälp av kommandon och händelser i stället för det klassiska CRUD-mönstret. 

 

Kombination av Mediator + CQRS

Genom att kombinera de två mönstren i test-applikationen kan vi skapa en arkitektur som ser ut så här:


Kommandon och frågor

Med MediatR har vi ingen tydlig separation mellan kommandon och frågor eftersom båda implementerar IRequest<T>-gränssnittet. För att separera dem bättre kommer vi att introducera följande interface:

public interface ICommand<T> : IRequest<T>
{
}
public interface IQuery<T> : IRequest<T>
{
}


Här är ett exempel på kommando och fråga som använder dessa två interface:

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; }
}


För att förbättra vår kod ytterligare kan vi utnyttja den nya record-funktionen i C#9. Internt är detta fortfarande en klass, men vi får en hel del färdig kod genererad åt oss, inklusive equality, GetHashCode, ToString...

Kommandon och frågor i frontend

För att faktiskt ta emot kommandon och frågor utifrån måste vi skapa ett ASP .NET Core-API. Dessa action-metoder tar inkommande HTTP-kommandon och skickar vidare dem till MediatR för vidare hantering. En controller kan se ut på följande sätt:

[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 skickar sedan vidare kommandon och frågor till de olika hanterarna som kommer att hantera dem och returnera ett svar. Genom att utnyttja CQRS-mönstret kommer vi att använda separata klasser för de olika kommando- och frågehanterarna.

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>() });
        }
    }

Source generators

Det här är en ny funktion i Roslynkompilatorn, som låter oss generera ytterligare kod som en del av kompileringen. En Source Generator är kod som vi kan stoppa in i kompilatorn och som kommer att köras som en del av kompileringen.

På en väldigt hög nivå kan man se det som följande:

  1. Först kompilerar kompilatorn din C#-kod och skapar ett syntaxträd.
  2. Därefter kan källgeneratorn undersöka syntaxträdet och generera ny C#-källkod.
  3. Denna nya källkod kompileras sedan och läggs till i det slutliga resultatet.

Tänk på att en source generator aldrig kan modifiera existerande kod, utan bara lägga till ny kod i din applikation. Det finns en rad bra videor på YouTube som visar hur man kommer igång med källgeneratorer, så det behöver vi inte gå igenom här.



Kombinera Source Generators, MediatR och CQRS

För varje kommando och fråga vi implementerar måste vi skriva en motsvarande action-metod för ASP .NET Core. 

Det innebär att om vi har 50 kommandon och frågor i vårt system, måste vi skapa 50 action-metoder. Det är så klart långdraget, upprepande och kan dessutom lätt leda till fel.

Men hade det inte varit coolt om vi kunde generera API-koden som en del av kompileringen, endast baserat på kommandot/frågan? Så här: 

Det innebär att om jag skapar följande kommandoklass:

  /// <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; }
    }


... så kommer Source generator generera följande klass åt oss som en del av kompileringen: 

  /// <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);
        }
    }
}

API-dokumentering med OpenAPI

Det fina är att Swashbuckle, som ingår i ASP.NET Core 5 API-mallen som standard, kommer att se de här klasserna och generera utmärkt OpenAPI (Swagger)-dokumentation åt oss!


Jag vill se koden!

Källkoden finns på GitHub och består av följande:


  • SourceGenerator 
    Det här projektet innehåller den faktiska källgeneratorn som kommer att skapa åtgärdsmetoderna för API-controllern.
  • SourceGenerator-MediatR-CQRS
    • En testapplikation som använder källgeneratorn. Ta en titt på projektfilen för att se hur det refererar till källgeneratorn.  
    • Templates
      • I den här foldern ligger mallen för kommandon- och frågeklasserna. Källgeneratorn lägger in den genererade koden i de här mallarna. Mallen för åtgärdsmetoderna är hårdkodad i källgeneratorn.
    • CommandAndQueries 
      • Baserat på kommandona och frågorna som definieras i den här foldern, kommer generatorn att skapa motsvarande ASP .NET-endpoints.

Visa den genererade koden

Hur kan vi då se den genererade källkoden?

Genom att lägga till följande två rader i API-projektfilen kan vi be kompilatorn skriva den genererade koden till valfri folder: 

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


Det innebär att du kan hitta den genererade koden i denna katalog:

\obj\GeneratedFiles\SourceGenerator\SourceGenerator.MySourceGenerator


I den här foldern hittar du följande två filer:

Slutsats

Detta experiement visar att vi kan ta bort en hel del av boilerplate-koden som vi hade behövt skriva och underhålla genom att använda Source Generators. Jag är ingen kompilatorutvecklare och min approach för källgeneratorn är antagligen inte 100 % optimal (eller 100 % korrekt), men den visar åtminstone att alla kan skriva sin egen källgenerator relativt enkelt. Det svåraste är debuggingen, vilket jag är säker på att de jobbar på att förbättra.   

Om författaren

När Tore Nestenius inte svarar på IdentityServer-relaterade frågor på StackOverflow arbetar han som utbildare, och undervisar ibland annat i ämnen som .NET, C#, webbsäkerhet, mjukvaruarkitektur, DDD/CQRS/Event Sourcing, IdentityServer och OpenID Connect. Utöver utbildning erbjuder han även konsulttjänster och coachning åt utvecklingsföretag världen över. 

Några av Tores senaste utbildningar: 

Några av Tores senaste blogposter

Källor





JavaScript seem to be disabled in your browser.

You must have JavaScript enabled in your browser to utilize the functionality of this website.