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-метод (To, Process, SetBody, Filter, Split, Transaction, ~40 штук) раньше копировался в каждый scope-класс. Теперь все они живут один раз в RouteDefinitionBase<TSelf>. Каждый метод возвращает TSelf — цепочка всегда остаётся на конкретном типе текущего scope. Публичный API и AST маршрута идентичны 3.0.0. Чистый внутренний рефакторинг — но именно он разблокировал плоскую навигацию выше.
Фикс: GetContext() тихо возвращал null внутри вложенных scope-ов
IRouteDefinition.GetContext() делал cast к RouteDefinition, который совпадал только с корнем маршрута. Внутри любого вложенного scope — When, Loop, Traced, Catch — метод возвращал null без исключения. Теперь он идёт вверх по Parent до owning-маршрута. Важно, если у вас есть extension methods, которые читают контекст на этапе построения DSL.
Полное демо со всеми четырьмя примерами — DeepDslShowcaseRoutes.cs. Подробный список изменений — в CHANGELOG.md. Всё под Apache 2.0.