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