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

Комментарии (38)


  1. jorge-list
    25.06.2025 00:08

    Пост познавательный и полезный. Спасибо.


    1. ValeriyPus Автор
      25.06.2025 00:08

      Подождите, сейчас WebApi закончу и начну делать или BoilerTemplate(APB), или ETL (3 объекта 2 маппинга 1 валидатор).

      Там можно будет воочию увидеть как работает кодогенерация (в 20-100 раз больше генерируемого кода, чем пишется ручками).


  1. SolidSnack
    25.06.2025 00:08

    Скажите, а причем в вашей статье DDD? Зашел посмотреть в первую статью, вы там пишите минус существующих статей с генерацией кода:

    2) Отсутствие связи с DDD/CleanArchitecture. Да, я рад что Ваш проект уже пол года живет и без DDD :)

    Так-же в 1 статье никак тема DDD не раскрывается, тут тоже не видно.


    1. ValeriyPus Автор
      25.06.2025 00:08

      В первой статье мы только настраивали запуск генератора на нужном уровне и в нужном проекте :)

      А если присмотреться к картинке?

      Да и текстом вроде написано и в первой части, что у нас 1 проект с генераторами, каждый генератор генерирует на своем уровне в своем проекте.

      В 3 части можете посмотреть как:

      1) WebApi в Infrastructure,

      2) DTO в Application и все остальное

      Генерируется по описанию в Domain (да, я вынес описание специфичного WebApi на уровень Domain)

      Подождите, сейчас WebApi закончу и начну делать или BoilerTemplate(APB), или ETL (3 объекта 2 маппинга 1 валидатор).

      Там можно будет воочию увидеть как работает кодогенерация (в 20-100 раз больше генерируемого кода, чем пишется ручками).


      1. SolidSnack
        25.06.2025 00:08

        Ну если ваше DDD и его объяснение в статье (надо же хоть как-то на этом остановиться, темболее если вы другим авторам себя в этом вопросе противопостовляете и если оно у вас в тегах и заголовке?) это картинка где написано Domain в котором объект что-то там делает, то смыславая нагрузка по DDD (что в статье, что получается в вашем проекте) стремится к 0


        1. ValeriyPus Автор
          25.06.2025 00:08

          Да, причем к абсолютному

          Иногда я захожу на хаб PHP и спорю там.

          DDD как расшифровывается?

          Domain-Driven Design

          С вики

          Предметно-ориентированное проектирование (реже проблемно-ориентированное, англ. domain-driven design, DDD) — набор принципов и схем, направленных на создание оптимальных систем объектов. Сводится к созданию программных абстракций, которые называются моделями предметных областей.

          У нас предметная область какая?

          Запросы - Вот мы и создали объекты для описания HTTP запросов в Domain.

          Далее, на основе описания нашей пока скудной предметной области при помощи генераторов пишем весь Boilerplate (контроллеры в Infrastructure, воркеры в Application, DTO в Application).

          Спорить с Вами дальше не буду - прошлый проект показывали даже ребятам из Росатома, сказали круто ).

          Если будет время - напишу цикл по BoilerTemplate своими руками (прямо с обьектами ORM и flow в домене, чтобы даже плохо разбирающиеся в DDD люди понимали о чем речь).


          1. SolidSnack
            25.06.2025 00:08

            Спорить с Вами дальше не буду - прошлый проект показывали даже ребятам из Росатома, сказали круто ).

            Если честно это вообще не показатель, я уже на Хабре успел повидать и из бигтеха людей которые не до конца понимают о чем говорят (козырять статусом - пыль в глаза)

            Ваша выжимка с вики ничего не добавляет.

            Скажите, а вы HTTP запросы делаете для бизнеса? Куда логика бизнеса пойдет, в нагенеренный код?

            У нас предметная область какая?

            Запросы - Вот мы и создали объекты для описания HTTP запросов в Domain.

            Так можно вообще любой сайт описать) Темболее вы сами привели из вики что Domain это система объектов, вы все HTTP запросы будете объектами описывать или как?) Ничего не понятно


            1. ValeriyPus Автор
              25.06.2025 00:08

              В первой статье написано - есть бизнесы у которых прямо специфические запросы приходят, и их много, и их надо в базу писать.

              Прямо с завода пришли и говорят - наши вендинги вот так аналитику отсылают.

              Это - доменная область? (предметная)

              Да.

              Это похоже на ORM - нет.

              Из-за плохого понимания границ DDD и возникает куча вопросов.

              А в ORM что?

              1) У нас вот такие документы и поля

              2) А по состоянию они вот так передвигаются.

              3) А вот при таком переходе еще и интеграция отрабатывает

              О, привычные объекты.


              1. SolidSnack
                25.06.2025 00:08

                А вы запросы обрабатываете как-то? Ну всмысле перед записью в базу логика есть какая-то? Или просто перекладываете туда-сюда?

                Прямо с завода пришли и говорят - наши вендинги вот так аналитику отсылают.

                Вот так это как? Просто условный JSON вам показали? Или все-таки есть какое-то описание данных, которые, я так пологаю, надо обработать?

                Да и темболее, смотрите, у вас аналитики отправлял по HTTP запросы, потом условно перешли на ВебСокеты для мониторинга в реальном времени и вы выкидываете ваши генераторы кода HTTP объектов (самой важной доменной области в приложении) и пишете новые для веб сокетов)) Это конечно же не DDD, а вы так говорите будто понимаете и сейчас тут будете всем объяснять)


                1. ValeriyPus Автор
                  25.06.2025 00:08

                  Вот так это как? Просто условный JSON вам показали? Или все-таки есть какое-то описание данных, которые, я так пологаю, надо обработать?

                  Я обозначил такую предметную область. Запросы не меняются.

                  Я не собираюсь писать "Генератор, превращающий WebApi вашей еле работающей СЭД в Амазон".

                  А вы запросы обрабатываете как-то? Ну всмысле перед записью в базу логика есть какая-то? Или просто перекладываете туда-сюда?

                  Нет, об этом написано в первой статье. Вы их читаете или сразу приходите что-то обьяснять? Может, вы даже не понимаете ООП.

                  Да и темболее, смотрите, у вас аналитики отправлял по HTTP запросы, потом условно перешли на ВебСокеты для мониторинга в реальном времени и вы выкидываете ваши генераторы кода HTTP объектов (самой важной доменной области в приложении) и пишете новые для веб сокетов)) Это конечно же не DDD, а вы так говорите будто понимаете и сейчас тут будете всем объяснять)

                  Тут по описанию предметной области такой переход не возможен (только расширение API, суровый IOT).

                  В СЭД: Это аномальное явление - если меняю инфраструктуру переписываю генераторы инфраструктуры.


                  1. SolidSnack
                    25.06.2025 00:08

                    Я вам ничего не объяснял изначально, вы статью позиционируете как что-то про DDD (еще и с кем-то себя сравниваете) я спросил где у вас DDD? Вы мне сказали что на картинке, ну на картинке так на картинке)


                    1. ValeriyPus Автор
                      25.06.2025 00:08

                      Вот и нашли человека, не понимающего DDD )

                      Что вам не понравилось?

                      1) Структура проекта

                      2) Зависимости между проектами

                      3) Описание предметной области задачи

                      4) Как сделана генерация

                      Введите одну или несколько цифр.


                      1. SolidSnack
                        25.06.2025 00:08

                        Вы пишите 2 статьи с DDD в названии, но все что у вас от DDD это картинка (и то только во второй статье, в первой вы просто говорите что у вас DDD, а у других нет) на которой написано Domain. Точно не вам объяснять мне что-то про DDD


                      1. ValeriyPus Автор
                        25.06.2025 00:08

                        Я полностью с Вами согласен.

                        Время специалистов стоит дорого.

                        DDD - не материально, или эта концепция как-то отражается в нашем, материальном мире? (В структуре проектов, зависимостях, функциональном наполнении?)

                        Почему бы вам просто не написать 3 - Мне не понравилось описание предметной области. (Потому что вы делаете СЭД и привыкли к тому, что WebApi - Infrastructure?)


                      1. SolidSnack
                        25.06.2025 00:08

                        Потомучто Domain это предметная область обернутая в код, а не HTTP запрос) Вы получается техническую реализацию суете в предметную область и говорите что у вас DDD, я вам уже привёл пример про веб сокеты, вы сказали такого не может быть в вашей задаче, ок, если в другой задаче может быть, у вас будет другой DDD?) Это просто нелепо


                      1. ValeriyPus Автор
                        25.06.2025 00:08

                        Я вам тоже сказал, я захотел и выбрал такую предметную область.

                        Такая предметная область существует.

                        В этой предметной области запросы - не Infrastructure, а основа всей системы (уровень Domain. Прямо физическое оборудование, нерушимое, без смены протоколов (и со сменой, но не везде)). И запросы только расширяются (не изменяются).

                        Я не делаю СЭД.

                        Выбор предметной области это вообще вопрос философский.


                      1. SolidSnack
                        25.06.2025 00:08

                        Дак вы тогда и пишите что это вы так хотите, зачем вы конкретные техники притягиваете сюда, если даже не пользуетесь ими. DDD дает техники для выделения предметной области, а не просто повод пофилосовствовать http это Domain или нет (конечно нет)))


                      1. ValeriyPus Автор
                        25.06.2025 00:08

                        Люди делятся на 2 типа - со способностью к абстрактному мышлению и без таковой.

                         Точно не вам объяснять мне что-то про DDD

                        Я ФОРМЫ ДЕЛАЛ! И ДЕЛАЮ ФОРМЫ! ДЛЯ СЭД! У МЕНЯ HTTP - INFRASTRUCTURE! DDD КАЖДЫЙ ДЕНЬ!


                      1. SolidSnack
                        25.06.2025 00:08

                        Вы бы вместо поясничества лучше бы в двух словах объяснили хоть что-то про DDD, а не кидали цитаты из вики, неужели так сложно парой слов описать то чем вы пользуетесь?



  1. itGuevara
    25.06.2025 00:08

    C#, Кодогенерация и DDD

    Можете что-то посоветовать из: "JS, Кодогенерация и DDD" для уровня Hello world?


  1. totsamiynixon
    25.06.2025 00:08

    Жаркая там у вас дисскусия с @SolidSnack, но я с ним согласен, более чем полностью.

    Домен это данные и способы их обработки. Действительно, HTTP запрос может быть объектом домента. Если ваши доменные специалисты (менеджеры, руководители и тд) используют данную терминологию (во что верится с трудом), пусть будет так.

    В обоих статьях Вы описываете, как Вы реализуете кодогерератор, который реализует какой-то сегмент вашего домента. Генерирует для него API, Cхемы данных, Worker процессы. Ну может это кому будет и полезно иммено с точки зрения того, как работает кодогенерация, какие есть приемы и тд.

    Может даже Ваши коллеги и внешние наблюдатели дают Вам восхищенную обратную связь на то, как вы описываете сам контракт.

    Но с моей точки зрения это решение не самое лучшее, по следующим причинам:

    • Домен - это данные и правила их трансформации; а так же машина состояний бизнес процесса, если это необходимо и по натуре процесс подразумевает растянутость во времени и несколько шагов. Ключевым элементом DDD являются агрегаты, сущности, объекты-значения, которые защищают инварианты. Техническое решение вторично: домену не важно, воркеры у вас обрабатывают данные, или вы обрабатываете их в контроллере, Mass Transit у вас или AWS Step Functions. Web API принимает запрос или gRPC или GraphQL. Это все вторично.

    • Соответственно, домен невозможно сгенерировать. Это возможно единственное, что не имеет смысла генерировать, это сердце вашего приложения. А вот дальше технические детали можно сгенерировать:

      • Генерацию Web API / gRPC / GraphQA и соответсвующих адаптеров к домену можно сгенерировать, ровно как и клиенты к этим API под разные языки.

      • Контракты Commands, DTO, IntegrationEvents под разные языки тоже.

      • DB можно сгенерировать (EntityFramework CodeFirst)

      • Генерация Kafka топиков (например)

      • По описанию стейт машины в коде на вашем DSL генерировать например AWS Step Functions.

    • Во всех этих случаях все работает ровно наоборот - Вы вешаете аттрибуты не на доменные классы; а вешаете атрибуты классов домена на элементы инфраструктуры. Вроде [WorkerFor(IOTRequestProcessingProcess)] в какой-нибудь инфраструктурной библиотеке. Или например вызвать app.SetupAsyncProcessingFor<IOTRequestProcessingProcess>()` в кодогенераторе инфры. Дальше по конвенциям он должен будет понять, в какой Kafka топик подписаться, как настроить Worker для пуллинга из Kafka, какие у вас там подходы для обработки ошибок и тд.

    Т.е. когда меняется инфраструктура, домену знать этого не нужно. А у Вас получится, что нужно менять классы домена, чтобы получить новую инфраструктуру. Нарушена классическая направленность зависимостей.

    Вот тогда у вас получится Domain Driven Design. Где драйвает реально домен.

    В некотором смысле то, что Вы пытаетесь реализовать, в общем виде уже реализовано в Microsoft Orleans. С точки зрения достижения ваших целей, Orleans предоставляет Shared Kernel, который позволяет моделировать домен. Вместо IAggregateRoot используются интерфейсы вроде ISomethingGrain и так далее. В Shared Kernel есть абстракции потоков данных, и далее фреймворк сам позаботится о большинстве деталей с минимальными настройками. Существуют различные адаптеры для разных провайдеров потоков, таких как Kafka, RabbitMQ, Azure и другие. Аналогично есть адаптеры для различных баз данных и API. Можно условно описать один раз, а затем менять инфраструктуру по необходимости. Не уверен, используют ли там кодогенерацию или рефлексию, но факт остается фактом.


    1. ValeriyPus Автор
      25.06.2025 00:08

      А я думаю вы с @SolidSnack даже не читали определений (или кучи публикаций\книг), а используете ChatGPT.

      Прочитайте хотя бы определение.

      https://en.wikipedia.org/wiki/Domain_model

      https://habr.com/ru/articles/453906/

      In the field of computer science a conceptual model aims to express the meaning of terms and concepts used by domain experts to discuss the problem, and to find the correct relationships between different concepts.

      Увы, я вам сказал что в моем цикле публикаций конечные точки - предметная область. И в моем случае эта "инфраструктура" гарантированно не будет меняться в течении всего времени жизни приложения.

      И у меня даже есть отделение доменной области (приходит Вася и говорит - новое оборудование вот по таким конечным точкам связывается)

      от непосредственно реализации этих конечных точек.

      Домен - это данные и правила их трансформации; а так же машина состояний бизнес процесса

      https://habr.com/ru/articles/921552/#comment_28483466

      Это для СЭД. И я видел более трех команд, не понимающих DDD даже на уровне СЭД (как раз объекты и переходы).

      В некотором смысле то, что Вы пытаетесь реализовать, в общем виде уже реализовано в Microsoft Orleans. 

      Я реализовал крутую кодогенерацию в DDD на C#.

      При чем тут Orleans?

      Все то, о чем вы пытаетесь написать уже пишет ChatGPT, ChatRTX, DeepSeek и Clippy.


      1. SolidSnack
        25.06.2025 00:08

        Опять Википедия и чужие статьи. Вы не отделяете техническую реализацию от бизнес логики, если у вас просто перекладка туда сюда, то впринципе DDD, наверное, даже лишний. И получается вы генераторы написали только для вашего HTTP домена)). Для остальных задач свои генераторы надо писать?


        1. ValeriyPus Автор
          25.06.2025 00:08

          Тебе уже 10 раз написали про доменную модель.

          Не читали определений\статей\публикаций, а лезете со своей кривой мордой.

          conceptual model aims to express the meaning of terms and concepts used by domain experts to discuss the problem, and to find the correct relationships between different concepts.

          Бизнес какой?

          Принять данные по указанным точкам WebApi (прямо оборудования сотни тысяч единиц, на миллионы), и занести в таблицы? (А потом привести к единому виду (etl) и отобразить?)

          Это и есть предметная область. В которой есть заказчики (эксперты доменной области).


          1. SolidSnack
            25.06.2025 00:08

            Не пригорайте так сильно, смотрите, мы уже раскопали чучуть вашу задачку, вот привод к единому виду, скорее и надо в модель ложить, а не HTTP запросы, вот мы ваш проект и подвинули ближе к DDD, пусть и на пол шага, это разве плохо?


          1. SolidSnack
            25.06.2025 00:08

            Ну и еще, вы говорите что доменная область это HTTP, но ваш бизнес и его участников HTTP как таковое волнует почти с 0 вероятностью. Их волнуют данные (которые 100% описаны) и работа с ними, вот вам и доменная модель. Дальше давайте сами ;) (еще и горите так, неприятно тут пытаться найти истину какую-то)


            1. ValeriyPus Автор
              25.06.2025 00:08

              Их волнуют данные (которые 100% описаны) и работа с ними, вот вам и доменная модель

              Конечные точки Web-Api их волнуют.

              Какие-то непонятные рассуждения про бизнес.

              Зашли рассказать, что не понимаете DDD, и испортить впечатление от публикаций?

              Вам еще вчера написали, что вы даже не можете внятно описать, что не так, а просто несете ахинею.

              При этом ахинея звучит довольно вызывающе (даже типично) - Вы дурак, DDD - это проявление Высшей Божественной Воли, и нельзя никак измерить и описать DDD!

              У вас не DDD! Где DDD?

              https://habr.com/ru/articles/921552/#comment_28483466


              1. SolidSnack
                25.06.2025 00:08

                Да я же говорю вам, не пригорайте, выдохните, поймите что HTTP это инфраструктура, и ваш код не может весь зависит от HTTP, потому-что в DDD есть четкая инверсия зависимостей и она направлена от домена во внешние слои, вы этого не соблюдаете, а когда с вами пытаешься поговорить у вас пригорает пукачело)


              1. totsamiynixon
                25.06.2025 00:08

                Вот Вы постоянно ссылаетесь на какие-то статьи и книги о том, чем DDD является и чем нет. При этом ссылок не приводите.

                Я как раз читал книги, статьи, публикации.

                Вот схожу 2 примера, от "создателя" DDD и амбассадор DDD:

                • Domain Driven Design Reference, Eric Evans, страница 10, предпоследний абзац выделен жирным.

                • Implanting Domain Driven Design, Vaughn Vernon, страница 125, глава "Порты и Адаптеры".

                Это видение евангелистов этого подхода и оно доказало свою жизнеспособность в реальном жизни: не важно, какой клиент у домена, хоть HTTP, Websockets, AMQP. Домен можно запустить и смоделировать процесс получения и анализа сотен тысяч девайсов и миллионов показателей просто в памяти. Когда меняются адаптеры (база, протоколы обмена сообщениями и ТД) - домен знать об этом не должен.

                Вместо того, чтобы кидаться какашками, ответили бы, что у вас за домен, какие агрегаты, как выглядит бизнес процесс. Тогда можно о чем-то рассуждать более предметно.

                Вы говорите, что бизнес это получение данных с сотен тысяч девайсов и миллионы записей. Возможно Вам DDD вообще не нужен за стадии сбора данных, но нужен на стадии анализа. А может и вообще не нужен. Я поэтому вам и предложил Microsoft Orleans - это решение, которое было разработано специально для таких кейсов (гейминг, IoT, трейдинг, гэмблинг и ТД), хотите с DDD, хотите без.

                В общем и целом пока вы не раскроете Ваш домен и не покажете, чем Ваше решение лучше альтернативного и общепринятого, Ваши статьи будут служить отличным примером использования кодогенерации, и плохим примером применения DDD.


                1. ValeriyPus Автор
                  25.06.2025 00:08

                  Я об этом позавчера и написал - да, я перенес WebApi в доменную область.

                  Это - предметная область (Вот такое WebApi, надо по 3-4 таблицам с кучи конечных точек все раскладывать. Таблицы хоть в mongo, хоть в SQL. И отображение - хоть WPF хоть MVC. Интеграции то ли будут, то ли не будут).

                  И я даже отделил описание предметной области (объект конфигурации) от физической реализации в Infrastructure.

                  https://habr.com/ru/articles/921552/#comment_28483466


                  1. totsamiynixon
                    25.06.2025 00:08

                    Это что-то из разряда - мы фронтэндеры и наш домен это показывать пользователю интерфейс. Бизнес приходит и говорит: "покажите данные с этого датчика в тэге <table>" или "надо добавить тэг <button>, и по событию onClick вызывать вот такую конечную точку используя метод POST". И нам вообще пофиг в каком браузере это будет работать, на каком устройстве, нажатие на кнопку через клавиатуру или мышку; потому что мы используем лучший придуманный для нашей сферы на данный момент DSL: HTML. И там уже какой браузер как хочет, пусть так и отрисовывает интерфейс. Мы считаем, что раз мы не знаем четкого определения DDD, то его не существует. Поэтому у нас DDD.


                    1. ValeriyPus Автор
                      25.06.2025 00:08

                      Это из разряда Один "эксперт" по DDD уже ушел

                      вы этого не соблюдаете, а когда с вами пытаешься поговорить у вас пригорает пукачело)

                      Теперь вот вы. Вы же в соседней статье даже ссылку на Apigee привели.

                      Скажите, в Apigee у нас конечные точки WebApi - Доменная сущность или нет?

                      А если вы в общем - https://habr.com/ru/articles/922418/

                      Отсутствие четких контрактов и API.

                      Да, может имеет смысл описывать WebApi (хоть и как-то видоизмененно) не только потому что система из цикла статей - Apigee, и это точно доменная сущность.

                      Может даже стоит вынести описание контрактов )

                      • Формализовать контракт: Необходимо создать чёткий интерфейс взаимодействия между контекстами и определить способы взаимодействия между контекстами.


                      1. totsamiynixon
                        25.06.2025 00:08

                        Так вот, в Apigee есть сущность эндпойнт. Ее можно изменять, версионировать и тд. Ее можно залочить для изменения. Можно описать какой пэйлоад ожидается и включить функцию автовалидации. Можно вроде даже какой-то JS добавить. И это все динамически, в рантайме. Apigee не надо перекомпилировать свой проект, когда пользователь изменяет эндпойнт в системе Apigee. Apigee надо будет перекомпилировать проект только, если они добавляют новый ТИП эндпойнта или новый протокол, что бывает очень редко. И никакой кодогенерации им не нужно. Слишком дорого каждый раз регенерировать то, что пользователь там в интерфейсе натыкал, слишком непредсказуемо.

                        В терминах DDD: Endpoint это агрегат. HTTP Method это Value Object. EbdpointWasLocked - доменное событие.

                        Далее делается админка (как у Apigee) и пользователю даётся возможно создавать или модифицировать инстансы агрегатов. Комбинируюя разные агрегаты он может реализовать свой юз кейс. Но базовый юз кейс Apigee это когда пользователь сделал запись, убедится, что все валидно, нет конкуренции, возможно асинхронно проверить сложные правила, которые распространяются на несколько агрегатов и ТД. И потом в своей стейт машине вызвать метод инвалидации кеша edge proxy, чтобы он учитывал последнее изменение.


                      1. ValeriyPus Автор
                        25.06.2025 00:08

                        И как вы предлагаете делать кодогенерацию?

                        Или вы говорите, что мне надо писать свой Apigee только потому что DDD не позволяет создать какой-то класс в Domain, и по нему что-то генерировать?

                        А в CRM мне значит нельзя сразу мои объекты в таблицы ложить из Domain, а надо сделать виртуальные таблицы, чтобы в рантайме можно было создавать таблицы и связи, и только потом работать с таким CRM?

                        Значит ли это, что создатели ABP - полные дураки?

                        Ведь там эти, доменные сущности приходится в коде создавать и перекомпилировать проект.


                      1. totsamiynixon
                        25.06.2025 00:08

                        Я вам предлагаю описать входные порты путем декларации application services. У вас будет IEndpointsService, IintgrationsService и ещё нужные вам сервисы. Далее используя кодогенерации можно при очень большом желании сгенерировать все, от HTTP API к этому порту, до вообще в целом UI клиента (смотрите HATEOAS принцип из REST). Далее предположим у вас есть какие-то датчики, которые должны писать в определенные эндпойнта. Решается это так - создаём через сгенерированную CMS эндпойнт, и присваиваем ему ID.

                        Далее через тот же CMS конфигурируете девайс (новый ТИП агрегата) и делаете ассоциацию девайса на эндпойнт; далее каждый девайс программируется с двумя параметрами: id девайса и захардкоженный Дискавери эндройнт. Девайс будет ходить в Дискавери эндпойнт и получать свою актуальную конфигурацию. Так вы динамически прямо в рантайме сможете менять эндпойнты девайсам, если это надо.

                        Опять же, можно сделать и проще, если число типов девайсов ограничено, можно привязывать эндпойнт к типу девайса (value object). А id девайса становится просто метаданными отправленных данных с девайса.

                        Далее у вас есть процессинг, опять же, может он не такой и сложный и не нужно прямо каждый раз привлекать разработчика. Если бизнес понимает, какая ему нужна агрегация, точно так же можно задать правила агрегации через CMS. Тут решение надо принимать о сложности данных правил, их уникальности и ТД.

                        Общее правило в бизнесе всегда следующее - если бизнес просит разработчика сделать одного и то же уже несколько раз - надо рассмотреть вариант переноса этого в CMS. Это будет не core domain, а supporting domain.

                        Поэтому надо знать, как в бизнесе появляются новые типы девайсов, что там за данные, что реально требует конфигурирования, нужно ли удаленная манипуляция устройствами, нужно ли например предусматривать обратную совместимость и ТД. Это все детали домена, без которых построить систему я не смогу.

                        Вы можете делать полностью статическую конфигурацию и редактировать ее только через код, и это умно, если вы хотите, чтобы бизнес встал, когда разработчики ушли на больничный. Это повышает вашу ценность для бизнеса. Но это нечестная игра ;)


                      1. ValeriyPus Автор
                        25.06.2025 00:08

                        Никто не предлагает делать полностью статическую или полностью динамическую конфигурацию.

                        Это вопрос веры

                        Вы можете делать полностью статическую конфигурацию и редактировать ее только через код, и это умно, если вы хотите, чтобы бизнес встал, когда разработчики ушли на больничный. Это повышает вашу ценность для бизнеса. Но это нечестная игра ;)

                        Наоборот. Коробочные решения для создания CRM, где объекты конфигурируются на лету или работают медленно, или стоят много или очень медленно развиваются.

                        Иногда все 3 разом.

                        Разработка же в доменных сущностях (добавляя их через классы) - Довольно быстро (если это DDD), и просто, да еще и быстро работает.

                        В ряде сценариев не существует даже отдаленно нужного функционала в готовых BPM/CRM системах, и расширять их практически невозможно (поэтому и ищут разработчиков микросервисов для любимых BPM Online/Directum и прочих).

                        Нет абсолютно ничего плохого в статической конфигурации (тех же магазинов и прочих мелких CRM). Даже 1С программируется через Конфигуратор.


                      1. totsamiynixon
                        25.06.2025 00:08

                        Так я же не говорю, что обязательно надо делать CMS для всего. Ее можно делать для ключевых сценариев, которые требуют гибкости. Очевидно, что вместе с гибкостью растет сложность конфигурации (runtime / compiletime). Когда приходит новый типовой заказчик c запросом на интеграцию - то научить условного Олега кликать 3 кнопки проше и дешевле, чем привлекать разработчика. Тем более разработчик всегда занят, у него важные дела, он пишет кодогенераторы.

                        Вы четко скажите, что за домен, нарисуйте диаграмму юз кесов, объясните, чем Ваше решение лучше, чем альтернативные методы, и вообще какие альтернативы рассматривали. А потом уже рассказывайте, как вы это имплементировали. Тогда можно будет проследить цепочку: проблема - анализ - решение.

                        Т.е. может быть ваше решение типа ввести какие-то доменные декораторы через атрибуты даже окей. Может быть даже действительно Ваши типы агрегатов представляют собой сигнатуры HTTP запросов и то, что Вы вводите новый тип аггрегата под интеграцию это норм решение и Вы берете на себя обе роли - и разработчика и менеджера по интеграциям. А может все можно сделать по-другому и это будет более эффективно в данном конкретном случае? Например будет конфигурироваться в 3 раза дольше условным Олегом, зато Олег в 5 раз дешевле, а еще он и заказчику позвонит, попросит включить девайс и проверит, собираются ли все данные и тд. Может бизнесу было бы проще самому это все конфигурировать, и потом в CMS в 2 клика исправлять мелкие недочеты путем выпуска новой ревизии, когда позвонил клиент, чем ждать, пока разработчик запулит это из бэклога, внесет изменения, поправит свои юнит тесты и выкатит код на стейджинг, а потом и в прод. Так вот для ответа на этот вопрос нужены результаты анализа.

                        А так вижу решение проблемы, которой я не понимаю, и мне сложно дать этому даже свою личную оценку, не то, что экспертную. Поэтому как статья о DDD - это слабо. В связи с этим, я оцениваю только кодогенерацию, она да, расписана хорошо, с чего начать, какие приемы, сложности и тд.

                        P.S. Что же все-таки такое домен

                        В общем случае домен, это то, что раньше бизнес делал мануально (пользователи заполняли бумажные формы (призывники в военкомате), специалист заводил карточку профиля пользователя (личное дело), потом какой-то специалист считал что-то на калькуляторе (индекс массы новобранца), записывал в какие-то сводки или отчеты (сколько там у нас новобранцев за этот призыв), потом передавал эти отчеты в другой отдел (штаб и тд), а теперь это выражено в коде и происходит само. Т.е. домен это то, что в этой цепочке имеет смысл автоматизировать и по каким правилам. А инфраструктура, это каким цветом чернил и на какого оттенка бумаге писали сотрудники, какой марки был калькулятор и какая высота полок архива в котором хранились личные дела. Но если в военкомате доверяют только одной марке калькулятора (HTTP), а рассчеты на другой считаются невалидным (gRPC, GraphQL), значит это тоже идет в домен.

                        Т.е. всегда надо проектировать, а как бы этот процесс был организован без какой-либо автоматизации - и записывать это в каком-то документе в как можно более коротком виде. Все то, что есть в этом документе - это домен. Остальное - инфраструктура.

                        Даже по датчикам - раньше бы приходил (и не важно, какую обувь он носит) специалист Олег и смотрел бы, что там за датчики, что за информацию собирают, записывал бы показания в какой-то бланк, и сдавал бы тете Клаве в отдел формирования отчетов. А тетя Клава по "типу датчика" находит доступные типы отчетов, а по "профилю заказчика" фильтрует те, которые нужны заказчику. Или например было бы так, что клиент ходит к своим датчикам сам и потом сообщает тете Клаве напрямую, что он собрал и описывает, какой отчет хочет получить. А тетя Клава должна сама разработать алогорим расчета данных в отчете. Это разные процессы, требующие разных входных данных и доменных знаний и имеющие разную степень конфигурируемости и автоматизации.