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

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


  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. Можно условно описать один раз, а затем менять инфраструктуру по необходимости. Не уверен, используют ли там кодогенерацию или рефлексию, но факт остается фактом.