Как стать автором
Обновить

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

Уровень сложностиСложный
Время на прочтение10 мин
Количество просмотров1.4K

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

Теги:
Хабы:
0
Комментарии21

Публикации

Работа

Ближайшие события