redb.Route.Xml
redb.Route.Xml

Маршрут в .route.xml на том же движке, что и C#: подсказки в VS Code, проверка пакета в dotnet build, граф-редактор и горячая замена модуля.

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

В 4.0 у redb.Route появилась вторая запись маршрута: XML. Файл .route.xml кладётся в пакет, пакет в папку работающего сервиса, и меньше чем через полсекунды по новому маршруту идёт первое сообщение. При этом XML здесь не отдельный движок и не упрощённый конструктор «для аналитиков». Загрузчик превращает разметку в те же вызовы DSL, которые пишутся на C#, и дерево определений выходит тем же самым. Для примеров из репозитория это закреплено тестами: разметка, сгенерированный из неё C# и дерево, которое строит загрузчик, совпадают.

Вокруг разметки выросло то, ради чего она и затевалась: схема, которую редактор подхватывает сам по пространству имён, проверка пакета прямо в dotnet build, граф-редактор в VS Code, пакеты, которые приносят в разметку собственные элементы, и модули без единой сборки, которые воркер меняет на лету. Ниже всё это разобрано на живом примере.

Откуда пример

Модуль серийных номеров появился из обсуждения на GitHub. Читатель описал систему из четырёх задач Quartz, между которыми лежат таблицы со статусами, и спросил, как это ложится на redb.Route. Ответом стал SerialNumbersDemo: два партнёра (один на SFTP, другой на AS2), SQL Server, объекты в redb, отчёт по квотам по расписанию и упаковка в .tpkg для Tsak. Маршруты там написаны на C#.

Рядом с ним лежит XML-двойник, SerialNumbers.Xml. Домен, сервисы и база у них общие, другим стал только способ описать поток. Это честная точка сравнения: одна интеграция, две записи, один движок.

Один маршрут, две записи

Сердце модуля: запрос на серийные номера. Файл проверяется по XSD и разбирается в объект, решение принимается в транзакции, ответ ставится в исходящую очередь, а после коммита пишется лог о том, что уже произошло. На C#:

From(RouteUris.SerialNumberRequest)
    .RouteId("serial-number-request")
    .MessageHistory()

    .DoTry()
        .ValidateXsd(XmlSchemas.SerialNumberRequest)
        .Unmarshal<SerialNumberRequestXml>("application/xml")
    .DoCatch<ValidationException>()
        .ProcessWithRedb(IntakeRecorder.RecordInvalidAsync)
        .Log("${header.serials.partner}: ${header.serials.fileName} violates the schema, recorded as Invalid", LogLevel.Warning)
        .Stop()
    .EndTryCatch()

    .Transacted()
        .ProcessWithRedb(SerialRequestService.RegisterAsync)
        .Choice()
            .When(e => DecisionOf(e) == MessageStatuses.Accepted)
                .ProcessWithRedb(SerialRequestService.AllocateAsync)
                .ProcessWithRedb(SerialRequestService.QueueResponseAsync)
            .When(e => DecisionOf(e) == MessageStatuses.Rejected)
                .ProcessWithRedb(SerialRequestService.RejectAsync)
                .ProcessWithRedb(SerialRequestService.QueueResponseAsync)
        .EndChoice()
    .EndTransaction()
    // ...логи после коммита

Тот же маршрут в разметке:

<route id="serial-number-request" description="A serial number request: validate, decide, respond">
  <from uri="direct://serial-number-request"/>
  <messageHistory/>

  <tryCatch>
    <try>
      <validateXsd file="SerialNumberRequest.xsd"/>
      <unmarshal format="application/xml"
                 target="SerialNumbers.Core.Integration.Xml.SerialNumberRequestXml, SerialNumbers.Core"/>
    </try>
    <catch exceptions="redb.Route.Validation.ValidationException">
      <to uri="bean:#intake?method=RecordInvalid"/>
      <log level="Warning">${header.serials.partner}: ${header.serials.fileName} violates the schema, recorded as Invalid</log>
      <stop/>
    </catch>
  </tryCatch>

  <transaction>
    <to uri="bean:#serial-requests?method=Register"/>
    <choice>
      <when expr="${header.serials.decision} == 'Accepted'">
        <to uri="bean:#serial-requests?method=Allocate"/>
        <to uri="bean:#serial-requests?method=QueueResponse"/>
      </when>
      <when expr="${header.serials.decision} == 'Rejected'">
        <to uri="bean:#serial-requests?method=Reject"/>
        <to uri="bean:#serial-requests?method=QueueResponse"/>
      </when>
    </choice>
  </transaction>

  <choice>
    <when expr="${header.serials.decision} == 'OnHold'">
      <log>${header.serials.partner}: request ${header.serials.requestId} is on hold until its product is activated</log>
    </when>
    <otherwise>
      <log>${header.serials.partner}: request ${header.serials.requestId} ${header.serials.decision}, response queued</log>
    </otherwise>
  </choice>

  <log level="Debug">${messageHistory()}</log>
</route>

Соответствие построчное. DoTry и DoCatch стали <tryCatch> с ветками <try> и <catch>Transacted() стал <transaction>, лямбды-предикаты стали выражениями в атрибуте expr. Граница транзакции в разметке видна глазами: всё, что лежит внутри <transaction>, коммитится вместе, всё, что снаружи, пишется уже после коммита.

Вызов сервиса ProcessWithRedb(SerialRequestService.RegisterAsync) превратился в bean:#serial-requests?method=Register. Бин здесь тонкая обёртка, которая берёт redb с обмена и отдаёт обмен тому же сервису:

public sealed class SerialRequestBeans
{
    public Task Register(IExchange exchange, CancellationToken ct)
        => SerialRequestService.RegisterAsync(Redb(exchange), exchange, ct);

    public Task Allocate(IExchange exchange, CancellationToken ct)
        => SerialRequestService.AllocateAsync(Redb(exchange), exchange, ct);

    // Reject, QueueResponse: так же

    internal static IRedbService Redb(IExchange exchange)
        => exchange.ServiceProvider?.GetService<IRedbService>()
           ?? throw new InvalidOperationException("The exchange carries no IRedbService.");
}

Экземпляр redb тот же, что получил бы ProcessWithRedb: одно соединение на обмен, в транзакции, которую открыл маршрут. В демо таких обёрток десять, и это весь код, который понадобился разметке сверх уже существующего модуля. Разметка решает, когда выполняется шаг, сервис решает, что происходит.

Не второй движок, а вторая запись

Главное свойство формата легко проговорить и трудно переоценить: разметка ничего не исполняет сама. Загрузчик читает документ и вызывает Filter(...)When(...)SetHeader(...) и остальной DSL ровно так, как их вызвал бы C#-код. Отсюда следствия, которые достаются бесплатно:

  • Всё, что умеет движок, умеет и разметка. Ретраи, транзакции, история сообщений, метрики, маскировка секретов в URI, моки в тест-ките: всё это живёт в движке и работает одинаково для обеих записей.

  • Маршрут переводится обратно в C#. Команда redb-route-xml csharp печатает класс RouteBuilder. В читаемом стиле комментарии разметки становятся комментариями кода, в машинном в код добавляются директивы #line, и точка останова в отладчике встаёт на строку XML-файла.

  • Диаграмма берётся из того же источника. redb-route-xml mermaid строит flowchart по дереву определений, поэтому схема в документации не расходится с работающим маршрутом.

  • Переход идёт по одному маршруту. Обе записи живут в одном контексте, ссылаются на одни и те же direct:-адреса и бины, и переписывать всё разом незачем.

Для примеров из репозитория это закреплено тестами: каждый пример загружается и сравнивается с эталонным описанием, из каждого генерируется C#, который компилируется в тестовом проекте и строит побайтно то же дерево определений. Всего у разметки 174 теста на трёх целевых платформах, net8, net9 и net10.

Схема, которую редактор подхватывает сам

Документ разметки начинается с пространства имён:

<routes xmlns="urn:redb:route:1.0">

По этому пространству имён расширение для VS Code отдаёт схему через каталог OASIS, и дальше работает Red Hat XML: автодополнение элементов и атрибутов, подсказки значений перечислений, подчёркивание ошибок. Схему файл получает по тому, что сказано в его корне, с любым именем и в любой папке. Чужой route.xml без нашего пространства имён редактор не трогает.

Схема не пишется руками. Её генерирует тот же реестр элементов, которым пользуется загрузчик, так что редактор знает ровно то, что потом примет сервис: 77 элементов, включая элементы пакетов, и типизированные опции 38 схем коннекторов. У sql: это dataSourceplaceholderStyle с вариантами AtColon и QuestionreadOnly и остальные опции с типами и значениями по умолчанию. Опции, помеченные в коде коннектора как чувствительные, подсвечиваются как секреты.

Подсказки и проверка разметки в VS Code
Подсказки и проверка разметки в VS Code

Каталог опций собирается рефлексией по сборкам коннекторов. Появилась в коннекторе новая опция, каталог пересобран, и редактор предлагает её без правки единой строки в расширении.

Граф-редактор: текст остаётся правдой

Второй режим того же расширения открывает маршрут как граф: Open With, затем redb Route Graph, или кнопкой в заголовке редактора. Граф здесь проекция текста, а не второй формат хранения. Файл никогда не пересериализуется: каждая правка в графе превращается в точечную замену в тексте, обычно в одну строку, и отменяется обычным Ctrl+Z. Комментарии, отступы и порядок атрибутов остаются такими, какими их оставил автор.

Что в нём есть:

Маршрут в граф-редакторе VS Code
Маршрут в граф-редакторе VS Code
  • Три раскладки. «Змейка» идёт слева направо и переносит длинный маршрут на новый ряд, как текст переносит строку. «Колонки» рисуют маршрут сверху вниз, маршруты стоят рядом. Третья раскладка рисует в духе Mermaid: кривые рёбра, ромбы ветвлений, шестиугольники условий, скобки вокруг filter и transaction. Масштаб меняется кнопками и Ctrl+колесо, режим и масштаб запоминаются для каждого файла.

  • Панель свойств по клику. Идентификатор и описание первыми, перечисления и флаги выпадающими списками. URI эндпоинта разложен на отдельные опции из каталога коннектора: заданные сверху, остальные ниже, секреты замаскированы. Плейсхолдер {{sftp.host}} правится как есть, а значение, в которое он раскроется из конфигурации, показано рядом.

  • Правка мышью. Шаг вставляется через «+» между шагами и палитру, собранную из того же реестра элементов. Шаг перетаскивается на новое место или внутрь скобки. Правая кнопка открывает меню: вставить до, после или внутрь, показать в тексте, удалить. Шаг переезжает вместе со своими комментариями.

  • Панель свойств эндпоинта
    Панель свойств эндпоинта

Эндпоинт: короткий URI или структура

Короткий адрес пишется как в C#:

<to uri="kafka://orders?key=${header.tripId}"/>

Длинный лучше читается структурой. Текст SQL-запроса ложится содержимым элемента без экранирования, параметры идут отдельными строками:

<to>
  <sql dataSource="#main-db">
    <![CDATA[ INSERT INTO auth_log(login, at) VALUES (:#login, :#at) ]]>
    <param name="login" value="${header.login}"/>
    <param name="at" value="${dateformat(now(), 'o')}"/>
  </sql>
</to>

Загрузчик собирает из структуры ту же строку URI, которую написал бы человек, и дальше по конвейеру идёт одна каноническая форма: её видят кэш эндпоинтов, статистика, маски моков и маскировка секретов. Здесь работает правило, которое держит формат в порядке: ни у одного коннектора нет кода, написанного специально для XML. Структурная форма, типы опций, подсказки и проверка выводятся из того, что у коннектора уже есть. Новый коннектор получает разметку в день своего появления.

Фабрики, конфигурация и секреты

Объекты, на которые ссылаются маршруты, объявляются в context.xml пакета. Главный их вид это фабрики подключений: они уже лежат в именованном реестре контекста, а URI ссылается на них по имени. Значит фабрика объявляется в разметке, настройки приходят плейсхолдерами из конфигурации, а в URI не остаётся ни одного секрета:

<bean name="main-db" type="redb.Route.Sql.Connection.SqlConnectionFactory, redb.Route.Sql">
  <constructorArg>
    <bean type="redb.Route.Sql.Connection.SqlConnectionOptions, redb.Route.Sql">
      <property key="ConnectionString" value="{{db.main.connection}}"/>
    </bean>
  </constructorArg>
</bean>

{{key}} и {{key:default}} раскрываются через цепочку конфигурации до приведения к типу, поэтому {{ldap.port:636}} честно становится числом. Каждый плейсхолдер без значения по умолчанию попадает в манифест пакета как обязательный ключ: тот, кто выкладывает модуль, видит список настроек, которые нужно задать, ещё до запуска.

Бывают фабрики, которые хранят не строку, а объект. У AS2 это сертификаты: свой с закрытым ключом и сертификат партнёра. Для таких случаев свойство принимает вложенный бин, а атрибут factoryMethod строит объект статическим методом вместо конструктора. Сертификат грузится штатным X509CertificateLoader:

<bean name="globex" type="redb.Route.As2.As2ConnectionFactory, redb.Route.As2">
  <property key="OurCertificate">
    <bean type="System.Security.Cryptography.X509Certificates.X509CertificateLoader, System.Security.Cryptography.X509Certificates"
          factoryMethod="LoadPkcs12FromFile">
      <constructorArg value="{{As2.CertificateDirectory}}/hub.pfx"/>
      <constructorArg value="{{As2.CertificatePassword}}"/>
    </bean>
  </property>
  <property key="As2From" value="{{As2.Id}}"/>
  <property key="As2To" value="GLOBEX"/>
  <property key="Sign" value="true"/>
  <property key="Encrypt" value="true"/>
</bean>

Партнёр по AS2 описан целиком: идентификаторы, профиль подписи и шифрования, ключевой материал. Пароль от сертификата приходит из слоя конфигурации воркера и в пакет не попадает никогда.

Пакеты приносят свои элементы

Реестр элементов открыт. Пакет коннектора может добавить в разметку собственные элементы, и схема, проверка пакета и редактор узнают о них из его сборки. Так в разметке появились <cache><rest><transformJson><payload> и мост к хранилищу redb: <redbGet><redbSave><redbQuery><redbDelete>, а в контексте блок <redb>.

Вот маршрут из второго демо, XmlDemo, который работает с redb без единой строки C#:

<route id="redbdemo-cycle" description="redb bridge: upsert one row by unique key, query it back">
  <from uri="timer://redbdemo?period={{demo.period:5000}}"/>
  <setBody expr='{"name":"xmldemo:heartbeat","value_unique":"xmldemo:heartbeat","properties":{"ProcessorName":"xmldemo","MessageKey":"heartbeat","Confirmed":true}}'/>
  <redbSave type="redb.Route.RedbCore.Models.IdempotentEntryProps, redb.Route.Core" byUnique="true"/>
  <redbQuery type="redb.Route.RedbCore.Models.IdempotentEntryProps, redb.Route.Core"
             where="ProcessorName == 'xmldemo' AND MessageKey == 'heartbeat'"
             orderBy="MessageKey" take="10"/>
  <setBody expr="REDBDEMO total=${body.Count}"/>
  <to uri="log://redbdemo"/>
</route>

<redbSave byUnique="true"> сохраняет объект по уникальному ключу: запись с таким ключом обновляется, новая не появляется. <redbQuery> переводит выражение из where в серверный запрос redb с сортировкой и ограничением, а не выбирает всё подряд, чтобы отфильтровать в памяти. Выражение, которое нельзя выполнить на стороне базы, отклоняется при загрузке маршрута с понятным сообщением. Схема данных синхронизируется один раз при старте контекста:

<context xmlns="urn:redb:route:1.0">
  <redb>
    <syncScheme type="redb.Route.RedbCore.Models.IdempotentEntryProps, redb.Route.Core"/>
  </redb>
</context>

Проект маршрутов

Маршруты в XML живут в обычном проекте .NET, у которого есть соглашение о раскладке. Его создаёт одна команда, redb-route-xml new, и дальше пакет собирается из того, что лежит по местам:

Путь

Что там

routes/*.route.xml

маршруты, порядок загрузки задаёт манифест

context.xml

компоненты, бины контекста, одноразовый конвейер инициализации <onInit>

resources/

схемы XSD, XSLT и прочее, на что маршруты ссылаются по имени файла

config/{Имя}.config.json

идентичность модуля: имя контекста и автозапуск

config/context.sample.json

образец настроек для того, кто выкладывает модуль

schema/

схема для работы с проектом без расширения, по привязке в .vscode

Конфигурация раскладывается по слоям. В пакете едет только идентичность модуля, настройки приходят из общей конфигурации воркера и его слоя переопределений, секреты задаются переменными окружения на воркере. Один и тот же .tpkg поэтому выкладывается на стенд и в прод без пересборки: меняется окружение, а не пакет.

Если проект маршрутов ссылается на свои типы (как SerialNumbers.Xml на домен и сервисы), это обычные ссылки на проекты и пакеты NuGet. Сборка кладёт их в выход, проверка пакета видит их там же, где их увидит воркер.

Тесты: тот же тест-кит

XML-маршрут тестируется ровно так же, как маршрут на C#, тем же тест-китом и без собственного тестового хоста. Загрузили разметку в контекст, подменили внешний эндпоинт моком, отправили сообщение, проверили ожидания. Это тест из самого репозитория:

[Fact]
public async Task TheCanonicalTestKitFlow_WorksOverAnXmlRoute()
{
    _context.AddXmlRoutesFromContent("""
        <routes xmlns="urn:redb:route:1.0">
          <route id="under-test">
            <from uri="direct://tk-in"/>
            <setHeader name="seen" value="true"/>
            <to uri="kafka://orders"/>
          </route>
        </routes>
        """);
    _context.AdviceAllRoutes(a => a.MockEndpoints("kafka://*"));
    await _context.Start();

    var mock = _context.Mock("kafka://orders").ExpectMessageCount(1).ExpectHeader("seen", "true");
    await _context.SendBody("direct://tk-in", "payload");

    await mock.AssertIsSatisfiedAsync(TimeSpan.FromSeconds(2));
}

Маршрут из файла грузится так же, одной строкой AddXmlRoutes("routes/orders.route.xml").

WeaveById находит шаг по атрибуту id в разметке, так что любой шаг XML-маршрута можно заменить в тесте, не трогая файл. Пароль из URI не всплывает ни в именах моков, ни в отчётах теста: маскировка секретов работает на той же канонической строке URI, что и в рабочем маршруте.

Проверка до выкладки

Разметку правят руками, поэтому формат устроен так, чтобы ошибка находилась как можно раньше. Первая линия это загрузчик: он собирает все ошибки документа в один список с номерами строк и колонок, а не падает на первой. Битое выражение в условии ловится при загрузке, а не на первом сообщении.

Вторая линия это проверка пакета, redb-route-xml check и pack. Её можно включить прямо в сборку проекта:

dotnet build -p:PackRouteOnBuild=true -p:Version=1.0.0

После сборки проверка идёт по свежему выходу проекта и видит реальные сборки. Что она проверяет:

  • разметку целиком по схеме, с позицией каждой ошибки;

  • ссылки на объекты реестра: #main-db без объявления в пакете называется поимённо, а параметр SQL :#login ссылкой не считается, отличие определяется по позиции решётки;

  • бины по реальным сборкам: тип существует, свойства из <property> записываемые, метод из bean:#x?method=M действительно есть;

  • ресурсы: схема из validateXsd file= лежит в пакете;

  • секреты, записанные буквально, вроде password=... в URI;

  • обязательные ключи конфигурации, которые уходят в манифест.

Вот её ответ на опечатку в имени метода бина и в имени элемента:

error: routes/serial-number-request.route.xml(22,10): The element 'catch' in namespace
       'urn:redb:route:1.0' has invalid child element 'stopp' ...
error: routes/serial-number-request.route.xml: bean 'serial-requests':
       type 'SerialNumbers.Xml.Beans.SerialRequestBeans' has no public method 'Regster'
       (bean:#serial-requests?method=Regster).

И на исправленный файл:

serial-numbers-xml 0.0.0: 1 artifact(s), 0 required config key(s) - ok

Ошибка в разметке становится ошибкой сборки проекта, пакет с ошибкой не собирается. Висячая ссылка, несуществующий метод и забытый файл схемы находятся на машине разработчика, а не в логе продуктового воркера. Инструмент следит и за собой: если он собран против другой версии redb.Route.Xml, чем сборки проекта, он останавливается и называет обе версии.

Модуль без единой сборки

Пакет, в котором есть только .route.xmlcontext.xml, ресурсы и конфигурация, это полноценный модуль Tsak. Кладёте .tpkg в папку modules работающего воркера, и он поднимает его сам. Так это выглядит в логе для XmlDemo:

03:33:17.720 [INF] Registered module xmldemo v4.0.1
03:33:17.743 [INF] Created context xmldemo
03:33:17.881 [INF] XmlRouteModule xmldemo: loaded 1 artifact(s)
03:33:18.091 [INF] Exchange [redbdemo-cycle]: Body: REDBDEMO total=1
03:33:18.095 [INF] Context 'xmldemo' started successfully: all 1 endpoints operational
03:33:23.108 [INF] Exchange [redbdemo-cycle]: Body: REDBDEMO total=1
03:33:28.134 [INF] Exchange [redbdemo-cycle]: Body: REDBDEMO total=1

От регистрации модуля до первого обмена 0,37 секунды. Счётчик стоит на единице тик за тиком, потому что сохранение по уникальному ключу обновляет одну и ту же запись. Удалили файл из modules, и модуль выгрузился так же тихо:

03:34:27.821 [INF] Package xmldemo-4.0.1.tpkg removed from disk, unloading 1 modules
03:34:27.857 [INF] Removed context xmldemo
03:34:27.871 [INF] Module xmldemo unloaded: context stopped, ALC released

Модулю, которому нужен код, пакет несёт и сборку. Воркер сначала поднимает код модуля, а потом разметку, поэтому объекты, которые код положил в реестр, разметка видит по #имени. Так устроен и SerialNumbers.Xml: его сборка содержит бины-обёртки и бутстрап, который поднимает демо-базу, а весь поток описан в XML. Как Tsak собирает сервисы из модулей, рассказано в статье про микросервисы на Tsak.

Выражения: один язык и ошибки при загрузке

Язык выражений у разметки тот же, что у C#-маршрутов, и разобран в отдельной статье про выражения redb.Route. Для XML важны три вещи.

Позиция решает смысл. В условии (filterwhenvalidate) строка становится предикатом, в значении (setBodysetHeadertoD) становится вычисленным объектом, в uri и id остаётся строкой как есть. Спрашивать «это выражение или текст» не приходится: ответ даёт атрибут, в котором строка стоит.

Пробелы вокруг операторов незначимы, и это удобно именно для XML. В значении атрибута знак «больше» можно не экранировать, а «меньше» экранировать обязательно, поэтому сравнение без пробелов выходит ещё и короче:

<filter expr="header.amount>1000"/>
<filter expr="header.amount &gt; 1000"/>
<filter expr="header.amount &lt; 10"/>

Диагностика читается выражением. Функция messageHistory() отдаёт след обмена по шагам: таблицей, одной строкой log > choice > to(http), JSON-массивом или числами ('count''totalMs''slowestMs'). Функция stats('otel:...') читает счётчики слоя OpenTelemetry, которых нет в статистике эндпоинтов: задержки throttle, срабатывания circuitbreaker, сообщения, отброшенные filter. В разметке это превращается в условия, для которых раньше нужен был код:

<setHeader name="trail" expr="messageHistory('compact')"/>
<choice>
  <when expr="messageHistory('slowestMs') &gt; 500">
    <log level="Warning">${routeId} slow: ${messageHistory()}</log>
  </when>
</choice>

Запись следа включается шагом <messageHistory/> или опцией движка. Там, где её не включали, функция отвечает пустой строкой и нулём: диагностический лог не должен ронять маршрут.

Что важно знать до старта

Топология описывается явно. В C#-версии демо SFTP-консьюмер и маршрут доставки создаются в цикле по списку партнёров из базы. Разметка описывает каждого партнёра отдельным маршрутом, и топология видна в файле, а не вычисляется при старте. Когда партнёров десяток и меняются они выкладкой, так читается лучше. Когда партнёры это таблица, которая меняется на ходу, эти маршруты строит C#-билдер в той же сборке пакета, а разметка описывает общую часть потока. Обе записи живут в одном контексте.

Код остаётся там, где он код. Бизнес-логика живёт в сервисах, разметка вызывает их через бины. Если сигнатура сервиса отличается от (IExchange) или (IExchange, CancellationToken), между ними встаёт обёртка в две строки, как в примере выше.

XML добавляется, а не заменяет. Маршруты переходят в разметку по одному, остальные продолжают работать на C#. Если маршрут перерос разметку, redb-route-xml csharp превращает его в код, и с этого места он развивается как обычный RouteBuilder.

Как попробовать

Инструмент ставится из NuGet:

dotnet tool install -g redb.Route.Xml.CodeGen

Он создаёт проект маршрутов по соглашению пакета, со схемой и привязкой для редактора:

redb-route-xml new Orders --context orders

Проект собирается и упаковывается вместе с проверкой, как показано выше. Дальше .tpkg уходит в modules воркера Tsak, а маршрут при желании превращается в C# или в диаграмму:

dotnet build -p:PackRouteOnBuild=true -p:Version=1.0.0
redb-route-xml csharp routes/main.route.xml --namespace Orders.Routes
redb-route-xml mermaid routes/main.route.xml

Библиотека разметки это пакет redb.Route.Xml версии 4.0.1, расширение для VS Code прикладывается файлом .vsix к релизу redb-route на GitHub. Оба демо лежат в папке demo того же репозитория.

Если было полезно, ⭐ на GitHub поможет другим это найти.

Другие мои статьи — redb.ru/articles, ещё — на Хабре.

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