
Это - третья публикация в серии DDD и кодогенерация. (первая часть, вторая часть, третья часть, четвертая часть).
В этой статье мы сгенерируем код класса для хранения всех данных запроса, код MVC контроллера.
Правильное подключение nuget пакетов
Да, отображение сгенерированных файлов в проекте еще не дает гарантии того, что эти файлы используются при сборке.
Выглядит это обычно вот так:

Проблема оказалась в подключении 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(https://habr.com/ru/articles/906778/comments/#comment_28260784) должен быть доволен. (И, да, у нас все генераторы для всего Solution в одном проекте).
Так же подобная работа со сборками уже позволяет реализовать генераторы, создающие описания для других генераторов. (См. часть 2 - некоторые генераторы можно представить как генераторы описаний для других генераторов. И убрать дублирование кода)
Итог
Наши конечные точки генерируются и их видно в Swagger

Вызвать Api еще нельзя - мы не сделали работу с шинами и Worker-ы, поэтому все запросы будут падать из-за отсутствующих сервисов.
Однако у нас уже есть база даже для создания CRUD по атрибутам в БД. И даже база для создания CRUD через CQRS :).
Даже не смотря на то, что основной код еще не написан, у нас уже генерируется в 10 раз больше кода, чем пишется (20 строк при задании точек WebApi против 200 в контроллере\DTO).
В следующей части мы разберемся с шинами и воркерами (и их регистрации в контейнере).