Series: redb ecosystem
Before 3.0.1, deep nested scopes required closing in exact reverse order — tedious and easy to get wrong. Three things changed.
Flat End*() navigation
A typed closer walks the Parent chain and exits to the right level in one call:
From("direct://demo-cascade-endchoice")
.Choice()
.When(e => true)
.Split(e => new object?[] { 1, 2, 3 })
.Process(e => { /* per-item work */ })
.Log("item=${body}")
.EndChoice() // walks past Split → When → lands at route root
.SetHeader("post-cascade", "ok")
.Log("cascade done");
Universal .End() exits to the nearest scope without naming it:
.Choice()
.When(e => true)
.Split(e => new object?[] { "a", "b" })
.Log(LogLevel.Information)
.Message("inside")
.End() // closes RichLog → returns Split body
.End() // closes Split → returns When body
.EndChoice() // closes Choice → returns route root
Sibling branches open naturally after a closed sub-scope — .When() and .Otherwise() walk up to the enclosing ChoiceDefinition via the Parent chain, so this compiles as-is:
.Choice()
.When(e => e.In.Body is IEnumerable<string> && e.In.Body is not string)
.Split(...)
.Process(...)
.EndSplit()
.Log("list branch done [${routeId}]") // still on the When body, not on Split
.When(e => e.In.Body is string s && s.Length > 0) // ← sibling, works after EndSplit
.Process(e => { /* ... */ })
.Otherwise()
.Process(e => { /* fallback */ })
.EndChoice()
Three logging styles, same pipeline step
The updated demo shows all three forms side by side — useful to see the tradeoffs:
// (A) Lambda — arbitrary C# at runtime
.Log(e => $"[lambda] item={e.In.Body} branch={e.In.Headers["branch"]}")
// (B) String template — compiled by the expression engine, zero alloc when level is off
.Log("[tmpl] item=${body} branch=${header.branch} [${routeId}]")
// (C) Rich-log scope — structured, multi-message, headers/properties as separate fields
.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} — all resolved by the compiled expression engine (Tokenizer → Parser → AST → IL). The template compiles once, runs as a cached delegate. .Message() in a rich-log scope accepts both forms simultaneously.
CRTP base — 27 duplicate class bodies removed
Every leaf method (To, Process, SetBody, Filter, Split, Transaction, ~40 more) now lives once in RouteDefinitionBase<TSelf>. Each returns TSelf — chaining stays on the concrete scope type throughout. Public API and route AST identical to 3.0.0.
Fix: GetContext() silently returned null inside nested scopes
IRouteDefinition.GetContext() was casting to RouteDefinition, which only matched the route root. Inside When, Loop, Traced, Catch — it returned null without throwing. Now walks Parent to the owning route. Matters if you have extension methods that read context at DSL build time.
Full demo: DeepDslShowcaseRoutes.cs · Changelog: 3.0.1 · Apache 2.0
Top comments (0)