redb.Route 3.0.1 — плоская навигация по DSL, рефакторинг CRTP и тихий null

от автора

Продолжаем серию про redb.Route — вводная и разбор четырёх in-memory каналов уже вышли. Сегодня не статья серии, а релизная заметка: в 3.0.1 три конкретных изменения в DSL, каждое с боевым примером из демо.

До 3.0.1 глубоко вложенные scope-ы требовали закрывать себя в строго обратном порядке — утомительно и легко ошибиться. Три вещи изменились.


Плоская навигация через End*()

Typed-closer теперь идёт вверх по цепочке Parent и выходит на нужный уровень за один вызов:

From("direct://demo-cascade-endchoice")    .Choice()        .When(e => true)            .Split(e => new object?[] { 1, 2, 3 })                .Process(e => { /* работа с элементом */ })                .Log("item=${body}")            .EndChoice()   // проходит мимо Split → When → возвращается в корень маршрута    .SetHeader("post-cascade", "ok")    .Log("каскад завершён");

Универсальный .End() выходит к ближайшему scope, не называя его:

.Choice()    .When(e => true)        .Split(e => new object?[] { "a", "b" })            .Log(LogLevel.Information)                .Message("inside")            .End()   // закрывает RichLog → возвращает тело Split        .End()        // закрывает Split   → возвращает тело When    .EndChoice()      // закрывает Choice  → возвращает корень маршрута

Соседние ветки открываются естественно после закрытого sub-scope — .When() и .Otherwise() как extension methods находят ближайший ChoiceDefinition через Parent, поэтому это компилируется как есть:

.Choice()    .When(e => e.In.Body is IEnumerable<string> && e.In.Body is not string)        .Split(...)            .Process(...)        .EndSplit()        .Log("ветка list завершена [${routeId}]")   // всё ещё на теле When, не на Split    .When(e => e.In.Body is string s && s.Length > 0)   // ← соседняя ветка после EndSplit        .Process(e => { /* ... */ })    .Otherwise()        .Process(e => { /* fallback */ }).EndChoice()

Три формы логирования — в одном pipeline-шаге

Обновлённое демо показывает все три рядом — полезно понять, где что уместно:

// (A) Лямбда — произвольный C# в рантайме.Log(e => $"[lambda] item={e.In.Body} branch={e.In.Headers["branch"]}")// (B) Строковый шаблон — компилируется expression-движком,//     ноль аллокаций когда уровень логирования выключен.Log("[tmpl] item=${body} branch=${header.branch} [${routeId}]")// (C) Rich-log scope — структурированный, несколько сообщений,//     заголовки и свойства выводятся как отдельные поля.Log(LogLevel.Information)    .Message("[rich-tmpl]   item=${body}")    .Message(e => $"[rich-lambda] upper={((string)e.In.Body!).ToUpperInvariant()}")    .Header("branch")    .Property("item-index")    .ShowRouteId(true).EndLog()

${body}${header.x}${property.y}${routeId}${exception.type}${exception.message} — всё разрешается скомпилированным expression-движком (Tokenizer → Parser → AST → IL). Шаблон компилируется один раз, дальше исполняется как закэшированный делегат. .Message() в rich-log scope принимает обе формы одновременно.

То же самое работает внутри catch-блока:

.TryCatch()    .Process(e => throw new InvalidOperationException("boom")).DoCatch<InvalidOperationException>()    .Log("[tmpl] caught: ${exception.type} — ${exception.message} [${routeId}]")    .Log(LogLevel.Warning)        .Message(e => $"[lambda] stack: {e.Exception?.StackTrace?.Split('\n').First().Trim()}")        .ShowRouteId(true)    .EndLog().EndTryCatch()

CRTP-база — удалено 27 дублирующихся тел классов

Каждый leaf-метод (ToProcessSetBodyFilterSplitTransaction, ~40 штук) раньше копировался в каждый scope-класс. Теперь все они живут один раз в RouteDefinitionBase<TSelf>. Каждый метод возвращает TSelf — цепочка всегда остаётся на конкретном типе текущего scope. Публичный API и AST маршрута идентичны 3.0.0. Чистый внутренний рефакторинг — но именно он разблокировал плоскую навигацию выше.


Фикс: GetContext() тихо возвращал null внутри вложенных scope-ов

IRouteDefinition.GetContext() делал cast к RouteDefinition, который совпадал только с корнем маршрута. Внутри любого вложенного scope — WhenLoopTracedCatch — метод возвращал null без исключения. Теперь он идёт вверх по Parent до owning-маршрута. Важно, если у вас есть extension methods, которые читают контекст на этапе построения DSL.


Полное демо со всеми четырьмя примерами — DeepDslShowcaseRoutes.cs. Подробный список изменений — в CHANGELOG.md. Всё под Apache 2.0.

ссылка на оригинал статьи https://habr.com/ru/articles/1043332/