
Это - третья публикация в серии 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).
В следующей части мы разберемся с шинами и воркерами (и их регистрации в контейнере).
Комментарии (4)
IvanG
13.06.2025 17:21Ничего себе в статье тэгнут, но кстати это никак не уведомляется на Хабре, и контекста читателям не даёт, почему кого-то это касается, лучше дать ссылку на коммент.
Надо будет пример покрутить на досуге, сейчас проблем решаемых генерацией уже нет, но для развития на будущее полезно.
ValeriyPus Автор
13.06.2025 17:21Бойлерплейт же - основная проблема, решаемая генерацией.
А все, что не объекты и флоу - boilerplate (увы).
AlexCatLeva
Ddd, это когда у разработчиков множество свободного времени, а у компании много деняк
ValeriyPus Автор
Да, без DDD бизнес-логика размазывается, появляется дублирование кода, а при поддержке все эти копипасты еще получают различное поведение.