C#, Кодогенерация и DDD. Часть 2 — Получаем данные и пробуем генерировать

от автора

Это — вторая публикация в серии 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.

Что cделаем

Что cделаем

Опишем конечные точки

Конечные точки 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, складывающее все в шину и читающее из шины пачками, и пачками вставляющее все в БД (миллионы запросов в минуту\секунду).


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


Комментарии

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

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