Продолжаем серию про 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.

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