
Это - вторая публикация в серии DDD и кодогенерация. (первая часть, вторая часть, третья часть, четвертая часть)
В этой части мы научимся получать данные через рефлексию и Roslyn в одинаковой форме. Даже типизированные атрибуты как
var attribute = em.GetAttribute<WebApiMethodAttribute>();
Так же мы опишем конечные точки WebApi при помощи классов, и сделаем генерацию Mock-ов на паре уровней.
Утилиты получения информации
Посмотрим на наши абстракции. Всего у нас есть 3 основных обьекта:
Атрибуты
using System.Collections.Generic; namespace CodeGen.Utils.Scan.Data.ClassInfo { /// <summary> /// Информация об атрибуте /// </summary> internal interface IAttributeInfo { /// <summary> /// Тип атрибута /// </summary> IClassInfo AttributeType { get; } /// <summary> /// Аргументы атрибута /// </summary> List<(IClassInfo, TypedArgument)> ConstructorArguments { get; } /// <summary> /// Именованные аргументы атрибута /// </summary> List<(string, TypedArgument)> NamedArguments { get; } /// <summary> /// Конвертирует информацию об аттрибуте в типизированный обьект /// </summary> /// <typeparam name="T">Тип аттрибута</typeparam> /// <returns>Типизированный обьект</returns> T getAsTypedAttribute<T>() where T : class; } }
Типы:
using System.Collections.Generic; namespace CodeGen.Utils.Scan.Data.ClassInfo { /// <summary> /// Представляет информацию о классе /// </summary> internal interface IClassInfo : IItemWithAttributes { /// <summary> /// Имя класса /// </summary> string Name { get; } /// <summary> /// Неймспейс класса /// </summary> string Namespace { get; } /// <summary> /// Все NameSpace обьектов, используемых в классе /// </summary> List<string> Namespaces { get; } /// <summary> /// Публичные поля класса /// </summary> List<IPropertyInfo> Properties { get; } /// <summary> /// Атрибуты класса /// </summary> List<IAttributeInfo> Attributes { get; } } }
Свойства классов:
using System.Collections.Generic; namespace CodeGen.Utils.Scan.Data.ClassInfo { /// <summary> /// Описание свойства /// </summary> internal interface IPropertyInfo : IItemWithAttributes { /// <summary> /// Имя свойства /// </summary> string Name { get; } /// <summary> /// Тип свойства /// </summary> IClassInfo Type { get; } /// <summary> /// Аттрибуты свойства /// </summary> List<IAttributeInfo> Attributes { get; } } }
Типы и свойства типов обладают атрибутами, и реализуют соответствующий интерфейс:
Описание сущности с атрибутами:
using System; using System.Collections.Generic; namespace CodeGen.Utils.Scan.Data.ClassInfo { /// <summary> /// Сущность, именющая аттрибуты /// </summary> internal interface IItemWithAttributes { /// <summary> /// Получает атрибут заданного типа /// </summary> /// <typeparam name="T">Тип атрибута</typeparam> /// <returns>Атрибут заданного типа или null</returns> T GetAttribute<T>() where T : Attribute; /// <summary> /// Получает аттрибуты заданного типа /// </summary> /// <typeparam name="T">Тип атрибута</typeparam> /// <returns>Атрибуты заданного типа или null</returns> List<T> GetAttributes<T>() where T : Attribute; } }
Данные о классах с атрибутами или классах, реализующих интерфейс мы получаем из контекста генерации через Extension-методы. Например, вот так:
public List<RequestEntityGeneratorDTO> Scan(GenerationContext projectContext) { var result = new List<RequestEntityGeneratorDTO>(); //Получаем все типы с интерфейсом IRequestEntity var items = projectContext.GetAllClassesWithInterface<IWebApiMethod>();
Как правильно работать с атрибутами
В System.Reflection и Roslyn атрибуты можно получить как список именованных полей и список параметров конструктора. Но всегда удобнее работать с атрибутом как с объектом (а не списком полей (string Name, object Value)).
Мы сделаем наши атрибуты без конструкторов. В Reflection можно получить типизированный атрибут (прямо объект, с заполненными полями). А в Roslyn — нельзя.
Чтобы это исправить, можно просто сделать ExpandoObject и привести его к типу атрибута.
public T getAsTypedAttribute<T>() where T : class { ICollection<KeyValuePair<string, object>> attr = new System.Dynamic.ExpandoObject(); foreach (var item in NamedArguments) { attr.Add(new KeyValuePair<string, object>(item.Item1, item.Item2.Value)); } T result = JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(attr)); return result; }
Этот подход, не смотря на кажущуюся «кастыльность» вполне справляется с задачей.
Архитектура генератора
Но прежде всего поговорим об архитектурных изменениях в нашем генераторе.
Я добавил сканнер. Так же вся работа по добавлению файлов идет через отдельный сервис.
using CodeGen.GeneratorBase.Context; using CodeGen.GeneratorBase.FileManager; using System.Collections.Generic; namespace CodeGen.GeneratorBase { /// <summary> /// Выполняет сканирование всех сборок и возвращает собранную информацию /// </summary> /// <typeparam name="T">Тип с собранной информацией</typeparam> internal interface ICodeGeneratorScanner<T> { /// <summary> /// Сканирует доменные сборки и контекст исполнения на предмет наличия описаний для генератора /// </summary> /// <param name="context">Контекст кодогенерации</param> /// <returns>Список сконвертированных описаний</returns> List<T> Scan(GenerationContext context); /// <summary> /// Конвертирует указанный обьект в файл описания /// </summary> /// <param name="data">Обьект, описывающий генерацию чего-либо</param> /// <returns>Файл, при сканировании которого можно получить тот же обьект</returns> GeneratedFileInfo GetDescription(T data); } }
Зачем? Давайте посмотрим на текущую архитектуру нашего генератора. Чтобы не выстрелить себе в ногу, начнем с простого.

Как видим, все наполнение слоев Application/UseCases и Infrastructure делается на основе одного и того же объекта.
Логично, что все 3 генератора (Генератор Action/UseCase, Генератор Job и генератор WebApi) будут использовать один и тот же сканер контекста генерации.
Так же я заранее добавил возможность конвертации объектов в файлы с классами, из которых мы получаем эти объекты. И вынес добавление файлов в отдельный сервис.
Т.е. я могу создать какое-то описание программно, и сохранить его в файл.
Далее, на основе этого файла запустить генератор ).
Это позволяет практически полностью исключить дублирование кода даже в написании генераторов.
Давайте посчитаем.
Нам на наше API нужно будет написать 3 генератора описания (генератор описания Job-а, генератор описания WebApi, генератор описания Action).
И написать 3 универсальных генератора (генератор WebApi, генератор Job-а и генератор Action).
Но этот подход требует тщательной аналитики (а что у нас есть кроме WebApi, а Job у нас один, или есть MessageBusReadJob, и т.д.).
Поэтому мы остановимся на простейшем варианте. А именно — напишем 3 генератора и 1 сканер. Этого более чем достаточно для демонстрации мощи и силы кодогенерации в DDD.

Опишем конечные точки
Конечные точки WebApi можно описать всего 3 атрибутами и 1 интерфейсом. Давайте попробуем описать 1 конечную точку:
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; } } }
Вполне понятно, что этот класс описывает WebApi метод
IWebApiWithBulkInsert
типа Methods = WebApiMethodRequestTypes.Post
который находится по адресу Endpoint = "/MachineOne/alert"
и принимает объект AlertBodyObject в Body. [WebApiMethodParameterFromBody()]
Опишем этот объект в виде DTO:
using CodeGen.Utils.Scan.Data.ClassInfo; using Domain.Common.Generation.WebApiMethod.Attributes; using System; using System.Collections.Generic; namespace CodeGen.Generators.RequestEntity { /// <summary> /// Данные для кодогенерации контроллера /// </summary> class RequestEntityGeneratorDTO { /// <summary> /// Список параметров в URI /// </summary> public List<RequestEntityParam> uriParameters = new List<RequestEntityParam>(); /// <summary> /// Список параметров в теле запроса /// </summary> public List<RequestEntityParam> bodyParam = new List<RequestEntityParam>(); /// <summary> /// Методы доступа /// </summary> public WebApiMethodRequestTypes methods { get; set; } /// <summary> /// Путь по-умолчанию для контроллера /// </summary> public string defaultPath { get; set; } public IClassInfo requestEntityType { get; set; } } }
И опишем RequestEntityParam:
using CodeGen.Utils.Scan.Data.ClassInfo; namespace CodeGen.Generators.RequestEntity { internal class RequestEntityParam { /// <summary> /// Имя параметра в объекте /// </summary> public string Name { get; set; } /// <summary> /// Имя параметра в URI /// </summary> public string UriNameParameter { get; set; } /// <summary> /// Тип параметра /// </summary> public IClassInfo Parameter { get; set; } } }
Т. к. это наш IClassInfo — мы легко и просто получим и имя типа, и Namespace для генерации using-а.
Т. к. это наш IClassInfo — мы легко и просто получим и имя типа, и Namespace для генерации using-а.
Сделаем сканер
На получившихся утилитах сканер прост и незатейлив, и вряд-ли нуждается в комментариях:
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.WebApiMethod.Interfaces; using System.Collections.Generic; using System.Linq; namespace CodeGen.Generators.RequestEntity { 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<IWebApiMethod>(); 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, //Http методы methods = requestAttr?.Methods ?? WebApiMethodRequestTypes.Get, //Параметры в теле bodyParam = bodyParamsRaw.Select(i => new RequestEntityParam() { Name = i.Name, UriNameParameter = i.Name, Parameter = i.Type }) .ToList(), //Параметры в uri uriParameters = uriParamsRaw.Select(i => new RequestEntityParam() { Name = i.Name, UriNameParameter = i.GetAttribute<WebApiMethodParameterFromUriAttribute>().ParameterName, Parameter = i.Type }) .ToList(), //Информация о классе-описании (нам нужно будет имя) requestEntityType = item }); } return result; } } }
Тут мы получаем все классы с интерфейсом IWebApiWithBulkInsert
и получаем всю информацию о построении точек WebApi.
Генерируем прообраз WebApi
Сам генератор будем делать просто текстом. Это проще, удобнее и быстрее, чем использовать что-либо еще. Можно просто копипастить готовый код и делать генератор(!).
Тем более информация о namespace-ах у нас всегда есть. А работать со строками умеют даже стажеры.
Сам генератор:
using CodeGen.GeneratorBase; using CodeGen.GeneratorBase.Context; using CodeGeneration.GeneratorBase; using Domain.Common.Generation.WebApiMethod.Attributes; using Microsoft.CodeAnalysis; using System.Collections.Generic; using System.Linq; namespace CodeGen.Generators.RequestEntity.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-и //Добавляем остальное txtExample += $@" namespace Infrastructure.Web.Controllers {{ [ApiController] partial class GeneratedWebController : ControllerBase {{ "; foreach (var item in data) { txtExample += $"\r\n//{item.defaultPath} {item.methods}"; txtExample += "__"+item.uriParameters.Count(); foreach (var uri in item.uriParameters) txtExample += $"\r\n//URI: {uri.UriNameParameter} {uri.Name} {uri.Parameter}"; foreach (var body in item.bodyParam) txtExample += $"\r\n//body: {body.UriNameParameter} {body.Name} {body.Parameter}"; txtExample += $"\r\n \r\n\r\n\r\n"; //Добавляем атрибут к методу 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}() {{ return Ok(); }} "; } txtExample += $@" }} }}*/ "; context.AddSource("InfrastructureWeb", txtExample); } public override List<RequestEntityGeneratorDTO> Parse(GenerationContext projectContext) { return scanner.Scan(projectContext); } } }
После пересборки проекта смотрим на то, что получилось (в Infrastructure.Web):
После пересборки проекта смотрим на то, что получилось (в Infrastructure.Web):
И результат:
using Microsoft.AspNetCore.Mvc; namespace Infrastructure.Web.Controllers { [ApiController] partial class GeneratedWebController : ControllerBase { ///MachineOne/state Get__1 //URI: stateObject state CodeGen.Utils.Scan.Data.ClassInfo.Reflection.ReflectionClassInfo [HttpGet("/MachineOne/state")] public IActionResult GetMachineOneRequestState() { return Ok(); } ///MachineOne/alert Post__0 //body: alert alert CodeGen.Utils.Scan.Data.ClassInfo.Reflection.ReflectionClassInfo [HttpPost("/MachineOne/alert")] public IActionResult GetMachineOneRequestAlert() { return Ok(); } ///MachineThree/state Get__1 //URI: stateObject state CodeGen.Utils.Scan.Data.ClassInfo.Reflection.RoslynClassInfo [HttpGet("/MachineThree/state")] public IActionResult GetMachineThreeRequestState() { return Ok(); } } }
Как видим, наши утилиты могут читать информацию об интерфейсах, атрибутах и из доменных сборок (Reflection), и из файлов в проекте(Roslyn).
Кроме того, наш сканер правильно получил информацию об описаниях всех трех точек.
Заключение
В этой части мы сделали набор утилит, позволяющий получать информацию о классах, свойствах и атрибутах как через рефлексию, так и через Roslyn.
Так же мы сделали описание наших точек WebApi при помощи 3-х атрибутов и интерфейса. Описание прос��ое и идеоматическое.
Напомню, все классы описаний — internal, и их не видно за пределами Domain.Entities.
Исходный код можно посмотреть тут.
В следующей части мы допишем 1 генератор, напишем еще 2 и получим наш готовый проект — WebApi, складывающее все в шину и читающее из шины пачками, и пачками вставляющее все в БД (миллионы запросов в минуту\секунду).
