C#, Кодогенерация и DDD Часть 3.1 — Правим подключение пакетов Nuget. Убираем рефлексию. Генерируем конечные точки MVC

от автора

Это — третья публикация в серии DDD и кодогенерация. (первая часть). В этой статье мы сгенерируем код класса для хранения всех данных запроса, код MVC контроллера.

Правильное подключение nuget пакетов

Да, отображение сгенерированных файлов в проекте еще не дает гарантии того, что эти файлы используются при сборке.

Выглядит это обычно вот так:

Файл сгенерирован, однако при сборке у partial класса нет нужных полей\методов. В файле они есть.

Файл сгенерирован, однако при сборке у partial класса нет нужных полей\методов. В файле они есть.

Проблема оказалась в подключении nuget пакетов. Не смотря на официальный cookbook, подключать nuget в кодогенерацию следует вот так:

<ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" GeneratePathProperty="true" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" GeneratePathProperty="true" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" PrivateAssets="all" GeneratePathProperty="true" />  <PackageReference Include="Newtonsoft.Json" Version="12.0.1" PrivateAssets="all" GeneratePathProperty="true" /> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />  <None Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> </ItemGroup>  <PropertyGroup> <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn> </PropertyGroup>  <Target Name="GetDependencyTargetPaths"> <ItemGroup> <!-- <TargetPathWithTargetPlatformMoniker Include="$(PKGCsvTextFieldParser)\lib\netstandard2.0\CsvTextFieldParser.dll" IncludeRuntimeDependency="false" /> <TargetPathWithTargetPlatformMoniker Include="$(PKGHandlebars_Net)\lib\netstandard2.0\Handlebars.dll" IncludeRuntimeDependency="false" />--> <TargetPathWithTargetPlatformMoniker Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" /> </ItemGroup> </Target>

Без GetTargetPathDependsOn файлы генерируются и отображаются в VisualStudio, но в компиляцию не попадают (по причине того, что генератор вдруг не отрабатывает).

Итоговый результат

Описание конечных точек:

using Domain.Common.Generation.WebApiMethod.Attributes; using Domain.Common.Generation.WebApiWithBulkInsert.Interfaces;  namespace Domain.Entities.RequestEntities.MachineOne.Alert {     [WebApiMethod(Endpoint = "/MachineOne/alert", Methods = WebApiMethodRequestTypes.Post)]     internal class MachineOneRequestAlert : IWebApiWithBulkInsert     {         [WebApiMethodParameterFromBody()]         public AlertBodyObject alert { get; set; }     } } 
using Domain.Common.Generation.WebApiMethod.Attributes; using Domain.Common.Generation.WebApiWithBulkInsert.Interfaces;  namespace Domain.Entities.RequestEntities.MachineOne.State {     [WebApiMethod(Endpoint = "/MachineOne/state", Methods = WebApiMethodRequestTypes.Get)]     internal class MachineOneRequestState : IWebApiWithBulkInsert     {         [WebApiMethodParameterFromUri(ParameterName = "stateObject")]         public StateUriObject state { get; set; }     } } 

Вот такой красивый сгенерированный код конечных точек WebApi, записывающий все в шину. C IP источника запроса и датой.

using Microsoft.AspNetCore.Mvc;  using Domain.Common.Interfaces.Infrastructure.MessageBus;  using Domain.Entities.RequestEntities.MachineOne.State; using Domain.Entities.RequestEntities.MachineOne.Alert;  namespace Infrastructure.Web.Controllers {     //[ApiController]     public partial class GeneratedWebController : ControllerBase     {                  [HttpGet("/MachineOne/state")]         public IActionResult GetMachineOneRequestState([FromServices] ILogger logger,[FromServices] IMessageBus busService, [FromQuery]StateUriObject stateObject)         {             try             {                                  GeneratedMachineOneRequestStateRequestObject request = new GeneratedMachineOneRequestStateRequestObject()                 {                     stateObject = stateObject,                      SourceIp = Request.HttpContext.Connection.RemoteIpAddress.ToString(),                     Date = DateTime.Now                 };                                            busService.Send(request, MessageBus.WebApiBulk).Wait();             }             catch (Exception ex)             {                 logger.LogError(ex, "WebAPIWithBulkInsert", Request);             }                         return Ok();         }          [HttpPost("/MachineOne/alert")]         public IActionResult GetMachineOneRequestAlert([FromServices] ILogger logger,[FromServices] IMessageBus busService, [FromBody]AlertBodyObject alert)         {             try             {                                  GeneratedMachineOneRequestAlertRequestObject request = new GeneratedMachineOneRequestAlertRequestObject()                 {                     alert = alert,                      SourceIp = Request.HttpContext.Connection.RemoteIpAddress.ToString(),                     Date = DateTime.Now                 };                                            busService.Send(request, MessageBus.WebApiBulk).Wait();             }             catch (Exception ex)             {                 logger.LogError(ex, "WebAPIWithBulkInsert", Request);             }                         return Ok();         }      } } 

А так же классы, сохраняющие информацию о запросах. Эти классы мы складываем в шину, читаем из шины и ложем в БД.

using System;  using Domain.Entities.RequestEntities.MachineOne.State; using Domain.Entities.RequestEntities.MachineOne.Alert;  namespace Infrastructure.Web.Controllers {          public class GeneratedMachineOneRequestStateRequestObject     {                                public StateUriObject stateObject {get;set;}               public DateTime Date {get;set;}          public string SourceIp {get;set;}     }       public class GeneratedMachineOneRequestAlertRequestObject     {                  public AlertBodyObject alert {get;set;}                             public DateTime Date {get;set;}          public string SourceIp {get;set;}     }   } 

Генерация

Из общих рекомендаций следует отметить:

1) Не пишите все в 1 методе — да, это написано у Стива Макконнелла. Разбивайте генерацию методов, метода, параметров, присвоение.

2) Используйте интерполируемые строки — да, строки в стиле

@$" using Microsoft.AspNetCore.Mvc;  using Domain.Common.Interfaces.Infrastructure.MessageBus; {GenerateUsings(data)}  namespace Infrastructure.Web.Controllers {{     //[ApiController]     public partial class GeneratedWebController : ControllerBase     {{         {GenerateMethods(data)}     }} }} "

читается очень легко и просто. Не забывайте про отступы.

3) Добавляйте префикс Generated — да, мы сделали отличный проект, где информация из доменных сборок доступна всюду, а генераторы запускаются только в нужных местах. Однако у нас уже есть public DTO, и их будет много. Да и простых Internal классов будет тоже много. Чтобы отгородить эту кучу классов следует добавлять префикс Generated.

4) Работа со строками предпочтительна — это гораздо проще, чем делать код как-либо иначе. И переводить готовые решения тоже гораздо проще используя строки. Даже если вы будете использовать что-то для генерации кода — помните, что написав 500 строк и сделав нормальный класс в нормальном namespace с нормальным методом придется писать еще тело метода. И отлаживать это все.

Код сканера:
using CodeGen.GeneratorBase; using CodeGen.GeneratorBase.Context; using CodeGen.GeneratorBase.FileManager; using CodeGen.Utils.Scan; using Domain.Common.Generation.WebApiMethod.Attributes; using Domain.Common.Generation.WebApiWithBulkInsert.Interfaces; using System.Collections.Generic; using System.Linq;  namespace CodeGen.Generators.WebApiWithBulkInsert {     internal class RequestEntityScanner : ICodeGeneratorScanner<RequestEntityGeneratorDTO>     {         public RequestEntityScanner()         {         }          public GeneratedFileInfo GetDescription(RequestEntityGeneratorDTO data)         {             throw new System.NotImplementedException();         }          public List<RequestEntityGeneratorDTO> Scan(GenerationContext projectContext)         {             var result = new List<RequestEntityGeneratorDTO>();              //Получаем все типы с интерфейсом IRequestEntity             var items = projectContext.GetAllClassesWithInterface<IWebApiWithBulkInsert>();              foreach (var item in items)             {                 //Получаем атрибут с указанием Endpoint-а и Http метода                 WebApiMethodAttribute requestAttr = item.GetAttribute<WebApiMethodAttribute>();                  //Получаем все параметры приходящие в запросе                 var uriParamsRaw = item.Properties.Where(i=>i.GetAttribute<WebApiMethodParameterFromUriAttribute>()!=null).ToList();                 var bodyParamsRaw = item.Properties.Where(i => i.GetAttribute<WebApiMethodParameterFromBody>() != null).ToList();                                   //Добавляем все в DTO                 result.Add(new RequestEntityGeneratorDTO()                 {                     defaultPath = requestAttr.Endpoint,                     methods = requestAttr?.Methods ?? WebApiMethodRequestTypes.Get,                      //requestEntityType = item,                     bodyParam = bodyParamsRaw.Select(i => new RequestEntityParam()                     {                         Name = i.Name,                         UriNameParameter = i.Name,                         Parameter = i.Type                     })                     .ToList(),                      uriParameters = uriParamsRaw.Select(i => new RequestEntityParam()                     {                         Name = i.Name,                         UriNameParameter = i.GetAttribute<WebApiMethodParameterFromUriAttribute>().ParameterName,                         Parameter = i.Type                     })                     .ToList(),                      requestEntityType = item                 });             }              return result;         }     } } 
Код генератора WebApi:
using CodeGen.GeneratorBase; using CodeGen.GeneratorBase.Context; using CodeGen.Utils.Scan; using CodeGeneration.GeneratorBase; using Domain.Common.Generation.WebApiMethod.Attributes; using System.Collections.Generic;  namespace CodeGen.Generators.WebApiWithBulkInsert.Infrastructure.Web {     class RequestEntityWebGenerator : CodeGeneratorBase<RequestEntityGeneratorDTO>     {         private ICodeGeneratorScanner<RequestEntityGeneratorDTO> scanner;         public RequestEntityWebGenerator()          {             place = GeneratorRunPlace.InfrastructureWeb;             scanner = new RequestEntityScanner();         }                  public override void Generate(GenerationContext context, List<RequestEntityGeneratorDTO> data)         {             //Добавляем шапку             string txtExample = $@" using Microsoft.AspNetCore.Mvc;  using Domain.Common.Interfaces.Infrastructure.MessageBus; {GenerateUsings(data)}  namespace Infrastructure.Web.Controllers {{     //[ApiController]     public partial class GeneratedWebController : ControllerBase     {{         {GenerateMethods(data)}     }} }} ";              context.AddSource("Generated_WebApiWithBulkInsert_WebControllers", txtExample);         }          //Генерируем методы WebAPI         private string GenerateMethods(List<RequestEntityGeneratorDTO> data)         {             var txtExample = "";              foreach (var item in data)             {                 txtExample += GenerateMethod(item)+"\r\n";             }              return txtExample;         }          //Генерируем метод WebApi         private string GenerateMethod(RequestEntityGeneratorDTO item)         {             var txtExample = "";              //Добавляем атрибут к методу             if (item.methods == WebApiMethodRequestTypes.Get)                 txtExample += $@"         [HttpGet(""{item.defaultPath}"")]";             else if (item.methods == WebApiMethodRequestTypes.Post)                 txtExample += $@"         [HttpPost(""{item.defaultPath}"")]";              //Делаем метод             txtExample += $@"         public IActionResult Get{item.requestEntityType.Name}([FromServices] ILogger logger,[FromServices] IMessageBus busService, {GenerateParameters(item)})         {{             try             {{                 {GenerateBody(item)}                  busService.Send(request, MessageBus.WebApiBulk).Wait();             }}             catch (Exception ex)             {{                 logger.LogError(ex, ""WebAPiWithBulkInsert"", Request);             }}                         return Ok();         }}";              return txtExample;         }          //Добавляем using-и         private string GenerateUsings(List<RequestEntityGeneratorDTO> data)         {             var txtExample = "";             foreach (var item in data)             {                 foreach (var uri in item.uriParameters)                     txtExample += $"\r\nusing {uri.Parameter.Namespace};";                  foreach (var body in item.bodyParam)                     txtExample += $"\r\nusing {body.Parameter.Namespace};";              }                          return txtExample;         }          //Генерируем тело метода         private object GenerateBody(RequestEntityGeneratorDTO data)         {             return $@"                 Generated{data.requestEntityType.Name}RequestObject request = new Generated{data.requestEntityType.Name}RequestObject()                 {{                     {GenerateAssigns(data)}                     SourceIp = Request.HttpContext.Connection.RemoteIpAddress.ToString(),                     Date = DateTime.Now                 }};                          ";         }          //Генерируем присваивание         private string GenerateAssigns(RequestEntityGeneratorDTO data)         {             string result = "";              foreach (var item in data.uriParameters)             {                 result += $"{item.UriNameParameter} = {item.UriNameParameter},\r\n";             }              foreach (var item in data.bodyParam)             {                 result += $"{item.UriNameParameter} = {item.UriNameParameter},\r\n";             }              return result;         }          //Генерируем строку параметров         private object GenerateParameters(RequestEntityGeneratorDTO data)         {             var result = "";             foreach(var item in data.uriParameters)             {                 result += "[FromQuery]";                 result += item.Parameter.Name;                 result += " " + item.UriNameParameter + ", ";             }              foreach (var item in data.bodyParam)             {                 result += "[FromBody]";                 result += item.Parameter.Name;                 result += " " + item.UriNameParameter + ", ";             }              return result.Substring(0, result.Length - 2);         }          public override List<RequestEntityGeneratorDTO> Parse(GenerationContext projectContext)         {             return scanner.Scan(projectContext);         }     } } 
И код генератора DTO для сохранения данных запроса:
using CodeGen.GeneratorBase; using CodeGen.GeneratorBase.Context; using CodeGen.Utils.Scan; using CodeGeneration.GeneratorBase; using System.Collections.Generic;  namespace CodeGen.Generators.WebApiWithBulkInsert.Application.Common {     class RequestEntityObjectWebGenerator : CodeGeneratorBase<RequestEntityGeneratorDTO>     {         private ICodeGeneratorScanner<RequestEntityGeneratorDTO> scanner;         public RequestEntityObjectWebGenerator()          {             place = GeneratorRunPlace.ApplicationCommon;             scanner = new RequestEntityScanner();         }                  public override void Generate(GenerationContext context, List<RequestEntityGeneratorDTO> data)         {             //Добавляем шапку             string txtExample = $@" using System; {GenerateUsings(data)}  namespace Infrastructure.Web.Controllers {{     {GenerateClasses(data)} }} ";              context.AddSource("Generated_WebApiWithBulkInsert_RequestDTOs", txtExample);         }          private string GenerateClasses(List<RequestEntityGeneratorDTO> data)         {             var txtExample = "";              foreach (var item in data)             {                 txtExample += GenerateClass(item)+"\r\n";             }              return txtExample;         }          private string GenerateClass(RequestEntityGeneratorDTO item)         {             var txtExample = $@"     public class Generated{item.requestEntityType.Name}RequestObject     {{         {GenerateProperties(item)}              {GenerateFields(item)}              public DateTime Date {{get;set;}}          public string SourceIp {{get;set;}}     }} ";             return txtExample;         }          private object GenerateFields(RequestEntityGeneratorDTO data)         {             var result = "";              foreach (var item in data.uriParameters)             {                 result += @"         public ";                 result += item.Parameter.Name;                 result += " " + item.UriNameParameter + " {get;set;}\r\n";             }              return result;         }          private object GenerateProperties(RequestEntityGeneratorDTO data)         {             var result = "";              foreach (var item in data.bodyParam)             {                 result += @"         public ";                 result += item.Parameter.Name;                 result += " " + item.UriNameParameter + " {get;set;}\r\n";             }              return result;         }          private string GenerateUsings(List<RequestEntityGeneratorDTO> data)         {             var txtExample = "";             foreach (var item in data)             {                 foreach (var uri in item.uriParameters)                     txtExample += $"\r\nusing {uri.Parameter.Namespace};";                  foreach (var body in item.bodyParam)                     txtExample += $"\r\nusing {body.Parameter.Namespace};";              }                          return txtExample;         }          public override List<RequestEntityGeneratorDTO> Parse(GenerationContext projectContext)         {             return scanner.Scan(projectContext);         }     } } 

Что добавили еще

Код DTO для работы в Application (запись и чтение из шины) вынесен в Application.Common.

Так же добавлен интерфейс для работы с шинами (и проект Infrastructure.MessageBus).

А что с рефлексией?

По совету@onets(https://habr.com/ru/articles/542300/) убрал получение данных через рефлексию. Оказалось информацию о типах (с атрибутами и методами) можно получать через глобальный Namespace компиляции. Даже тех типов, которые не public. Обертки вокруг информации о типах оставил, т.к. гораздо удобнее и читабельней.

Так что теперь даже @IvanGдолжен быть доволен. (И, да, у нас все генераторы для всего Solution в одном проекте).

Так же подобная работа со сборками уже позволяет реализовать генераторы, создающие описания для других генераторов. (См. часть 2 — некоторые генераторы можно представить как генераторы описаний для других генераторов. И убрать дублирование кода)

Итог

Наши конечные точки генерируются и их видно в Swagger

Swagger

Swagger

Вызвать Api еще нельзя — мы не сделали работу с шинами и Worker-ы, поэтому все запросы будут падать из-за отсутствующих сервисов.

Однако у нас уже есть база даже для создания CRUD по атрибутам в БД. И даже база для создания CRUD через CQRS :).

Даже не смотря на то, что основной код еще не написан, у нас уже генерируется в 10 раз больше кода, чем пишется (20 строк при задании точек WebApi против 200 в контроллере\DTO).

Файл проекта можно взять тут

В следующей части мы разберемся с шинами и воркерами (и их регистрации в контейнере).


ссылка на оригинал статьи https://habr.com/ru/articles/918236/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *