<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: rinat kozin</title>
    <description>The latest articles on DEV Community by rinat kozin (@rinat_kozin).</description>
    <link>https://dev.to/rinat_kozin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3929984%2F9dcd2f94-0b1b-4161-9693-3e8c3f6ce385.jpg</url>
      <title>DEV Community: rinat kozin</title>
      <link>https://dev.to/rinat_kozin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rinat_kozin"/>
    <language>en</language>
    <item>
      <title>Two routes in an evening: from a debug worker to an enterprise runtime with redb.Route + Tsak</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Thu, 02 Jul 2026 22:31:42 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/two-routes-in-an-evening-from-a-debug-worker-to-an-enterprise-runtime-with-redbroute-tsak-m8k</link>
      <guid>https://dev.to/rinat_kozin/two-routes-in-an-evening-from-a-debug-worker-to-an-enterprise-runtime-with-redbroute-tsak-m8k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqupcc1i4pxfb6awdrht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqupcc1i4pxfb6awdrht.png" alt="redb.tsak" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Series:&lt;/strong&gt; redb ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integration code has a funny asymmetry to it.&lt;/p&gt;

&lt;p&gt;Writing a couple of routes — "take an HTTP request, stash it in a database, hand something back" — is a half-hour job. But getting that same thing to run in production, come up on its own, show you metrics, let you stop and start individual pieces by hand, and redeploy without a rebuild? That's usually a completely different stack and a completely different afternoon.&lt;/p&gt;

&lt;p&gt;This post is about how, with &lt;strong&gt;redb.Route + redb.Tsak&lt;/strong&gt;, it's literally the &lt;em&gt;same code&lt;/em&gt;. We'll:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;write &lt;strong&gt;two tiny routes&lt;/strong&gt; (POST to save, GET to fetch by a query param) on redb.Route;&lt;/li&gt;
&lt;li&gt;build &lt;strong&gt;our own debug worker&lt;/strong&gt; — a plain console app you can run under the debugger with breakpoints;&lt;/li&gt;
&lt;li&gt;and then, &lt;strong&gt;without changing a single line of the routes&lt;/strong&gt;, pack it into a module and drop it into Tsak — where the exact same project picks up a dashboard, hot-reload, live route management, and enterprise deployment (with Docker and without).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All the code is in the repo (&lt;code&gt;redb.Route/demos/EchoWorkerDemo&lt;/code&gt;), and I'm pasting it here in full so you can follow along.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part of the redb / redb.Route series&lt;/strong&gt; — recent posts first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/leaving-masstransit-for-a-camel-state-of-mind-the-kafka-connector-scatter-gather-and-what-really-106h"&gt;Leaving MassTransit for a Camel state of mind: the Kafka connector, Scatter-Gather, and transactions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/a-homegrown-apache-camel-for-net-dissected-the-http-connector-with-no-aspnet-mvc-the-56bd"&gt;Apache Camel for .NET, dissected: the HTTP connector with no ASP.NET MVC + the Content-Based Router&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/redbroute-apache-camel-for-net-22-transports-30-eip-patterns-compiled-dsl-11m0"&gt;redb.Route — Apache Camel for .NET: 22 transports, 30+ EIP patterns, compiled DSL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources: &lt;a href="https://github.com/redbase-app/" rel="noopener noreferrer"&gt;github.com/redbase-app&lt;/a&gt;. About the database itself: &lt;a href="https://redb.ru/" rel="noopener noreferrer"&gt;redb.ru&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This one is the gentle intro to workers and deployment. Clustering — the coordinator, leader election, failover — gets its own post.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A tiny notes service. Two endpoints on a single HTTP port:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /api/notes&lt;/code&gt; with a body like &lt;code&gt;{"tag":"work","text":"hello"}&lt;/code&gt; — save a note into a redb database (on SQLite);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/notes?tag=work&lt;/code&gt; — return notes with that tag; the lookup runs server-side via &lt;code&gt;Where(...).ToListAsync()&lt;/code&gt;, with the parameter pulled straight from the query string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing fancy. The point of the post isn't the business logic — it's the &lt;strong&gt;lifecycle&lt;/strong&gt;: how one and the same set of routes first lives in a debug console, and then in an enterprise runtime.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faid7oyddpfn2yuzm4vu0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faid7oyddpfn2yuzm4vu0.png" alt="the payoff shot — console with curl , the Tsak dashboard" width="799" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcproanrdlps7h0b6ky2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcproanrdlps7h0b6ky2e.png" alt="Tsak dashboard" width="800" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F74zdabpenkd6gi6s9e2b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F74zdabpenkd6gi6s9e2b.png" alt="Tsak dashboard" width="800" height="653"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Project layout: two projects, one entry point
&lt;/h2&gt;

&lt;p&gt;Here's the shape of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EchoWorkerDemo/
├─ EchoModule/            &amp;lt;- project 1: the module (class library -&amp;gt; EchoModule.tpkg)
│  ├─ InitRoute.cs        &amp;lt;- main(IRouteContext): two routes + the Note class
│  ├─ manifest.json       &amp;lt;- { Name, Version, EntryPoints: ["EchoModule.dll"] }
│  ├─ EchoModule.config.json  &amp;lt;- context name (ContextName) + AutoStart
│  └─ EchoModule.csproj   &amp;lt;- references to the connectors + the PackTpkg target
└─ EchoWorker/            &amp;lt;- project 2: the debug host (exe)
   ├─ Program.cs          &amp;lt;- redb on SQLite + a call to InitRoute.main + Start
   └─ EchoWorker.csproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole thing hinges on one idea: &lt;strong&gt;there is exactly one entry point — &lt;code&gt;InitRoute.main(IRouteContext)&lt;/code&gt;&lt;/strong&gt;. Both our debug worker and the real Tsak runtime call it. The route code lives in exactly one place — &lt;code&gt;EchoModule&lt;/code&gt;. The debug &lt;code&gt;EchoWorker&lt;/code&gt; is just the plumbing that stands up a database and calls that same &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why two projects and not one? You &lt;em&gt;can&lt;/em&gt; do one — an exe still produces a DLL, and Tsak finds &lt;code&gt;InitRoute.main&lt;/code&gt; by reflection in any assembly. But splitting the two roles makes them obvious: &lt;strong&gt;&lt;code&gt;EchoModule&lt;/code&gt; is what you ship&lt;/strong&gt; (it packs into a &lt;code&gt;.tpkg&lt;/code&gt;), and &lt;strong&gt;&lt;code&gt;EchoWorker&lt;/code&gt; is what you debug with&lt;/strong&gt;. The module carries zero host code — which is exactly right, because in production Tsak hands it the database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project 1: the module with two routes
&lt;/h2&gt;

&lt;p&gt;Here it is in full — &lt;code&gt;EchoModule/InitRoute.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                          &lt;span class="c1"&gt;// IRedbService, Query, SaveAsync, SyncSchemeAsync&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Core.Attributes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// RedbScheme&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Core.Models.Entities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// RedbObject&amp;lt;T&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Route.Abstractions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// IRouteContext, IExchange&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Route.Core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// RouteContext&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Route.Http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// HttpComponent, SharedHttpServerManager&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Route.RedbCore.Extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// ProcessWithRedb, GetRedbService&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;EchoModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Tsak module entry point.&lt;/span&gt;
&lt;span class="c1"&gt;/// The worker discovers it by convention — a public static class named InitRoute&lt;/span&gt;
&lt;span class="c1"&gt;/// with a public static main(IRouteContext) — and calls it once when the module&lt;/span&gt;
&lt;span class="c1"&gt;/// loads. The debug host (EchoWorker/Program.cs) calls the very same method, so the&lt;/span&gt;
&lt;span class="c1"&gt;/// route code below lives in exactly one place.&lt;/span&gt;
&lt;span class="c1"&gt;///&lt;/span&gt;
&lt;span class="c1"&gt;/// Two minimal endpoints on the shared HTTP server (port 5099), backed by redb/SQLite:&lt;/span&gt;
&lt;span class="c1"&gt;///   POST /api/notes   body {"tag":"work","text":"hello"}   -&amp;gt; save one note&lt;/span&gt;
&lt;span class="c1"&gt;///   GET  /api/notes?tag=work                               -&amp;gt; list notes with that tag&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InitRoute&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;JsonSerializerOptions&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;PropertyNameCaseInsensitive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IRouteContext&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IRouteContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// redb schema for Note. Idempotent — safe to call every load. The worker&lt;/span&gt;
        &lt;span class="c1"&gt;// (or the debug host) has already brought redb + SQLite up by now.&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRedbService&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;SyncSchemeAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetAwaiter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// One shared HTTP server; both routes below bind to it.&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ServerManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SharedHttpServerManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;RouteContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AddRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// --- POST /api/notes — save one note ---&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5099/api/notes?inOut=true&amp;amp;methods=POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notes-post"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                       &lt;span class="c1"&gt;// HTTP body -&amp;gt; string (JSON)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RedbObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"note:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// one insert into redb (SQLite)&lt;/span&gt;
                    &lt;span class="nf"&gt;Reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
                &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Save ${body}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// --- GET /api/notes?tag=work — list by tag ---&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5099/api/notes?inOut=true&amp;amp;methods=GET"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notes-get"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// ?tag=... arrives as the header redbHttp.QueryParam.tag&lt;/span&gt;
                    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.QueryParam.tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
                        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

                    &lt;span class="c1"&gt;// Server-side filter: the GET parameter goes straight into Where(...).&lt;/span&gt;
                    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

                    &lt;span class="nf"&gt;Reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
                &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Load ${header.redbHttp.QueryParam.tag}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// inOut=true -&amp;gt; whatever the body is at the end becomes the HTTP response.&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;Persisted note. [RedbScheme] marks the class as a redb schema.&amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;RedbScheme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Note&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Tag&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's walk through the parts that matter — nearly every line is pulling its weight here.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;main&lt;/code&gt; entry point
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IRouteContext&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IRouteContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;em&gt;is&lt;/em&gt; the Tsak module contract. No attributes, no interface to implement — just a &lt;strong&gt;naming convention&lt;/strong&gt;: a public static class &lt;code&gt;InitRoute&lt;/code&gt;, a public static method &lt;code&gt;main&lt;/code&gt; taking an &lt;code&gt;IRouteContext&lt;/code&gt;. When Tsak loads your assembly, it scans it by reflection, finds this method, and calls it with a context. Whatever you hang on that &lt;code&gt;context&lt;/code&gt; — components, routes, listeners — becomes part of the runtime.&lt;/p&gt;

&lt;p&gt;The neat bit: this works in the debug worker too. There, &lt;em&gt;we&lt;/em&gt; create the &lt;code&gt;RouteContext&lt;/code&gt; and &lt;em&gt;we&lt;/em&gt; call &lt;code&gt;InitRoute.main(ctx)&lt;/code&gt;. Same method, two hosts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The schema
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetRedbService&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;SyncSchemeAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetAwaiter&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetResult&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Note&lt;/code&gt; is tagged &lt;code&gt;[RedbScheme]&lt;/code&gt;, which marks it as a persistable redb class. &lt;code&gt;SyncSchemeAsync&amp;lt;Note&amp;gt;()&lt;/code&gt; registers its schema (idempotent — call it on every load, no harm done). &lt;code&gt;GetRedbService()&lt;/code&gt; pulls &lt;code&gt;IRedbService&lt;/code&gt; off the context: in Tsak the database is already up before your module runs, and in the debug worker we'll stand it up ourselves before &lt;code&gt;main&lt;/code&gt;. Either way, redb is there by the time we ask.&lt;/p&gt;

&lt;p&gt;Notice what the module &lt;em&gt;doesn't&lt;/em&gt; do: &lt;strong&gt;it never configures the database.&lt;/strong&gt; It doesn't know or care whether that's SQLite, Postgres, or MSSQL — it just asks the context for an &lt;code&gt;IRedbService&lt;/code&gt;. The provider is the host's problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  One HTTP server, two routes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ServerManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SharedHttpServerManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SharedHttpServerManager&lt;/code&gt; is a shared HTTP server — several routes can hang off one port. Both of ours sit on &lt;code&gt;5099&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5099/api/notes?inOut=true&amp;amp;methods=POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5099/api/notes?inOut=true&amp;amp;methods=GET"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both listen on the &lt;strong&gt;same path&lt;/strong&gt; &lt;code&gt;/api/notes&lt;/code&gt;, but they split on method via &lt;code&gt;?methods=POST&lt;/code&gt; / &lt;code&gt;?methods=GET&lt;/code&gt;. The server matches an incoming request on the pair "path + method": a POST goes to the first route, a GET to the second, and a &lt;code&gt;PUT /api/notes&lt;/code&gt; gets an honest &lt;strong&gt;405 Method Not Allowed&lt;/strong&gt; (path matched, method didn't). &lt;code&gt;inOut=true&lt;/code&gt; means request/response: whatever ends up in the exchange body at the end of the route becomes the HTTP response.&lt;/p&gt;

&lt;h3&gt;
  
  
  POST: save
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RedbObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"note:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Save ${body}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ConvertBody&amp;lt;string&amp;gt;()&lt;/code&gt; turns the raw HTTP body (bytes) into a string. &lt;code&gt;ProcessWithRedb&lt;/code&gt; is a processing step into which redb.Route injects the &lt;code&gt;IRedbService&lt;/code&gt; (&lt;code&gt;db&lt;/code&gt;) for you: under the hood it grabs the per-exchange DI scope if there is one, otherwise the context singleton. The rest is plain C#: deserialize the JSON into a &lt;code&gt;Note&lt;/code&gt;, wrap it in a &lt;code&gt;RedbObject&amp;lt;Note&amp;gt;&lt;/code&gt;, save it with a single &lt;code&gt;SaveAsync&lt;/code&gt;, hand back &lt;code&gt;{ saved, id }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trailing &lt;code&gt;.Log("Save ${body}")&lt;/code&gt; is a log step with interpolation: &lt;code&gt;${body}&lt;/code&gt; substitutes the current exchange body. redb.Route's &lt;code&gt;${...}&lt;/code&gt; can pull &lt;code&gt;body&lt;/code&gt;, headers (&lt;code&gt;${header.X}&lt;/code&gt;), and more — handy for tracing right inside the DSL.&lt;/p&gt;

&lt;h3&gt;
  
  
  GET: fetch by param
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.QueryParam.tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;Reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;redb.Route lays out HTTP query params as headers prefixed with &lt;code&gt;redbHttp.QueryParam.&lt;/code&gt;, so &lt;code&gt;?tag=work&lt;/code&gt; shows up as the header &lt;code&gt;redbHttp.QueryParam.tag&lt;/code&gt;. Grab it, drop it straight into a server-side query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tag&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not "load everything into memory, then filter" — it's a query on the database side: the lambda &lt;code&gt;n =&amp;gt; n.Tag == tag&lt;/code&gt; compiles to a condition on the &lt;code&gt;Note.Tag&lt;/code&gt; field. The result is a collection of &lt;code&gt;RedbObject&amp;lt;Note&amp;gt;&lt;/code&gt;, so we reach the fields through &lt;code&gt;.Props&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And that's the whole module. Two routes, a data class, zero infrastructure. Now let's run and debug it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project 2: our own debug worker
&lt;/h2&gt;

&lt;p&gt;To run the routes under a debugger without spinning up any Tsak at all, we'll build a tiny console app. It reproduces exactly what the Tsak worker does when it loads a module, just in the smallest possible way. Here's &lt;code&gt;EchoWorker/Program.cs&lt;/code&gt; in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ============================================================================&lt;/span&gt;
&lt;span class="c1"&gt;//  EchoWorker — a debug host for the EchoModule Tsak module.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;//  It reproduces, in the smallest possible way, what the Tsak worker does:&lt;/span&gt;
&lt;span class="c1"&gt;//    1) stand redb up on SQLite (Free tier — the worker's default),&lt;/span&gt;
&lt;span class="c1"&gt;//    2) create the redb system tables once,&lt;/span&gt;
&lt;span class="c1"&gt;//    3) hand a RouteContext to EchoModule.InitRoute.main — the SAME entry point&lt;/span&gt;
&lt;span class="c1"&gt;//       the worker calls, so no route code is duplicated here.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;//  Run it, then (PowerShell — JSON in single quotes; cmd.exe needs \" escaping instead):&lt;/span&gt;
&lt;span class="c1"&gt;//    POST: curl.exe -X POST http://localhost:5099/api/notes -H "Content-Type: application/json" -d '{"tag":"work","text":"hello"}'&lt;/span&gt;
&lt;span class="c1"&gt;//    GET:  curl.exe "http://localhost:5099/api/notes?tag=work"&lt;/span&gt;
&lt;span class="c1"&gt;// ============================================================================&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.DependencyInjection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                          &lt;span class="c1"&gt;// IRedbService&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Core.Extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// AddRedb&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Core.Models.Configuration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// PropsSaveStrategy&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.SQLite.Pro.Extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// UseSqlite (tier-agnostic: AddRedb -&amp;gt; Free)&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.SQLite.Data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                   &lt;span class="c1"&gt;// SqliteDataSource.NativeExtensionPath&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Route.Core&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// RouteContext&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;EchoWorker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Free SQLite needs the native redb extension. The packaged Tsak worker ships&lt;/span&gt;
        &lt;span class="c1"&gt;// it; running from source we point at the one built under redb.SQLite/native/build.&lt;/span&gt;
        &lt;span class="n"&gt;SqliteDataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NativeExtensionPath&lt;/span&gt; &lt;span class="p"&gt;??=&lt;/span&gt; &lt;span class="nf"&gt;ResolveNativeExtension&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// DI: console logging + redb on SQLite (single-file DB next to the exe).&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ServiceCollection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLogging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSimpleConsole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SingleLine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimestampFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"HH:mm:ss "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetMinimumLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRedb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Data Source=echo_demo.db"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropsSaveStrategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PropsSaveStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeleteInsert&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildServiceProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Create the redb system tables once (the worker does this on boot).&lt;/span&gt;
        &lt;span class="c1"&gt;// ensureCreated: true builds the base tables on a fresh SQLite file.&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IRedbService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ensureCreated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Build a route context over that provider and call the module entry point.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RouteContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"echo-worker"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILoggerFactory&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILoggerFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
        &lt;span class="n"&gt;EchoModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// &amp;lt;- the exact method the Tsak worker calls&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EchoWorker running: http://localhost:5099/api/notes"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  POST  {\"tag\":\"work\",\"text\":\"hello\"}   -&amp;gt; save"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"  GET   ?tag=work                          -&amp;gt; list by tag"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ctrl+C to exit."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ManualResetEventSlim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancelKeyPress&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Walk up from the app dir to the repo's built Free SQLite native extension.&lt;/span&gt;
    &lt;span class="c1"&gt;// Returns null when running from a packaged worker (it resolves the extension itself).&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;ResolveNativeExtension&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OperatingSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsWindows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;".dll"&lt;/span&gt;
                   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OperatingSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsMacOS&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;".dylib"&lt;/span&gt;
                   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;".so"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DirectoryInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseDirectory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"redb.SQLite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"native"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"redb"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step by step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The SQLite native extension.&lt;/strong&gt; On its Free tier, redb-on-SQLite uses a native loadable extension (&lt;code&gt;redb.dll&lt;/code&gt; / &lt;code&gt;.so&lt;/code&gt; / &lt;code&gt;.dylib&lt;/code&gt;) — part of the query machinery lives in there. The packaged Tsak worker ships that extension in the box. When you run from source, you have to point at the built binary — &lt;code&gt;ResolveNativeExtension()&lt;/code&gt; just walks up the directory tree looking for &lt;code&gt;redb.SQLite/native/build/redb.dll&lt;/code&gt;. One honest detail worth not hiding: without that extension, the Free SQLite provider won't start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. DI and the database.&lt;/strong&gt; A regular &lt;code&gt;ServiceCollection&lt;/code&gt;: console logging plus &lt;code&gt;AddRedb(...).UseSqlite("Data Source=echo_demo.db")&lt;/code&gt;. &lt;code&gt;UseSqlite&lt;/code&gt; is tier-agnostic: &lt;code&gt;AddRedb&lt;/code&gt; gives you Free, &lt;code&gt;AddRedbPro&lt;/code&gt; gives you Pro — you flip tiers without touching the &lt;code&gt;using&lt;/code&gt;s. And this is the exact same tier (Free/SQLite) the Tsak worker runs by default, so your debug environment matches production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. System tables.&lt;/strong&gt; &lt;code&gt;InitializeAsync(ensureCreated: true)&lt;/code&gt; creates redb's base tables on a fresh file. In Tsak the worker does this on boot; in the debug host, we do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The context and the module call.&lt;/strong&gt; We build a &lt;code&gt;RouteContext&lt;/code&gt; over our provider and call &lt;code&gt;EchoModule.InitRoute.main(ctx)&lt;/code&gt;. &lt;strong&gt;That's the exact entry point Tsak will call.&lt;/strong&gt; No route code is duplicated — it all lives in the module.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Start and wait.&lt;/strong&gt; &lt;code&gt;ctx.Start()&lt;/code&gt; brings up the HTTP server on 5099; then we sit until Ctrl+C.&lt;/p&gt;

&lt;h3&gt;
  
  
  The project files
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;EchoModule/EchoModule.csproj&lt;/code&gt; — a library plus a &lt;code&gt;.tpkg&lt;/code&gt; packaging target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Project 1 of 2: the Tsak MODULE.
       A class library. Its DLL is what gets packed into EchoModule.tpkg and
       hot-loaded by the Tsak worker. It contains ONLY route code — no host,
       no DB provider: the worker supplies redb + SQLite at runtime. --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net9.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RootNamespace&amp;gt;&lt;/span&gt;EchoModule&lt;span class="nt"&gt;&amp;lt;/RootNamespace&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;CopyLocalLockFileAssemblies&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/CopyLocalLockFileAssemblies&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- The connectors the worker already ships in its shared Libs. We compile
       against them but the .tpkg carries only EchoModule.dll (see PackTpkg). --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\..\..\src\redb.Route\redb.Route.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\..\..\src\redb.Route.Core\redb.Route.Core.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\..\..\src\redb.Route.Http\redb.Route.Http.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\..\..\..\redb.Core\redb.Core.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- After build: zip manifest.json + the module DLL into output/EchoModule.tpkg. --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TsakModuleName&amp;gt;&lt;/span&gt;EchoModule&lt;span class="nt"&gt;&amp;lt;/TsakModuleName&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Target&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"PackTpkg"&lt;/span&gt; &lt;span class="na"&gt;AfterTargets=&lt;/span&gt;&lt;span class="s"&gt;"Build"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;_TpkgStaging&amp;gt;&lt;/span&gt;$(IntermediateOutputPath)tpkg&lt;span class="nt"&gt;&amp;lt;/_TpkgStaging&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;_TpkgFile&amp;gt;&lt;/span&gt;$(MSBuildThisFileDirectory)output\$(TsakModuleName).tpkg&lt;span class="nt"&gt;&amp;lt;/_TpkgFile&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RemoveDir&lt;/span&gt; &lt;span class="na"&gt;Directories=&lt;/span&gt;&lt;span class="s"&gt;"$(_TpkgStaging)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;MakeDir&lt;/span&gt; &lt;span class="na"&gt;Directories=&lt;/span&gt;&lt;span class="s"&gt;"$(_TpkgStaging)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;MakeDir&lt;/span&gt; &lt;span class="na"&gt;Directories=&lt;/span&gt;&lt;span class="s"&gt;"$(MSBuildThisFileDirectory)output"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Copy&lt;/span&gt; &lt;span class="na"&gt;SourceFiles=&lt;/span&gt;&lt;span class="s"&gt;"$(MSBuildThisFileDirectory)manifest.json"&lt;/span&gt; &lt;span class="na"&gt;DestinationFolder=&lt;/span&gt;&lt;span class="s"&gt;"$(_TpkgStaging)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Copy&lt;/span&gt; &lt;span class="na"&gt;SourceFiles=&lt;/span&gt;&lt;span class="s"&gt;"$(TargetPath)"&lt;/span&gt; &lt;span class="na"&gt;DestinationFolder=&lt;/span&gt;&lt;span class="s"&gt;"$(_TpkgStaging)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- {ModuleName}.config.json gives the module a named context (ContextName). --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Copy&lt;/span&gt; &lt;span class="na"&gt;SourceFiles=&lt;/span&gt;&lt;span class="s"&gt;"$(MSBuildThisFileDirectory)$(TsakModuleName).config.json"&lt;/span&gt;
          &lt;span class="na"&gt;DestinationFolder=&lt;/span&gt;&lt;span class="s"&gt;"$(_TpkgStaging)"&lt;/span&gt;
          &lt;span class="na"&gt;Condition=&lt;/span&gt;&lt;span class="s"&gt;"Exists('$(MSBuildThisFileDirectory)$(TsakModuleName).config.json')"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ZipDirectory&lt;/span&gt; &lt;span class="na"&gt;SourceDirectory=&lt;/span&gt;&lt;span class="s"&gt;"$(_TpkgStaging)"&lt;/span&gt; &lt;span class="na"&gt;DestinationFile=&lt;/span&gt;&lt;span class="s"&gt;"$(_TpkgFile)"&lt;/span&gt; &lt;span class="na"&gt;Overwrite=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Message&lt;/span&gt; &lt;span class="na"&gt;Importance=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt; &lt;span class="na"&gt;Text=&lt;/span&gt;&lt;span class="s"&gt;"Packed $(TsakModuleName) -&amp;gt; $(_TpkgFile)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Target&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EchoWorker/EchoWorker.csproj&lt;/code&gt; — a console exe that references the module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Project 2 of 2: the DEBUG HOST.
       A tiny console app. It stands redb up on SQLite (the same Free tier the Tsak
       worker uses by default), then calls EchoModule.InitRoute.main(ctx) — the exact
       method the worker calls. Run/F5 this to debug the route without Tsak. --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Exe&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net9.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RootNamespace&amp;gt;&lt;/span&gt;EchoWorker&lt;span class="nt"&gt;&amp;lt;/RootNamespace&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\EchoModule\EchoModule.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- UseSqlite is tier-agnostic: AddRedb -&amp;gt; Free (what Tsak uses by default). --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\..\..\..\redb.SQLite.Pro\redb.SQLite.Pro.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Extensions.Logging.Console"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"10.0.3"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's an important detail in the dependencies. The debug host references the SQLite provider (&lt;code&gt;redb.SQLite.Pro&lt;/code&gt;) — because &lt;em&gt;it&lt;/em&gt; stands up the database. The module itself does &lt;strong&gt;not&lt;/strong&gt; reference any DB provider: in Tsak, the worker provides the database. So the &lt;code&gt;.tpkg&lt;/code&gt; only ships &lt;code&gt;EchoModule.dll&lt;/code&gt; (see the &lt;code&gt;PackTpkg&lt;/code&gt; target — it puts exactly the target DLL, the manifest, and the config into the archive, not the dependencies). The &lt;code&gt;redb.Route.*&lt;/code&gt; and &lt;code&gt;redb.Core&lt;/code&gt; connectors are already in the worker, which is why the package stays tiny.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run it and poke it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet run --project EchoWorker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in another window, hit the endpoints. One Windows wrinkle worth calling out: escaping JSON for &lt;code&gt;curl&lt;/code&gt; &lt;strong&gt;differs between cmd and PowerShell&lt;/strong&gt;. In PowerShell the &lt;code&gt;\"&lt;/code&gt; form does &lt;em&gt;not&lt;/em&gt; work — you'll get &lt;code&gt;'\' is an invalid start of a property name&lt;/code&gt;. So here's both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cmd.exe&lt;/strong&gt; (JSON in double quotes, inner ones escaped with &lt;code&gt;\"&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -X POST http://localhost:5099/api/notes -H "Content-Type: application/json" -d "{\"tag\":\"work\",\"text\":\"hello\"}"
curl "http://localhost:5099/api/notes?tag=work"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PowerShell&lt;/strong&gt; (wrap the JSON in single quotes — no escaping of the inner double quotes; and it must be &lt;code&gt;curl.exe&lt;/code&gt;, since &lt;code&gt;curl&lt;/code&gt; is an alias for &lt;code&gt;Invoke-WebRequest&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl.exe -X POST http://localhost:5099/api/notes -H "Content-Type: application/json" -d '{"tag":"work","text":"hello"}'
curl.exe -X POST http://localhost:5099/api/notes -H "Content-Type: application/json" -d '{"tag":"home","text":"other"}'
curl.exe "http://localhost:5099/api/notes?tag=work"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PowerShell, no curl&lt;/strong&gt; (native):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invoke-RestMethod -Method Post -Uri http://localhost:5099/api/notes -ContentType 'application/json' -Body '{"tag":"work","text":"hello"}'
Invoke-RestMethod "http://localhost:5099/api/notes?tag=work"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"saved":true,"id":1000019}
{"saved":true,"id":1000022}
[{"Tag":"work","Text":"hello"}]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;GET&lt;/code&gt; with &lt;code&gt;?tag=home&lt;/code&gt; returns the other note, and &lt;code&gt;PUT /api/notes&lt;/code&gt; gives you a 405. It all works, and it's all under the debugger: drop a breakpoint inside &lt;code&gt;ProcessWithRedb&lt;/code&gt;, fire a curl, and you're sitting right in the handler with a live &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;ex&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the &lt;strong&gt;debug zone&lt;/strong&gt;: a plain console app, F5, breakpoints, a live SQLite file next to the exe, raw logs to stdout. This is where you &lt;em&gt;write and debug&lt;/em&gt; your routes. Iteration loop: "tweak → run → poke with curl."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftr7wzjyacht721wfzox8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftr7wzjyacht721wfzox8.png" alt="the EchoWorker console — Save/Load logs — next to a curl session with the POST/GET responses" width="799" height="349"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Here's the trick: the same code, now enterprise
&lt;/h2&gt;

&lt;p&gt;Now for the good part. &lt;strong&gt;We don't touch the routes at all.&lt;/strong&gt; We take the same &lt;code&gt;EchoModule.dll&lt;/code&gt;, add two tiny descriptor files, pack it into a &lt;code&gt;.tpkg&lt;/code&gt;, and hand it to Tsak. One step — and our "console app under F5" gains a dashboard, metrics, hot-reload, and live management. New code written: zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a &lt;code&gt;.tpkg&lt;/code&gt; is
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;.tpkg&lt;/code&gt; is just a ZIP with three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;manifest.json&lt;/code&gt; — the module's passport;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EchoModule.dll&lt;/code&gt; — the assembly with &lt;code&gt;InitRoute.main&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EchoModule.config.json&lt;/code&gt; — the module's config (first and foremost, its context name).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;manifest.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EchoModule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EntryPoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"EchoModule.dll"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EchoModule.config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ContextName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"echo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AutoStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A word on &lt;code&gt;ContextName&lt;/code&gt;, because it's an easy trap. If you don't set it, Tsak loads the module into an &lt;strong&gt;anonymous&lt;/strong&gt; context with an auto-generated name (something like &lt;code&gt;EchoModule_dyn_&amp;lt;date&amp;gt;_&amp;lt;guid&amp;gt;&lt;/code&gt;). The module runs fine, the routes work — but the dashboard's Endpoints page hides anonymous contexts by default (while still counting them in the totals, so the numbers won't match what you see in the list). The takeaway is simple: &lt;strong&gt;name your context&lt;/strong&gt; via &lt;code&gt;EchoModule.config.json&lt;/code&gt;, and it shows up neatly as &lt;code&gt;echo&lt;/code&gt;. Small thing, big peace of mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pack it
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;PackTpkg&lt;/code&gt; target in the &lt;code&gt;.csproj&lt;/code&gt; does everything at build time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet build EchoModule -c Debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out comes &lt;code&gt;EchoModule/output/EchoModule.tpkg&lt;/code&gt;. Peek inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Archive:  output/EchoModule.tpkg
  Length      Name
---------  ----
       49  EchoModule.config.json
    13312  EchoModule.dll
      108  manifest.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three files, ~7 KB. That's your deployment artifact.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Tsak finds modules: folders and hot-reload
&lt;/h2&gt;

&lt;p&gt;This is the part the whole thing was built for. Tsak doesn't "watch" the folder with filesystem events — internally there's a background &lt;code&gt;HotReloadService&lt;/code&gt; that &lt;strong&gt;polls the directories&lt;/strong&gt; listed in the &lt;code&gt;Tsak:Modules:AssemblyPaths&lt;/code&gt; config on a timer.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The worker config has a list of paths, &lt;code&gt;Tsak:Modules:AssemblyPaths&lt;/code&gt;. The module folder is usually called &lt;code&gt;modules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;Tsak:HotReload:ScanIntervalSeconds&lt;/code&gt; (default &lt;strong&gt;10 seconds&lt;/strong&gt;) the service walks each path and grabs every &lt;code&gt;*.tpkg&lt;/code&gt; (and bare &lt;code&gt;*.dll&lt;/code&gt; too).&lt;/li&gt;
&lt;li&gt;"New / changed / removed" is decided by &lt;strong&gt;file modification time&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;unchanged mtime → skip;&lt;/li&gt;
&lt;li&gt;new file → unzip it, load the assembly into an &lt;strong&gt;isolated load context&lt;/strong&gt; (its own &lt;code&gt;AssemblyLoadContext&lt;/code&gt; per package), find &lt;code&gt;InitRoute.main&lt;/code&gt; by reflection, bring the context up;&lt;/li&gt;
&lt;li&gt;changed file → reload the package atomically (drop the old version, load the new one, preserving state such as whether the context was running);&lt;/li&gt;
&lt;li&gt;file gone from disk → unload every module in that package.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Shared connectors (&lt;code&gt;redb.Route.*&lt;/code&gt;, &lt;code&gt;redb.Core&lt;/code&gt;) resolve from the worker's shared libraries (&lt;code&gt;Libs/shared&lt;/code&gt;), which is why they're not inside the &lt;code&gt;.tpkg&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical upshot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drop or update &lt;code&gt;EchoModule.tpkg&lt;/code&gt; in &lt;code&gt;modules/&lt;/code&gt; and it gets picked up &lt;strong&gt;within ~10 seconds&lt;/strong&gt;, no restart.&lt;/li&gt;
&lt;li&gt;The trigger is the file's mtime. A plain copy bumps the mtime, so "re-copied" = "reloaded."&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;.tpkg&lt;/code&gt; lives in its own load context — module versions are isolated from each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, deploying a new module (or a new version) is &lt;strong&gt;copying one file into a folder&lt;/strong&gt;. Tsak takes it from there.&lt;/p&gt;




&lt;h2&gt;
  
  
  The dashboard: watch it, and run it
&lt;/h2&gt;

&lt;p&gt;Stand Tsak up (two ways below), drop &lt;code&gt;EchoModule.tpkg&lt;/code&gt; into the modules folder, wait a few seconds, and open the web dashboard. Our &lt;code&gt;echo&lt;/code&gt; context is right there, with its two endpoints.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqu3yzgwlv9esedo7x443.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqu3yzgwlv9esedo7x443.png" alt="Tsak dashboard — the Endpoints page, the echo context with routes notes-post and notes-get" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's the second big difference from the debug console. In the console we only had text logs. In Tsak it's the same information, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you see the &lt;code&gt;echo&lt;/code&gt; context and its endpoints — type (HTTP), role (Consumer), in/out counters, errors, throughput, uptime, health;&lt;/li&gt;
&lt;li&gt;you see the &lt;code&gt;_SYSTEM&lt;/code&gt; context — that's the worker's own management API;&lt;/li&gt;
&lt;li&gt;you can drill into each endpoint for details (messages, bytes, average processing time, last errors).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwlcngmig2f27myqnu0l4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwlcngmig2f27myqnu0l4.png" alt="Tsak — the notes-get endpoint card with metrics and details" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But the real point is that you can &lt;strong&gt;operate it, right from the browser&lt;/strong&gt;, without touching configs or redeploying. On the node detail page for our &lt;code&gt;echo&lt;/code&gt; context you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the context as a whole&lt;/strong&gt; — Start / Stop / Restart, plus Reset route states (clear the saved route states);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;individual routes&lt;/strong&gt; — stop/start a specific &lt;code&gt;notes-post&lt;/code&gt; or &lt;code&gt;notes-get&lt;/code&gt; without touching its neighbor;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;schedules&lt;/strong&gt; (if the module has Quartz jobs) — pause and resume jobs on the scheduler tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here's the important bit: &lt;strong&gt;state sticks.&lt;/strong&gt; If you manually stop a route, then hot-reload will &lt;strong&gt;not&lt;/strong&gt; bring it back up when the package updates — Tsak respects the operator's decision. A route you stopped in production won't suddenly come back to life just because you rolled out a new &lt;code&gt;.tpkg&lt;/code&gt; version. Management is both "hot" (takes effect immediately) and "sticky" (survives a module reload).&lt;/p&gt;

&lt;p&gt;The management actions live behind the admin role — so this is genuinely an operator's console, not just a metrics wall.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjhi8bap5b9ne6hhrlw5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjhi8bap5b9ne6hhrlw5.png" alt="Tsak — the node/context detail page with Start/Stop/Restart buttons on the notes-post / notes-get routes" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel the contrast: in the custom worker you &lt;strong&gt;debug&lt;/strong&gt; (F5, breakpoints, a bare console); in Tsak you &lt;strong&gt;operate&lt;/strong&gt; (observe, manage, deploy). Same code, two environments, two very different jobs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl1lqq544bzwwrmx8qtul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl1lqq544bzwwrmx8qtul.png" alt="two panels side by side — EchoWorker console on the left, the Tsak dashboard with the echo context on the right" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Ports: defaults, and how to set them
&lt;/h2&gt;

&lt;p&gt;Since we're running this across different environments, let's put the ports on the table — it's easy to get lost in what lives where.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The module (our routes) — &lt;code&gt;5099&lt;/code&gt;.&lt;/strong&gt; That's the port we wrote into &lt;code&gt;From("http:0.0.0.0:5099/...")&lt;/code&gt;. The module's own HTTP server. Want a different one? Change it in the route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The worker (Tsak management API) — &lt;code&gt;9090&lt;/code&gt;.&lt;/strong&gt; A separate server: health, metrics, context management, the cluster API. This is what the dashboard talks to. The port comes from the worker config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The web dashboard — pay attention here.&lt;/strong&gt; If neither &lt;code&gt;ASPNETCORE_URLS&lt;/code&gt; nor a &lt;code&gt;Kestrel&lt;/code&gt; binding is set in the web config, ASP.NET Core falls back to its &lt;strong&gt;default — &lt;code&gt;http://localhost:5000&lt;/code&gt;&lt;/strong&gt;. The Docker images and the distribution's startup scripts set &lt;code&gt;ASPNETCORE_URLS=http://localhost:8080&lt;/code&gt; (or &lt;code&gt;http://+:8080&lt;/code&gt; in a container), which is why the dashboard is on &lt;strong&gt;8080&lt;/strong&gt; under Docker and in the docs. Run the web "bare" without that variable and don't be surprised to find the UI on &lt;strong&gt;5000&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To set it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# before launching the web&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;ASPNETCORE_URLS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8080"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or in the web's &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Kestrel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Endpoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Http"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:8080"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheat sheet: &lt;strong&gt;9090&lt;/strong&gt; — worker API, &lt;strong&gt;5099&lt;/strong&gt; — our endpoints, &lt;strong&gt;8080&lt;/strong&gt; — dashboard (when &lt;code&gt;ASPNETCORE_URLS&lt;/code&gt; is set; otherwise the framework default &lt;strong&gt;5000&lt;/strong&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Enterprise deploy #1: Docker (the whole stack in one container)
&lt;/h2&gt;

&lt;p&gt;The simplest way to bring it all up at once is the &lt;code&gt;redb-tsak-stack&lt;/code&gt; image: worker + dashboard in a single container. The images live on the &lt;a href="https://github.com/orgs/redbase-app/packages?repo_name=redb-tsak" rel="noopener noreferrer"&gt;org's packages page&lt;/a&gt; — there are several: &lt;code&gt;redb-tsak-worker&lt;/code&gt;, &lt;code&gt;redb-tsak-web&lt;/code&gt;, and the combined &lt;code&gt;redb-tsak-stack&lt;/code&gt; (plus per-TFM variants). For a quick start, &lt;code&gt;stack&lt;/code&gt; is all we need. Here's the &lt;code&gt;docker-compose.yml&lt;/code&gt; in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Minimal redb.Tsak stack — Worker (API) + Blazor dashboard in one container.&lt;/span&gt;
&lt;span class="c1"&gt;# Drop a module as a .tpkg into ./modules and the worker hot-loads it.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Start:&lt;/span&gt;
&lt;span class="c1"&gt;#   docker compose up -d&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Dashboard:   http://localhost:8080   (login admin / admin)&lt;/span&gt;
&lt;span class="c1"&gt;# Tsak API:    http://localhost:9090/api/health/live&lt;/span&gt;
&lt;span class="c1"&gt;# EchoModule endpoints:  http://localhost:5099/api/notes&lt;/span&gt;
&lt;span class="c1"&gt;#   POST (PowerShell): curl.exe -X POST http://localhost:5099/api/notes -H "Content-Type: application/json" -d '{"tag":"work","text":"hello"}'&lt;/span&gt;
&lt;span class="c1"&gt;#   POST (cmd.exe):    curl -X POST http://localhost:5099/api/notes -H "Content-Type: application/json" -d "{\"tag\":\"work\",\"text\":\"hello\"}"&lt;/span&gt;
&lt;span class="c1"&gt;#   GET:  curl.exe "http://localhost:5099/api/notes?tag=work"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tsak&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/redbase-app/redb-tsak-stack:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tsak&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;   &lt;span class="c1"&gt;# Blazor dashboard&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9090:9090"&lt;/span&gt;   &lt;span class="c1"&gt;# Tsak management REST API (health, cluster, contexts...)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5099:5099"&lt;/span&gt;   &lt;span class="c1"&gt;# the EchoModule's own HTTP server (/api/notes)&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# redb store on the bundled SQLite — no external dependencies&lt;/span&gt;
      &lt;span class="na"&gt;Tsak__Storage__Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Redb&lt;/span&gt;
      &lt;span class="c1"&gt;# HMAC secret for API-key auth (any &amp;gt;=16 chars; change for real use)&lt;/span&gt;
      &lt;span class="na"&gt;Tsak__Auth__Secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo-secret-change-me-please-0123456789"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# your .tpkg modules — hot-loaded by the worker&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./modules:/app/worker/modules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The image&lt;/strong&gt; &lt;code&gt;ghcr.io/redbase-app/redb-tsak-stack:latest&lt;/code&gt; — worker and dashboard in one container (under a supervisor). Storage defaults to the bundled SQLite, no external dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ports&lt;/strong&gt; — the same three: &lt;code&gt;8080&lt;/code&gt; (dashboard), &lt;code&gt;9090&lt;/code&gt; (worker API), &lt;code&gt;5099&lt;/code&gt; (our endpoints).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The volume&lt;/strong&gt; &lt;code&gt;./modules:/app/worker/modules&lt;/code&gt; — this is where the &lt;code&gt;.tpkg&lt;/code&gt; goes. Inside the stack image the module folder is &lt;code&gt;/app/worker/modules&lt;/code&gt;, which is exactly the path hot-reload watches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Tsak__Auth__Secret&lt;/code&gt;&lt;/strong&gt; — the HMAC secret for signing API keys; for a local run any value of 16+ chars will do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Layout on the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tsak/
├─ docker-compose.yml
└─ modules/
   └─ EchoModule.tpkg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dashboard at &lt;code&gt;http://localhost:8080&lt;/code&gt; (admin / admin). Test the endpoints (PowerShell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl.exe -X POST http://localhost:5099/api/notes -H "Content-Type: application/json" -d '{"tag":"work","text":"hello"}'
curl.exe "http://localhost:5099/api/notes?tag=work"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The same module we debugged in a console now runs in a container with a dashboard. To update the module: rebuild the &lt;code&gt;.tpkg&lt;/code&gt;, re-copy it into &lt;code&gt;modules/&lt;/code&gt;, and it gets picked up in ~10 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2161tv4kqfkamuopgpa0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2161tv4kqfkamuopgpa0.png" alt="docker conteiner redb.tsak" width="800" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagwq88jdhlwsedffiy4m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagwq88jdhlwsedffiy4m.png" alt="docker conteiner redb.tsak" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Enterprise deploy #2: no container (standalone archive)
&lt;/h2&gt;

&lt;p&gt;Don't want Docker? Fine. The &lt;a href="https://github.com/redbase-app/redb-tsak/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt; has self-contained archives per platform — for example, the &lt;a href="https://github.com/redbase-app/redb-tsak/releases/tag/v3.2.0" rel="noopener noreferrer"&gt;v3.2.0&lt;/a&gt; tag ships archives for &lt;code&gt;win-x64&lt;/code&gt;, &lt;code&gt;linux-x64&lt;/code&gt;, and so on (each is cosign-signed, with a &lt;code&gt;.bundle&lt;/code&gt; and an SBOM alongside for integrity checks).&lt;/p&gt;

&lt;p&gt;Inside an extracted archive (&lt;code&gt;redb-tsak-&amp;lt;version&amp;gt;-&amp;lt;platform&amp;gt;&lt;/code&gt;), the layout is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;redb-tsak-3.2.0-win-x64/
├─ worker/     &amp;lt;- the runtime: redb.Tsak.Worker.exe (+ Libs/shared with connectors, + modules/)
├─ web/        &amp;lt;- the dashboard: redb.Tsak.Web.exe
├─ cli/        &amp;lt;- the tsak command-line tool
├─ scripts/    &amp;lt;- start-worker / start-web / start-stack (.bat / .ps1 / .sh)
├─ README.txt
├─ LICENSE, NOTICE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launch from &lt;code&gt;scripts/&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;start-stack&lt;/code&gt; — bring up worker and dashboard together;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start-worker&lt;/code&gt; — just the runtime;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start-web&lt;/code&gt; — just the dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Windows, for instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\scripts\start-stack.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same ports: worker on &lt;code&gt;9090&lt;/code&gt;, dashboard on &lt;code&gt;8080&lt;/code&gt; (the scripts set &lt;code&gt;ASPNETCORE_URLS&lt;/code&gt;; run &lt;code&gt;web&lt;/code&gt; completely bare, outside the script, and remember the &lt;code&gt;5000&lt;/code&gt; default from the ports section).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the module goes.&lt;/strong&gt; In the standalone layout, modules live next to the worker — in its &lt;code&gt;modules/&lt;/code&gt; folder (the path comes from &lt;code&gt;Tsak:Modules:AssemblyPaths&lt;/code&gt;). Drop our &lt;code&gt;EchoModule.tpkg&lt;/code&gt; there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;worker/
└─ modules/
   └─ EchoModule.tpkg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave &lt;code&gt;Libs/shared&lt;/code&gt; alone — that's the distribution's shared connectors, the layer your lightweight package loads on top of.&lt;/p&gt;

&lt;p&gt;Licensing: with no key, the runtime starts in OSS mode. Pro features (clustering included) turn on via the &lt;code&gt;Tsak__Redb__License__0&lt;/code&gt; environment variable with your JWT, or by editing &lt;code&gt;worker/appsettings.json&lt;/code&gt; before launch.&lt;/p&gt;

&lt;p&gt;So — two deployment paths, container or archive, but one model: &lt;strong&gt;the &lt;code&gt;.tpkg&lt;/code&gt; artifact goes into the modules folder, and hot-reload takes it from there.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Two zones: build and operate
&lt;/h2&gt;

&lt;p&gt;Let's put the whole picture together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The build/debug zone&lt;/strong&gt; — the &lt;code&gt;EchoWorker&lt;/code&gt; project. A console app, F5, breakpoints, a local SQLite file next to the exe, raw logs to the console. This is where you &lt;em&gt;write and debug&lt;/em&gt; routes. Loop: "tweak → run → poke with curl."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The operations zone&lt;/strong&gt; — Tsak (Docker or archive). The same module, but with a dashboard, metrics, hot-reload, and live management. This is where you &lt;em&gt;deploy and operate&lt;/em&gt;. Loop: "rebuild the &lt;code&gt;.tpkg&lt;/code&gt; → copy into &lt;code&gt;modules/&lt;/code&gt; → picked up."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the key thing: &lt;strong&gt;not a single line of the routes changes between those zones.&lt;/strong&gt; &lt;code&gt;InitRoute.main&lt;/code&gt; is the same. The only difference is the plumbing — in debug you write it (a dozen lines of &lt;code&gt;Program.cs&lt;/code&gt;), in production Tsak provides it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next: clustering (a teaser)
&lt;/h2&gt;

&lt;p&gt;Everything above is single-node. But Tsak clusters too: multiple workers, one shared dashboard, modules and routes distributed across nodes, leader election, automatic takeover when a node dies — and the whole cluster topology lives &lt;strong&gt;in the redb database itself&lt;/strong&gt;, with no separate membership infrastructure (no ZooKeeper/etcd/Consul). There's also active-passive for routes: the same route consumer runs on exactly one node, and if that node falls over, another one picks it up.&lt;/p&gt;

&lt;p&gt;That's a topic for the next post, though. This one was the intro: how to knock out two routes in an evening, build yourself a debug worker, and then take the exact same code into an enterprise runtime. The coordinator, the leader, and failover — we'll get into those properly next time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zghrjnys02sw9tq17l2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zghrjnys02sw9tq17l2.png" alt="redb.tsak dashboard" width="800" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm3s7polpabjxko0schwt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm3s7polpabjxko0schwt.png" alt="redb.tsak monitoring" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;We walked the whole path on one tiny example:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;wrote &lt;strong&gt;two routes&lt;/strong&gt; on redb.Route (POST to save into redb/SQLite, GET to fetch with a server-side &lt;code&gt;Where(...).ToListAsync()&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;built &lt;strong&gt;our own debug worker&lt;/strong&gt; — a console app under F5 that calls the same &lt;code&gt;InitRoute.main&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;packed the module into a &lt;strong&gt;&lt;code&gt;.tpkg&lt;/code&gt;&lt;/strong&gt; (DLL + &lt;code&gt;manifest.json&lt;/code&gt; + a &lt;code&gt;config.json&lt;/code&gt; with a context name);&lt;/li&gt;
&lt;li&gt;watched Tsak &lt;strong&gt;pick it up on its own&lt;/strong&gt; from the &lt;code&gt;modules/&lt;/code&gt; folder via hot-reload;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;operated&lt;/strong&gt; it from the dashboard (start/stop/restart of contexts and routes, with sticky state);&lt;/li&gt;
&lt;li&gt;sorted out the &lt;strong&gt;ports&lt;/strong&gt; (9090 — worker API, 5099 — our endpoints, 8080/5000 — dashboard);&lt;/li&gt;
&lt;li&gt;deployed it two ways: with &lt;strong&gt;Docker&lt;/strong&gt; (the whole &lt;code&gt;docker-compose.yml&lt;/code&gt;) and &lt;strong&gt;without&lt;/strong&gt; (an archive from the releases page).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One and the same codebase — comfortable to debug, and ready for an enterprise runtime. Clustering next.&lt;/p&gt;

&lt;p&gt;The full project: &lt;code&gt;redb.Route/demos/EchoWorkerDemo&lt;/code&gt; in the repo. Clone it, run it, break it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>microservices</category>
      <category>devops</category>
    </item>
    <item>
      <title>Leaving MassTransit for a Camel state of mind: the Kafka connector, Scatter-Gather, and what really happens under a transaction</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Tue, 30 Jun 2026 20:37:22 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/leaving-masstransit-for-a-camel-state-of-mind-the-kafka-connector-scatter-gather-and-what-really-106h</link>
      <guid>https://dev.to/rinat_kozin/leaving-masstransit-for-a-camel-state-of-mind-the-kafka-connector-scatter-gather-and-what-really-106h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71kq9e2a8fu5u678q11u.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71kq9e2a8fu5u678q11u.webp" alt="redb.route.kafka" width="800" height="1200"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Series:&lt;/strong&gt; redb ecosystem&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Part of the &lt;strong&gt;redb.Route&lt;/strong&gt; series — the Apache Camel we built for .NET. If you're just tuning in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin/redbroute-apache-camel-for-net-22-transports-30-eip-patterns-compiled-dsl-11m0"&gt;redb.Route — Apache Camel for .NET: 22 transports, 30+ EIP patterns, compiled DSL&lt;/a&gt; — the origin story;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin/enterprise-integration-patterns-in-net-the-deep-dive-series-part-1-the-four-in-memory-channels-3e24"&gt;Enterprise Integration Patterns in .NET — Part 1: the four in-memory channels (and the Exchange they carry)&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin/redbroute-301-flat-dsl-navigation-crtp-refactor-and-a-silent-null-fix-3m7n"&gt;redb.Route 3.0.1 — flat DSL navigation, CRTP refactor, and a silent null fix&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin/a-homegrown-apache-camel-for-net-dissected-the-http-connector-with-no-aspnet-mvc-the-56bd"&gt;Apache Camel for .NET, dissected: the HTTP connector with no ASP.NET MVC + the Content-Based Router pattern&lt;/a&gt; — the previous "EIP + connector" deep-dive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This time we open with the &lt;strong&gt;Kafka connector&lt;/strong&gt; — taken apart the same way we did HTTP — and then we mount two EIP patterns on top of it: &lt;strong&gt;Scatter-Gather&lt;/strong&gt; and &lt;strong&gt;Aggregator&lt;/strong&gt;. And the part tutorials skip: &lt;strong&gt;how the whole thing behaves once you wrap a transaction around it.&lt;/strong&gt; Also, &lt;strong&gt;3.2.0&lt;/strong&gt; shipped.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  One spoiler up front
&lt;/h2&gt;

&lt;p&gt;There's no "exactly-once out of the box" in Kafka here — and you'll see exactly why from the code below. What works and what doesn't — stated plainly.&lt;/p&gt;

&lt;p&gt;next A real production example: one HTTP request → six parallel aggregations&lt;/p&gt;


&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why leave MassTransit at all&lt;/li&gt;
&lt;li&gt;What landed in 3.2.0&lt;/li&gt;
&lt;li&gt;The Kafka connector: URI anatomy and the connection factory&lt;/li&gt;
&lt;li&gt;The Kafka producer: what &lt;code&gt;.To("kafka:...")&lt;/code&gt; actually does&lt;/li&gt;
&lt;li&gt;The Kafka consumer: polling, batches, rebalance&lt;/li&gt;
&lt;li&gt;Headers and tracing across the broker&lt;/li&gt;
&lt;li&gt;Straight talk about &lt;code&gt;transacted=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;EIP #1 — Scatter-Gather: one processor, both fan-out and join&lt;/li&gt;
&lt;li&gt;EIP #2 — Aggregator: assembly over time&lt;/li&gt;
&lt;li&gt;Transactions: two models, and that's the whole point&lt;/li&gt;
&lt;li&gt;The parallel cousins: Splitter and Multicast&lt;/li&gt;
&lt;li&gt;Saga — "a slightly different one"&lt;/li&gt;
&lt;li&gt;Outbox — there isn't one, and that's correct&lt;/li&gt;
&lt;li&gt;Bottom line: a trade, not "better/worse"&lt;/li&gt;
&lt;/ol&gt;



&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Why leave MassTransit at all
&lt;/h2&gt;

&lt;p&gt;MassTransit is genuinely good, and I'm not here to bury it. But it's an &lt;strong&gt;opinionated&lt;/strong&gt; framework: the durable state-machine saga, the transactional outbox, the retry policies — they're baked in, and you live by the framework's rules. When your scenario matches its worldview, it's a joy; everything "just works." When it doesn't, you're fighting an abstraction, spelunking through internals and bolting hacks onto someone else's decisions.&lt;/p&gt;

&lt;p&gt;Apache Camel is a different school. The bedrock is &lt;strong&gt;EIP primitives + connectors + a DSL&lt;/strong&gt;. Nobody &lt;em&gt;gifts&lt;/em&gt; you an outbox as a feature — you get Splitter, Aggregator, Scatter-Gather, transactional transports, and you &lt;strong&gt;assemble&lt;/strong&gt; exactly what your invariants need. Less magic, more explicit composition from bricks.&lt;/p&gt;

&lt;p&gt;redb.Route sits in the second school. Hence the title: not "batteries included," but "here are the bricks, and they snap together predictably." The rest of the article is about what that trade costs you, and what you get back.&lt;/p&gt;



&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. What landed in 3.2.0
&lt;/h2&gt;

&lt;p&gt;Full notes are in the &lt;a href="//../../redb.Route/CHANGELOG.md"&gt;CHANGELOG&lt;/a&gt;. Two items matter here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parallel &lt;code&gt;Splitter&lt;/code&gt; / &lt;code&gt;Multicast&lt;/code&gt; branches now isolate the ambient transaction per branch.&lt;/strong&gt; Each branch gets its own &lt;code&gt;DependentTransaction.DependentClone(BlockCommitUntilComplete)&lt;/code&gt;; the parent commit blocks until every branch signals done. Before this, parallel branches shared a single &lt;code&gt;Transaction.Current&lt;/code&gt;, and &lt;code&gt;System.Transactions&lt;/code&gt; forbids using one transaction concurrently across threads. We'll read this fix line by line and see &lt;strong&gt;why Scatter-Gather never needed it&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Throttle&lt;/code&gt; got an RFC 6585 mode&lt;/strong&gt; — &lt;code&gt;.RejectOnOverflow()&lt;/code&gt; (429 + &lt;code&gt;Retry-After&lt;/code&gt;). Off-topic today, but handy if you front all of this with an HTTP edge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All &lt;code&gt;redb.Route.*&lt;/code&gt; packages are version-aligned at &lt;strong&gt;3.2.0&lt;/strong&gt;.&lt;/p&gt;



&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3. The Kafka connector: URI anatomy and the connection factory
&lt;/h2&gt;

&lt;p&gt;We open with Kafka, as promised — and we open with the simplest thing there is: a &lt;strong&gt;string endpoint URI&lt;/strong&gt;. Here's a minimal consumer and a minimal producer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Consumer: read topic 'orders', group 'order-workers', from the beginning&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders?brokers=kafka:9092&amp;amp;groupId=order-workers&amp;amp;autoOffsetReset=earliest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Got: ${body}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:process"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Producer: publish to 'notifications' with acks=all&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:notify"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:notifications?brokers=kafka:9092&amp;amp;acks=All"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URI shape is &lt;code&gt;kafka:&amp;lt;topic&amp;gt;?&amp;lt;query&amp;gt;&lt;/code&gt;. The topic comes from the path (&lt;a href="//../../redb.Route/src/redb.Route.Kafka/KafkaEndpoint.cs"&gt;&lt;code&gt;KafkaEndpoint&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;KafkaEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EndpointUri&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KafkaComponent&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KafkaEndpointOptions&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TopicName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// ← topic = URI path&lt;/span&gt;

    &lt;span class="c1"&gt;// If the URI says connectionFactory=&amp;lt;name&amp;gt;, resolve it from the registry&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ResolvedFactory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetFromRegistry&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;KafkaConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// No brokers in the URI but the factory has them — fill it in&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Brokers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ResolvedFactory&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Brokers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ResolvedFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Brokers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the last touch: on a consumer, &lt;code&gt;groupId&lt;/code&gt; is &lt;strong&gt;mandatory&lt;/strong&gt;, and forgetting it fails endpoint creation rather than blowing up later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;IConsumer&lt;/span&gt; &lt;span class="nf"&gt;CreateConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IProcessor&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"The 'groupId' parameter is required for Kafka consumer on topic '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TopicName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;KafkaConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The connection factory — so you don't repeat yourself
&lt;/h3&gt;

&lt;p&gt;When you've got twenty Kafka endpoints on one cluster, spelling out &lt;code&gt;brokers=...&amp;amp;securityProtocol=...&amp;amp;saslMechanism=...&lt;/code&gt; in every URI is suicide. That's what &lt;a href="//../../redb.Route/src/redb.Route.Kafka/KafkaConnectionFactory.cs"&gt;&lt;code&gt;KafkaConnectionFactory&lt;/code&gt;&lt;/a&gt; is for: register it in the registry under a name, then reference it via &lt;code&gt;connectionFactory=name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its full parameter surface (all from source, nothing invented):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Properties&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brokers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Brokers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost:9092&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SecurityProtocol&lt;/code&gt;, &lt;code&gt;SaslMechanism&lt;/code&gt;, &lt;code&gt;SaslUsername&lt;/code&gt;, &lt;code&gt;SaslPassword&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Plaintext&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL/TLS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SslCaLocation&lt;/code&gt;, &lt;code&gt;SslCertificateLocation&lt;/code&gt;, &lt;code&gt;SslKeyLocation&lt;/code&gt;, &lt;code&gt;SslKeyPassword&lt;/code&gt;, &lt;code&gt;SslEndpointIdentificationAlgorithm&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Acks&lt;/code&gt;, &lt;code&gt;Retries&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Leader&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GroupId&lt;/code&gt;, &lt;code&gt;AutoOffsetReset&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—, &lt;code&gt;Latest&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer tuning&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GroupInstanceId&lt;/code&gt;, &lt;code&gt;SessionTimeoutMs&lt;/code&gt;, &lt;code&gt;HeartbeatIntervalMs&lt;/code&gt;, &lt;code&gt;MaxPollIntervalMs&lt;/code&gt;, &lt;code&gt;PartitionAssignmentStrategy&lt;/code&gt;, &lt;code&gt;IsolationLevel&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer tuning&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LingerMs&lt;/code&gt;, &lt;code&gt;BatchSize&lt;/code&gt;, &lt;code&gt;CompressionType&lt;/code&gt;, &lt;code&gt;MessageTimeoutMs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconnect&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ReconnectBackoffMs&lt;/code&gt;, &lt;code&gt;ReconnectBackoffMaxMs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advanced&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ClientId&lt;/code&gt;, &lt;code&gt;RequestTimeoutMs&lt;/code&gt;, &lt;code&gt;MetadataMaxAgeMs&lt;/code&gt;, &lt;code&gt;SocketTimeoutMs&lt;/code&gt;, &lt;code&gt;MaxInFlight&lt;/code&gt;, &lt;code&gt;AdditionalProperties&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;redb.Route&lt;/code&gt;, 30000, 300000, 60000, 5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Register and use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"prod-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KafkaConnectionFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Brokers&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"kafka1:9092,kafka2:9092,kafka3:9092"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SecurityProtocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SaslSsl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SaslMechanism&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ScramSha512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SaslUsername&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"svc-orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SaslPassword&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CompressionType&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Zstd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MaxInFlight&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Now the URI only carries what's endpoint-specific:&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders?connectionFactory=prod-cluster&amp;amp;groupId=order-workers"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:notifications?connectionFactory=prod-cluster&amp;amp;acks=All"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The factory builds three configs — &lt;code&gt;BuildConsumerConfig()&lt;/code&gt;, &lt;code&gt;BuildProducerConfig()&lt;/code&gt;, &lt;code&gt;BuildAdminConfig()&lt;/code&gt; (the last for topic-metadata queries). Endpoint-level options are applied &lt;strong&gt;on top of&lt;/strong&gt; the factory (&lt;code&gt;if (!string.IsNullOrWhiteSpace(Brokers)) config.BootstrapServers = Brokers;&lt;/code&gt; and so on), so the factory sets the baseline and the URI overrides it surgically.&lt;/p&gt;

&lt;h3&gt;
  
  
  The full endpoint option set
&lt;/h3&gt;

&lt;p&gt;The complete list — &lt;a href="//../../redb.Route/src/redb.Route.Kafka/KafkaEndpointOptions.cs"&gt;&lt;code&gt;KafkaEndpointOptions&lt;/code&gt;&lt;/a&gt;, grouped, with defaults:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection / security:&lt;/strong&gt; &lt;code&gt;brokers&lt;/code&gt; (&lt;strong&gt;required&lt;/strong&gt;, or &lt;code&gt;Validate()&lt;/code&gt; throws), &lt;code&gt;securityProtocol&lt;/code&gt; (&lt;code&gt;Plaintext&lt;/code&gt;/&lt;code&gt;Ssl&lt;/code&gt;/&lt;code&gt;SaslPlaintext&lt;/code&gt;/&lt;code&gt;SaslSsl&lt;/code&gt;), &lt;code&gt;saslMechanism&lt;/code&gt;/&lt;code&gt;saslUsername&lt;/code&gt;/&lt;code&gt;saslPassword&lt;/code&gt;, &lt;code&gt;sslCaLocation&lt;/code&gt;/&lt;code&gt;sslCertificateLocation&lt;/code&gt;/&lt;code&gt;sslKeyLocation&lt;/code&gt;/&lt;code&gt;sslKeyPassword&lt;/code&gt;, &lt;code&gt;connectionFactory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer:&lt;/strong&gt; &lt;code&gt;groupId&lt;/code&gt;, &lt;code&gt;autoOffsetReset&lt;/code&gt; (&lt;code&gt;Latest&lt;/code&gt;/&lt;code&gt;Earliest&lt;/code&gt;/&lt;code&gt;Error&lt;/code&gt;, default &lt;code&gt;Latest&lt;/code&gt;), &lt;code&gt;enableAutoCommit&lt;/code&gt; (default &lt;code&gt;true&lt;/code&gt;, framework-level — since 3.2.1), &lt;code&gt;maxPollRecords&lt;/code&gt; (0 = single mode, &amp;gt;0 = batch), &lt;code&gt;pollTimeoutMs&lt;/code&gt; (1000), &lt;code&gt;breakOnFirstError&lt;/code&gt;, &lt;code&gt;seekTo&lt;/code&gt; (&lt;code&gt;beginning&lt;/code&gt;/&lt;code&gt;end&lt;/code&gt;), &lt;code&gt;topicIsPattern&lt;/code&gt;, &lt;code&gt;groupInstanceId&lt;/code&gt;, &lt;code&gt;sessionTimeoutMs&lt;/code&gt;, &lt;code&gt;heartbeatIntervalMs&lt;/code&gt;, &lt;code&gt;maxPollIntervalMs&lt;/code&gt;, &lt;code&gt;partitionAssignmentStrategy&lt;/code&gt; (&lt;code&gt;Range&lt;/code&gt;/&lt;code&gt;RoundRobin&lt;/code&gt;/&lt;code&gt;CooperativeSticky&lt;/code&gt;), &lt;code&gt;isolationLevel&lt;/code&gt; (&lt;code&gt;ReadUncommitted&lt;/code&gt;/&lt;code&gt;ReadCommitted&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer:&lt;/strong&gt; &lt;code&gt;acks&lt;/code&gt; (&lt;code&gt;None&lt;/code&gt;/&lt;code&gt;Leader&lt;/code&gt;/&lt;code&gt;All&lt;/code&gt;, default &lt;code&gt;Leader&lt;/code&gt;), &lt;code&gt;retries&lt;/code&gt; (3), &lt;code&gt;recordMetadata&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;partitionNumber&lt;/code&gt;, &lt;code&gt;transacted&lt;/code&gt;, &lt;code&gt;transactionIdPrefix&lt;/code&gt; (&lt;code&gt;redb-kafka&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer tuning:&lt;/strong&gt; &lt;code&gt;lingerMs&lt;/code&gt;, &lt;code&gt;batchSize&lt;/code&gt;, &lt;code&gt;compressionType&lt;/code&gt; (&lt;code&gt;None&lt;/code&gt;/&lt;code&gt;Gzip&lt;/code&gt;/&lt;code&gt;Snappy&lt;/code&gt;/&lt;code&gt;Lz4&lt;/code&gt;/&lt;code&gt;Zstd&lt;/code&gt;), &lt;code&gt;messageTimeoutMs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced:&lt;/strong&gt; &lt;code&gt;additionalProperties&lt;/code&gt; — arbitrary librdkafka properties, applied &lt;strong&gt;last&lt;/strong&gt;, overriding everything typed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Validate()&lt;/code&gt; checks exactly: &lt;code&gt;brokers&lt;/code&gt; non-empty, &lt;code&gt;maxPollRecords &amp;gt;= 0&lt;/code&gt;, &lt;code&gt;pollTimeoutMs &amp;gt;= 0&lt;/code&gt;, &lt;code&gt;retries &amp;gt;= 0&lt;/code&gt;. Everything else has a default or is optional.&lt;/p&gt;

&lt;p&gt;One thing to get precise — about the offset commit, and it matters. At the &lt;strong&gt;librdkafka&lt;/strong&gt; level, &lt;code&gt;EnableAutoCommit&lt;/code&gt; is hard-wired to &lt;code&gt;false&lt;/code&gt; &lt;strong&gt;always&lt;/strong&gt; — the library never commits on a background timer (which would commit un-processed offsets). From &lt;code&gt;BuildConsumerConfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ConsumerConfig&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BootstrapServers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Brokers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GroupId&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GroupId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AutoOffsetReset&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseAutoOffsetReset&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;EnableAutoCommit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// librdkafka level: always manual commit&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there &lt;em&gt;is&lt;/em&gt; a commit — one level up, at the &lt;strong&gt;framework&lt;/strong&gt; level. As of &lt;strong&gt;3.2.1&lt;/strong&gt; the consumer has a typed &lt;strong&gt;&lt;code&gt;EnableAutoCommit&lt;/code&gt;&lt;/strong&gt; option (default &lt;code&gt;true&lt;/code&gt;): after a successful &lt;code&gt;Process&lt;/code&gt; it commits the offset &lt;strong&gt;inline&lt;/strong&gt; — exactly like the RabbitMQ consumer acks after processing. Set it from the URI (&lt;code&gt;?enableAutoCommit=false&lt;/code&gt;) or fluently (&lt;code&gt;.EnableAutoCommit(false)&lt;/code&gt;). A transactional route takes precedence: if a &lt;code&gt;.Transacted()&lt;/code&gt; already committed the offset, the inline path is skipped. The mechanics are in §5.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same thing, fluent
&lt;/h3&gt;

&lt;p&gt;String URIs are great for short snippets and config files. In code you usually build endpoints fluently — &lt;a href="//../../redb.Route/src/redb.Route.Kafka/Fluent/KafkaDsl.cs"&gt;&lt;code&gt;Kafka.Topic(...)&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Kafka&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Brokers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka1:9092,kafka2:9092"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-workers"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AutoOffsetReset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Earliest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxPollRecords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PartitionAssignmentStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CooperativeSticky"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsolationLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReadCommitted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SessionTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxPollInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;300_000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Kafka&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notifications"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Brokers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka1:9092"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Acks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"All"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Compression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"zstd"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Linger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BatchSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${header.orderId}"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// partition key — an expression&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fluent builder just assembles the same query string (&lt;code&gt;Kafka.Topic("orders").Brokers(...)...Build()&lt;/code&gt; → &lt;code&gt;"kafka:orders?brokers=...&amp;amp;..."&lt;/code&gt;), URL-encoding the values. The setters take an &lt;code&gt;IExpression&lt;/code&gt;, but options resolve at different times: the &lt;strong&gt;producer key&lt;/strong&gt; is evaluated as a &lt;code&gt;${...}&lt;/code&gt; expression &lt;strong&gt;per message&lt;/strong&gt; (in &lt;code&gt;DetermineKey&lt;/code&gt; at send time — see below), whereas connection-level options (&lt;code&gt;brokers&lt;/code&gt;, credentials) are resolved once on connect, where there's no exchange yet, so an expression there is meaningless. The per-message story is specifically the partition key.&lt;/p&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Kafka producer: what &lt;code&gt;.To("kafka:...")&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;Now let's read what &lt;a href="//../../redb.Route/src/redb.Route.Kafka/KafkaProducer.cs"&gt;&lt;code&gt;KafkaProducer&lt;/code&gt;&lt;/a&gt; really does when a message hits &lt;code&gt;.To("kafka:...")&lt;/code&gt;. It's a &lt;code&gt;ConnectableProducer&lt;/code&gt;, so it requires &lt;code&gt;Start()&lt;/code&gt; first (&lt;code&gt;EnsureStarted()&lt;/code&gt; at the top of &lt;code&gt;Process&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildProducerConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResolvedFactory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_producer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ProducerBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValueSerializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Serializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetErrorHandler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Kafka producer error: {Reason} (Code: {Code})"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transacted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitTransactions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Kafka transactional mode enabled: topic={Topic}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopicName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key is &lt;code&gt;string&lt;/code&gt;, value is &lt;code&gt;byte[]&lt;/code&gt;. On disconnect it does &lt;code&gt;Flush(30s)&lt;/code&gt; before &lt;code&gt;Dispose&lt;/code&gt; so buffered messages aren't lost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preparing the message
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PrepareMessage&lt;/code&gt; turns the exchange body into a &lt;code&gt;Message&amp;lt;string, byte[]&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;valueBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                  &lt;span class="c1"&gt;// already bytes — as-is&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;str&lt;/span&gt;   &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;            &lt;span class="c1"&gt;// string → UTF-8&lt;/span&gt;
    &lt;span class="k"&gt;null&lt;/span&gt;         &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;                    &lt;span class="c1"&gt;// null → empty&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;            &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// else → ToString → UTF-8&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then headers. &lt;code&gt;ContentType&lt;/code&gt; rides as a &lt;code&gt;content-type&lt;/code&gt; header, and the exchange's user headers move into Kafka headers — &lt;strong&gt;except the internal &lt;code&gt;redbKafka.*&lt;/code&gt; ones&lt;/strong&gt;, which are filtered out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KafkaHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsRedbHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// redbKafka.* are internal — don't propagate&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The partition key
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;DetermineKey&lt;/code&gt; — three branches, in priority order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;DetermineKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Explicit partition → no key (partitioner is bypassed)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PartitionNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Key from the option — supports ${...} expressions, resolved per message&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResolveOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Otherwise — keyless (round-robin across partitions)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;key=${header.orderId}&lt;/code&gt; means "partition by orderId" — all events for one order land in one partition and keep their order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Send: immediate vs deferred
&lt;/h3&gt;

&lt;p&gt;Here's the fork that matters for transactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transacted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Deferred send — the real publish happens when the route transaction commits&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;KafkaSendAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_producer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PartitionNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordMetadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;RegisterTransactedAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"kafka-send-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Immediate send&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PartitionNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProduceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TopicPartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PartitionNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProduceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordMetadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;AddDeliveryMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// redbKafka.Sent.Topic/Partition/Offset/Timestamp&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In transactional mode the producer doesn't send right away — it parks a &lt;code&gt;KafkaSendAction&lt;/code&gt; in the deferred-action bag under a unique key &lt;code&gt;kafka-send-{guid}&lt;/code&gt;. The real &lt;code&gt;ProduceAsync&lt;/code&gt; happens later, at the &lt;code&gt;.Transacted()&lt;/code&gt; boundary. More on that below.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;KafkaSendAction&lt;/code&gt; &lt;strong&gt;clones&lt;/strong&gt; the message at construction (a deep header copy via &lt;code&gt;Buffer.BlockCopy&lt;/code&gt;) so the deferred send doesn't depend on what happens to the exchange afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProduceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TopicPartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_topicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProduceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_topicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_recordMetadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* write the Sent.* headers */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The message is simply discarded — nothing was published&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tracing across the broker
&lt;/h3&gt;

&lt;p&gt;Before sending, the producer injects W3C trace context (&lt;code&gt;traceparent&lt;/code&gt;/&lt;code&gt;tracestate&lt;/code&gt;) into Kafka headers via the standard &lt;code&gt;DistributedContextPropagator&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;propagator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DistributedContextPropagator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;propagator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;Headers&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer on the other end lifts it back out — and your trace shows an unbroken chain across the broker. Zero configuration on your side.&lt;/p&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Kafka consumer: polling, batches, rebalance
&lt;/h2&gt;

&lt;p&gt;&lt;a href="//../../redb.Route/src/redb.Route.Kafka/KafkaConsumer.cs"&gt;&lt;code&gt;KafkaConsumer&lt;/code&gt;&lt;/a&gt; is a &lt;code&gt;DrainableConsumer&lt;/code&gt; (it can gracefully drain in-flight messages on stop). Step by step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start: subscribe/assign, seek, metadata
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OnStarting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildConsumerConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResolvedFactory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;_consumer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ConsumerBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValueDeserializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Deserializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetErrorHandler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fatal → LogError, else LogWarning */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPartitionsAssignedHandler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partitions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* log assigned */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPartitionsRevokedHandler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partitions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Commit current offsets before partitions are revoked&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partitions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KafkaException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* nothing to commit — fine */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPartitionsLostHandler&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partitions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* log involuntary loss */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PartitionNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TopicPartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopicName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PartitionNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;_consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopicName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;HandleSeekTo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// seekTo=beginning/end on first start&lt;/span&gt;
    &lt;span class="nf"&gt;LogTopicMetadata&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// dump: partitions, leaders, replicas, ISR&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two membership modes: &lt;code&gt;Subscribe(topic)&lt;/code&gt; (dynamic group assignment + rebalance) or an explicit &lt;code&gt;Assign(partition)&lt;/code&gt; when &lt;code&gt;partitionNumber&lt;/code&gt; is set (static assignment, no group rebalance).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LogTopicMetadata&lt;/code&gt; spins up an &lt;code&gt;AdminClient&lt;/code&gt; at start and logs the topic topology — partition count, each partition's leader, replicas, ISR. Handy for "why did I only get assigned 2 of 6 partitions" debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  The poll loop
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;RunAsync&lt;/code&gt; kicks off a long-running task with the poll loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;PollLoop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;pollCt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;processingCt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;pollCt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxPollRecords&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollCt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processingCt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// batch mode&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessSingleMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollCt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processingCt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// one at a time&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Error in Kafka poll loop for topic {Topic}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TopicName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pollCt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caught an exception in the loop? Log it, wait a second, carry on (one bad message doesn't kill the consumer).&lt;/p&gt;

&lt;h3&gt;
  
  
  Single mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessSingleMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;pollCt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;processingCt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_consumer&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="nf"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pollCt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateExchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;IncrementInflight&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Register the offset commit as a deferred action — for the transactional-route case.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;commitAction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;KafkaCommitAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_consumer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;RegisterTransactedAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"kafka-commit-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commitAction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processingCt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;ProcessedCount&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;

        &lt;span class="c1"&gt;// 3.2.1: auto-commit after success — unless a transaction already committed it (Committed flag).&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnableAutoCommit&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;commitAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Committed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;commitAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processingCt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;DecrementInflight&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail: the consumer registers a &lt;code&gt;KafkaCommitAction&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; processing. So the offset isn't committed immediately — it commits when the route transaction commits, alongside any deferred sends. If processing throws, the offset isn't committed and the message comes back.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;KafkaCommitAction.Commit&lt;/code&gt; is a plain &lt;code&gt;_consumer.Commit(result)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← plain offset commit, NOT SendOffsetsToTransaction&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Rollback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// no commit → message is re-read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Batch mode
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;maxPollRecords &amp;gt; 0&lt;/code&gt;, the consumer collects up to &lt;code&gt;maxPollRecords&lt;/code&gt; records, or until the &lt;code&gt;pollTimeoutMs&lt;/code&gt; deadline, and hands them off as &lt;strong&gt;one&lt;/strong&gt; exchange whose body is a &lt;code&gt;List&amp;lt;IMessage&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxPollRecords&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;pollCt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_consumer&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="nf"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateBatchExchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// Body = List&amp;lt;IMessage&amp;gt;, redbKafka.BatchSize = N&lt;/span&gt;
&lt;span class="c1"&gt;// Commit only the last offset of the batch&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nf"&gt;RegisterTransactedAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"kafka-batch-commit-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;KafkaCommitAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_consumer&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;processingCt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subtlety: only the &lt;strong&gt;last&lt;/strong&gt; offset is committed (Kafka offsets are monotonic per partition, so committing the last covers the whole batch). Downstream you can &lt;code&gt;.Split(body =&amp;gt; body)&lt;/code&gt; and handle each message individually — but the commit is still one per batch.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the exchange gets its metadata
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CreateExchange&lt;/code&gt; sets the body (&lt;code&gt;byte[]&lt;/code&gt;), transfers Kafka headers (UTF-8 decoded), restores &lt;code&gt;ContentType&lt;/code&gt;, and stamps metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KafkaHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KafkaHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Partition&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Partition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KafkaHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Offset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;TimestampType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotAvailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KafkaHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcDateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KafkaHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScopeFactory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ExchangePattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InOnly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// a consumer is InOnly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What the consumer does while a message travels the route
&lt;/h3&gt;

&lt;p&gt;Let's walk &lt;code&gt;ProcessSingleMessage&lt;/code&gt; step by step, because it also explains the commit story:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;Consume(pollCt)&lt;/code&gt; — pulled one record (in single mode the poll is &lt;strong&gt;blocking&lt;/strong&gt;: the next record isn't pulled until the current one finishes);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CreateExchange&lt;/code&gt; — built the exchange;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IncrementInflight()&lt;/code&gt; — marked the message &lt;strong&gt;in-flight&lt;/strong&gt; (this is what &lt;code&gt;DrainableConsumer&lt;/code&gt; needs for a graceful stop: on shutdown it drains all in-flight messages);&lt;/li&gt;
&lt;li&gt;registered a &lt;code&gt;KafkaCommitAction&lt;/code&gt; in &lt;code&gt;TRANSACT_ACTION&lt;/code&gt; — &lt;strong&gt;not committed yet&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;await Processor.Process(exchange)&lt;/code&gt; — ran the message through the whole route (synchronously awaited);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ProcessedCount++&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;if &lt;code&gt;EnableAutoCommit&lt;/code&gt; (default &lt;code&gt;true&lt;/code&gt;) and the offset wasn't already committed by a transaction — commits it &lt;strong&gt;inline&lt;/strong&gt; right here;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;finally&lt;/code&gt;: &lt;code&gt;exchange.DisposeAsync()&lt;/code&gt; + &lt;code&gt;DecrementInflight()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key point: by default (&lt;code&gt;EnableAutoCommit=true&lt;/code&gt;, since 3.2.1) the consumer &lt;strong&gt;commits the offset inline after a successful &lt;code&gt;Process&lt;/code&gt;&lt;/strong&gt; — at-least-once, just like RabbitMQ. The &lt;code&gt;KafkaCommitAction&lt;/code&gt; registered in &lt;code&gt;TRANSACT_ACTION&lt;/code&gt; is for the &lt;strong&gt;other&lt;/strong&gt; case — when a transaction should own the commit: then &lt;code&gt;.Transacted()&lt;/code&gt; commits it at its boundary, and the action's &lt;code&gt;Committed&lt;/code&gt; flag stops the consumer from committing it a second time inline.&lt;/p&gt;

&lt;p&gt;Batch mode is the same, except &lt;code&gt;Process&lt;/code&gt; gets a single exchange whose body is a &lt;code&gt;List&amp;lt;IMessage&amp;gt;&lt;/code&gt;, and the &lt;strong&gt;last&lt;/strong&gt; offset of the batch is committed (inline or by the transaction).&lt;/p&gt;

&lt;h3&gt;
  
  
  How the offset commits: default vs transactional mode
&lt;/h3&gt;

&lt;p&gt;As of 3.2.1 the default is simple and matches RabbitMQ: &lt;strong&gt;process it, commit it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Default (EnableAutoCommit=true): the offset commits inline after a successful Process.&lt;/span&gt;
&lt;span class="c1"&gt;//    A plain consumer needs no configuration — at-least-once out of the box.&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders?brokers=kafka:9092&amp;amp;groupId=w"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:process"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transactional mode is for when the offset must commit &lt;strong&gt;together&lt;/strong&gt; with other work (deferred sends to Kafka/Redis, a redb write) — atomically at the route boundary. Turn auto-commit off and wrap it in a transaction; the offset commit becomes part of the &lt;code&gt;TRANSACT_ACTION&lt;/code&gt; batch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Transactional: the offset + deferred sends commit together at the .Transacted() boundary.&lt;/span&gt;
&lt;span class="c1"&gt;//    enableAutoCommit=false → no inline commit; the transaction owns the offset.&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders?brokers=kafka:9092&amp;amp;groupId=w&amp;amp;enableAutoCommit=false"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Transacted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders.done?brokers=kafka:9092&amp;amp;transacted=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Imperative — the same via an explicit commit.&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders?brokers=kafka:9092&amp;amp;groupId=w&amp;amp;enableAutoCommit=false"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransaction&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:process"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you leave &lt;code&gt;enableAutoCommit=true&lt;/code&gt; (the default) AND wrap in &lt;code&gt;.Transacted()&lt;/code&gt;, the &lt;strong&gt;transaction wins&lt;/strong&gt;: it commits the offset at the boundary, and the &lt;code&gt;Committed&lt;/code&gt; flag on &lt;code&gt;KafkaCommitAction&lt;/code&gt; stops the consumer from duplicating the commit inline. So inside a transactional route the option is effectively ignored.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;By the way, in the &lt;a href="//../../redb.Route/demos/redb.Route.Demo/Routes"&gt;redb.Route.Demo/Routes&lt;/a&gt; demos Kafka is used &lt;strong&gt;only as a producer&lt;/strong&gt; (&lt;code&gt;.WireTap(KafkaWireTap)&lt;/code&gt; to the &lt;code&gt;demo-audit&lt;/code&gt; topic in &lt;a href="//../../redb.Route/demos/redb.Route.Demo/Routes/MainPipelineRoutes.cs"&gt;&lt;code&gt;MainPipelineRoutes.cs&lt;/code&gt;&lt;/a&gt;); there's no consumer there — so the production consume-process-produce pattern is in the transactions section.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Headers and tracing across the broker
&lt;/h2&gt;

&lt;p&gt;The full header reference — &lt;a href="//../../redb.Route/src/redb.Route.Kafka/KafkaHeaders.cs"&gt;&lt;code&gt;KafkaHeaders&lt;/code&gt;&lt;/a&gt;. They all share the &lt;code&gt;redbKafka.&lt;/code&gt; prefix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Set by&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Topic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;consumer&lt;/td&gt;
&lt;td&gt;topic of the consumed record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Partition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;consumer&lt;/td&gt;
&lt;td&gt;partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Offset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;consumer&lt;/td&gt;
&lt;td&gt;offset in the partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;consumer&lt;/td&gt;
&lt;td&gt;record timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;consumer&lt;/td&gt;
&lt;td&gt;key (if present)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.BatchSize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;consumer&lt;/td&gt;
&lt;td&gt;batch size (batch mode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Sent.Topic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;producer&lt;/td&gt;
&lt;td&gt;topic sent to (with &lt;code&gt;recordMetadata&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Sent.Partition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;producer&lt;/td&gt;
&lt;td&gt;destination partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Sent.Offset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;producer&lt;/td&gt;
&lt;td&gt;assigned offset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbKafka.Sent.Timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;producer&lt;/td&gt;
&lt;td&gt;send time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a route you reach them as &lt;code&gt;${header.redbKafka.Offset}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders?brokers=kafka:9092&amp;amp;groupId=w"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"offset=${header.redbKafka.Offset} partition=${header.redbKafka.Partition} key=${header.redbKafka.Key}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On re-publish these &lt;code&gt;redbKafka.*&lt;/code&gt; headers are &lt;strong&gt;not&lt;/strong&gt; propagated into the outgoing Kafka message (the &lt;code&gt;IsRedbHeader&lt;/code&gt; filter in the producer) — so one hop's metadata doesn't leak into the next.&lt;/p&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Straight talk about &lt;code&gt;transacted=true&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is where marketing wants to say "exactly-once." Let's read the code with no spin.&lt;/p&gt;

&lt;p&gt;What &lt;code&gt;transacted=true&lt;/code&gt; does (from &lt;code&gt;BuildProducerConfig&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Transacted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnableIdempotence&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Acks&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Acks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// required for the idempotent producer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it: no &lt;code&gt;transactional.id&lt;/code&gt;, and &lt;code&gt;InitTransactions&lt;/code&gt; / &lt;code&gt;BeginTransaction&lt;/code&gt; / &lt;code&gt;CommitTransaction&lt;/code&gt; / &lt;code&gt;SendOffsetsToTransaction&lt;/code&gt; appear nowhere. None of librdkafka's transactional API is in play.&lt;/p&gt;

&lt;p&gt;What that means in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"transacted" here = an &lt;strong&gt;idempotent producer&lt;/strong&gt; (&lt;code&gt;EnableIdempotence=true&lt;/code&gt;, dedup of retries within the session) + a &lt;strong&gt;deferred send&lt;/strong&gt; (via &lt;code&gt;KafkaSendAction&lt;/code&gt;, real &lt;code&gt;ProduceAsync&lt;/code&gt; at the route's transaction boundary);&lt;/li&gt;
&lt;li&gt;this is &lt;strong&gt;not&lt;/strong&gt; Kafka EOS over consume-process-produce: there's no atomic Kafka transaction via &lt;code&gt;SendOffsetsToTransaction&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a crash between the send and the offset commit yields a &lt;strong&gt;duplicate&lt;/strong&gt; on restart (at-least-once), not exactly-once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the honest phrasing is "idempotent + route-level deferred commit," not exactly-once. For most needs — "don't lose the message, don't commit the offset before the work is done" — that's enough; true read-process-write EOS (&lt;code&gt;BeginTransaction&lt;/code&gt; / &lt;code&gt;SendOffsetsToTransaction&lt;/code&gt; / &lt;code&gt;CommitTransaction&lt;/code&gt;) is a separate story, and it isn't here.&lt;/p&gt;

&lt;p&gt;Apache Camel, by the way, doesn't give you EOS via &lt;code&gt;transacted()&lt;/code&gt; either: it's auto-commit by default + a manual-commit hook (&lt;code&gt;KafkaManualCommit&lt;/code&gt;) + an idempotent repository for dedup — the same model. So this isn't "Kafka-lite" — it's the same honest trade a mature integration framework makes.&lt;/p&gt;

&lt;p&gt;With Kafka taken apart, we can put EIP patterns on it.&lt;/p&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. EIP #1 — Scatter-Gather: one processor, both fan-out and join
&lt;/h2&gt;

&lt;p&gt;Classic Scatter-Gather from Hohpe/Woolf is "broadcast a request to N recipients and gather their responses into one." In redb.Route it's a single processor, &lt;a href="//../../redb.Route/src/redb.Route/Processors/ScatterGatherProcessor.cs"&gt;&lt;code&gt;ScatterGatherProcessor&lt;/code&gt;&lt;/a&gt;, whose aggregator is &lt;strong&gt;mandatory&lt;/strong&gt; and whose parallelism is &lt;strong&gt;on by default&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The most stringly-typed shape — Kafka in, fan out to three services, fold, publish the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders.incoming?brokers=kafka:9092&amp;amp;groupId=enricher&amp;amp;autoOffsetReset=earliest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-enrich"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ScatterGather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="c1"&gt;// (accumulated, current) → merged. Called pair-wise.&lt;/span&gt;
        &lt;span class="n"&gt;aggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"merged"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"merged"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s"&gt;"http://pricing:8080/quote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"http://inventory:8080/check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"http://fraud:8080/score"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders.enriched?brokers=kafka:9092&amp;amp;acks=All"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ No transaction here, and that's fine: by default (3.2.1) the inbound offset commits inline after successful processing. You only need a transaction for an atomic "commit-offset + publish-result" coupling — then &lt;code&gt;enableAutoCommit=false&lt;/code&gt; + &lt;code&gt;.Transacted()&lt;/code&gt; (see §5).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A realistic note about Kafka.&lt;/strong&gt; Scatter-Gather &lt;em&gt;gathers responses&lt;/em&gt;, so its natural fan-out targets are &lt;strong&gt;request/reply&lt;/strong&gt; endpoints: HTTP, gRPC, SQL SELECT. A Kafka producer is fire-and-forget — there's nothing to "gather" beyond delivery metadata. So Kafka lives at the &lt;strong&gt;edges&lt;/strong&gt; here: source (&lt;code&gt;From("kafka:...")&lt;/code&gt;) and sink (&lt;code&gt;To("kafka:...")&lt;/code&gt;), while the fan-out targets services. That's how it shakes out in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Under the hood: the parallel path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessParallel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;recipients&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;callerCt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clones&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;IExchange&lt;/span&gt;&lt;span class="p"&gt;?[&lt;/span&gt;&lt;span class="n"&gt;recipients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;maxDop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_maxDegreeOfParallelism&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_maxDegreeOfParallelism&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessorCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SemaphoreSlim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxDop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendToRecipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                 &lt;span class="c1"&gt;// ← Clone(): Properties copied shallowly&lt;/span&gt;
            &lt;span class="n"&gt;clones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetOrCreateProducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipients&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;  &lt;span class="c1"&gt;// per-URI producer cache&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;clones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]!.&lt;/span&gt;&lt;span class="nf"&gt;ReleaseScopes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// release DI scopes early; body/headers live on for aggregation&lt;/span&gt;
            &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Enumerable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SendToRecipient&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// (stopOnException; best-effort wraps each branch in try/catch)&lt;/span&gt;

    &lt;span class="c1"&gt;// Aggregation — in deterministic index order, not arrival order&lt;/span&gt;
    &lt;span class="n"&gt;IExchange&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_aggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;ApplyAggregation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things here are critical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exchange.Clone()&lt;/code&gt;&lt;/strong&gt; — each recipient works on its own clone. &lt;code&gt;Clone()&lt;/code&gt; copies &lt;code&gt;Properties&lt;/code&gt; &lt;strong&gt;shallowly&lt;/strong&gt; (more on this in the transactions section), which matters for a consistent commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SemaphoreSlim(maxDop)&lt;/code&gt;&lt;/strong&gt; — parallelism is capped. &lt;code&gt;MaxDegreeOfParallelism=0&lt;/code&gt; means &lt;code&gt;Environment.ProcessorCount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregation in index order.&lt;/strong&gt; Even if &lt;code&gt;fraud&lt;/code&gt; answers first, the fold runs &lt;code&gt;pricing → inventory → fraud&lt;/code&gt;. Predictability you can lean on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ReleaseScopes()&lt;/code&gt; early&lt;/strong&gt; — the clone's DI scopes are released right after the send, while body and headers stay alive for aggregation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Producer cache&lt;/strong&gt; — &lt;code&gt;GetOrCreateProducer&lt;/code&gt; holds a &lt;code&gt;ConcurrentDictionary&amp;lt;string, Lazy&amp;lt;ToProcessor&amp;gt;&amp;gt;&lt;/code&gt;; &lt;code&gt;DisposeAsync&lt;/code&gt; stops every producer it created.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The sequential path
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;ParallelProcessing(false)&lt;/code&gt; — different code, and a different clone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recipients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloneLinked&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// ← CloneLinked(), not Clone()&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetOrCreateProducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_aggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Clone()&lt;/code&gt; vs &lt;code&gt;CloneLinked()&lt;/code&gt; difference isn't cosmetic — covered under transactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error handling: best-effort vs stop-on-exception
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;StopOnException(false)&lt;/code&gt; (default, best-effort): a failed branch gets its &lt;code&gt;clone.Exception&lt;/code&gt; set and is &lt;strong&gt;still&lt;/strong&gt; handed to the aggregator — your strategy decides what to do with a branch carrying an &lt;code&gt;Exception&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_stopOnException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aggregated&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_aggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Timeout in best-effort: the sequential path aggregates the partial set and bails (&lt;code&gt;break&lt;/code&gt;); the parallel path drops the timed-out branches (&lt;code&gt;null&lt;/code&gt;). &lt;code&gt;StopOnException(true)&lt;/code&gt; propagates the first failure, and a timeout is wrapped into a &lt;code&gt;TimeoutException&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_timeout&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Scatter-gather timed out after &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_timeout&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Every parameter
&lt;/h3&gt;

&lt;p&gt;The full contract — &lt;a href="//../../redb.Route/src/redb.Route/Abstractions/IScatterGatherDefinition.cs"&gt;&lt;code&gt;IScatterGatherDefinition&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Recipients(params string[])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Static list of recipient URIs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Recipients(Func&amp;lt;IExchange, IEnumerable&amp;lt;string&amp;gt;&amp;gt;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Dynamic — computed from the live message.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AggregationStrategy(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pair-wise &lt;code&gt;(acc, cur) → merged&lt;/code&gt;. Missing it fails the build.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ParallelProcessing(bool)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parallel or sequential.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MaxDegreeOfParallelism(int)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; → ProcessorCount&lt;/td&gt;
&lt;td&gt;Concurrency cap.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;StopOnException(bool)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;best-effort or fail-fast.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Timeout(TimeSpan)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Zero&lt;/code&gt; (none)&lt;/td&gt;
&lt;td&gt;Whole-operation deadline.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fully-knobbed fluent form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ScatterGather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sg&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sg&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Recipients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://pricing:8080/quote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"http://inventory:8080/check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"http://fraud:8080/score"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParallelProcessing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxDegreeOfParallelism&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopOnException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dynamic recipients
&lt;/h3&gt;

&lt;p&gt;The recipient list can be computed from the message — say, a fan-out across shards whose numbers arrived in a header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ScatterGather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sg&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sg&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Recipients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"shards"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;','&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"http://shard-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s"&gt;:8080/query"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;MergeJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's essentially a Dynamic Recipient List inside Scatter-Gather — exactly what EIP primitives exist for.&lt;/p&gt;

&lt;h3&gt;
  
  
  A real production example: one HTTP request → six parallel aggregations
&lt;/h3&gt;

&lt;p&gt;Enough synthetic snippets — here's a real production route (a logistics monitoring dashboard). The &lt;code&gt;POST /api/tsum/routes&lt;/code&gt; endpoint returns a page of routes &lt;strong&gt;plus&lt;/strong&gt; five different aggregate blocks for the dashboard widgets. That used to be six sequential database round-trips; here it's a single Scatter-Gather:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5090/api/tsum/routes?inOut=true&amp;amp;cors=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tsum-api-routes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;// authentication&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ParseAndStashFilter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// parse the filter → into Properties&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ScatterGather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sg&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sg&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Recipients&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"direct://routes-page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// the result page (pagination)&lt;/span&gt;
            &lt;span class="s"&gt;"direct://routes-ownership"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// aggregate: owned/hired × load status&lt;/span&gt;
            &lt;span class="s"&gt;"direct://routes-route-status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// aggregate: by route status&lt;/span&gt;
            &lt;span class="s"&gt;"direct://routes-point-status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// aggregate: by point status&lt;/span&gt;
            &lt;span class="s"&gt;"direct://routes-territory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// aggregate: vehicles on territory&lt;/span&gt;
            &lt;span class="s"&gt;"direct://routes-departure"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// aggregate: departure stats&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MergeRouteFragments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParallelProcessing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxDegreeOfParallelism&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopOnException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ComposeRoutesResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// assemble the final JSON from the fragments&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each branch is its own &lt;code&gt;direct://&lt;/code&gt; route with its &lt;strong&gt;own&lt;/strong&gt; redb query that drops its slice of the response into a &lt;code&gt;frag:*&lt;/code&gt; property (plus its own timing metric):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct://routes-ownership"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sw&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;BuildFilteredQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// the same filter every branch uses&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GroupBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Own&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadStatus&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;            &lt;span class="c1"&gt;// server-side aggregation in redb&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Own&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Agg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"frag:ownership"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildOwnershipFragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"metric:ownershipMs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElapsedMilliseconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The aggregator just folds each branch clone's &lt;code&gt;frag:*&lt;/code&gt; and &lt;code&gt;metric:*&lt;/code&gt; into the accumulator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="nf"&gt;MergeRouteFragments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frag:"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"metric:"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// branch fragment → into the shared result&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;aggregated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and &lt;code&gt;ComposeRoutesResponse&lt;/code&gt; builds the final JSON out of the six &lt;code&gt;frag:*&lt;/code&gt; pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is "very fast."&lt;/strong&gt; The six branches run &lt;strong&gt;in parallel&lt;/strong&gt; (cap of 4 at a time). The endpoint's latency is the &lt;strong&gt;slowest single branch&lt;/strong&gt;, not the sum of six. If each aggregation is ~80–150 ms, sequentially you'd be at ~0.6–0.9 s; through Scatter-Gather it's ~150 ms. One request from the frontend → one HTTP hop → six server-side redb aggregations at once → one JSON.&lt;/p&gt;

&lt;p&gt;And here everything we covered about transactions lands in practice: each branch's &lt;code&gt;ProcessWithRedb&lt;/code&gt; spins up its &lt;strong&gt;own&lt;/strong&gt; per-exchange redb scope → its &lt;strong&gt;own&lt;/strong&gt; connection (see §10). Six parallel queries on six connections, not sharing one transaction — which is exactly why it &lt;strong&gt;doesn't fall over&lt;/strong&gt;. No &lt;code&gt;.Transacted()&lt;/code&gt; is needed here: the branches only read, and the join is &lt;code&gt;frag:*&lt;/code&gt; in memory. &lt;code&gt;StopOnException(true)&lt;/code&gt; means "no half-built widget" — if any branch fails, the whole response is an error, not partial data.&lt;/p&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9. EIP #2 — Aggregator: assembly over time
&lt;/h2&gt;

&lt;p&gt;Scatter-Gather joins responses &lt;strong&gt;right now&lt;/strong&gt; (fan-out → join). The Aggregator is a different pattern: it collects &lt;strong&gt;independent messages over time&lt;/strong&gt; by a correlation key and emits the merge once a completion predicate trips.&lt;/p&gt;

&lt;p&gt;The &lt;a href="//../../redb.Route/src/redb.Route/Processors/AggregatorProcessor.cs"&gt;&lt;code&gt;AggregatorProcessor&lt;/code&gt;&lt;/a&gt; contract is four things: &lt;code&gt;correlationKey&lt;/code&gt;, &lt;code&gt;aggregationStrategy&lt;/code&gt;, &lt;code&gt;completionPredicate&lt;/code&gt;, target. The core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_correlationKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;IExchange&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_aggregated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_aggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;_aggregated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_completionPredicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;// is the group complete?&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;_aggregated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_aggregated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// first in the group&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_completionPredicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;_aggregated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// only completed groups flow downstream&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A live example from the &lt;a href="//../../redb.Route/demos/redb.Route.Demo/Routes/EipRoutes.cs"&gt;&lt;code&gt;EipRoutes.cs&lt;/code&gt;&lt;/a&gt; demo — collect 3 events sharing a &lt;code&gt;batchId&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"timer://agg-source?period=2000&amp;amp;repeatCount=9"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo-aggregator"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"batchId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"batch-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"event-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;correlationKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"batchId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;aggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldEx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newEx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;oldEx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;oldEx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; + &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;newEx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oldEx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"agg.count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;oldEx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"agg.count"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;oldEx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;completionPredicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"agg.count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[AGG] done — merged 3 events: ${body}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// runs on the completed group&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.Aggregate(...)&lt;/code&gt; opens a scope (&lt;a href="//../../redb.Route/src/redb.Route/Definitions/AggregateDefinition.cs"&gt;&lt;code&gt;AggregateDefinition&lt;/code&gt;&lt;/a&gt;) — the steps after it (&lt;code&gt;.Log(...)&lt;/code&gt;) build the &lt;strong&gt;target pipeline&lt;/strong&gt; that runs on completed groups. Pre-completion messages are consumed silently.&lt;/p&gt;

&lt;p&gt;On Kafka it's the classic "collect N by key":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:order-lines?brokers=kafka:9092&amp;amp;groupId=order-assembler"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;correlationKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"redbKafka.Key"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// key = orderId&lt;/span&gt;
        &lt;span class="n"&gt;aggregationStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;completionPredicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;IsOrderComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders.assembled?brokers=kafka:9092&amp;amp;acks=All"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ By default the inbound offset commits after processing. For an atomic "offset + publish-assembled-result" coupling, use &lt;code&gt;enableAutoCommit=false&lt;/code&gt; + &lt;code&gt;.Transacted()&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The hard limitation, said out loud
&lt;/h3&gt;

&lt;p&gt;The group store is a plain &lt;code&gt;Dictionary&amp;lt;string, IExchange&amp;gt;&lt;/code&gt; behind a &lt;code&gt;lock&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IExchange&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_aggregated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it's &lt;strong&gt;lost on a process restart&lt;/strong&gt; — all in-flight groups vanish;&lt;/li&gt;
&lt;li&gt;there is &lt;strong&gt;no timeout / eviction&lt;/strong&gt; — only the completion predicate. A group that never reaches its condition (you wait for 3 events, 2 arrived, the source died) sits in memory &lt;strong&gt;forever&lt;/strong&gt;. A potential leak if you have many keys that don't always complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; the persistent, recovery-capable aggregator from "big" Camel (completion timeout, persistent repository, recovery). For "collect N by key and emit a batch, a deploy losing in-flight state is acceptable" — great. For "buffer for a day, survive a restart" — you need persistence that isn't here. Honestly. There's a &lt;code&gt;PendingGroupCount&lt;/code&gt; for monitoring how many groups are pending — at least you can see the pile growing.&lt;/p&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Transactions: two models, and that's the whole point
&lt;/h2&gt;

&lt;p&gt;Here's the fork to internalize. redb.Route has &lt;strong&gt;two distinct transaction models&lt;/strong&gt;, and the connectors split into two camps. Getting this fork straight is half of using the framework well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Camp 1: deferred actions (&lt;code&gt;ITransactedAction&lt;/code&gt;) — they do NOT touch &lt;code&gt;System.Transactions&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is &lt;strong&gt;every broker plus redb&lt;/strong&gt;: Kafka, Redis, RabbitMQ, AMQP, IBM MQ, Azure Service Bus, and &lt;strong&gt;redb&lt;/strong&gt; itself. One mechanic for all. The transport doesn't do the "real" work immediately — it parks an &lt;code&gt;ITransactedAction&lt;/code&gt; in the shared bag at &lt;code&gt;exchange.Properties["TRANSACT_ACTION"]&lt;/code&gt; (a &lt;code&gt;ConcurrentDictionary&lt;/code&gt;) under a &lt;strong&gt;per-message-unique&lt;/strong&gt; key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;RegisterTransactedAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ITransactedAction&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TRANSACT_ACTION"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ITransactedAction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;dict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ITransactedAction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"TRANSACT_ACTION"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keys are &lt;code&gt;kafka-send-{guid}&lt;/code&gt;, &lt;code&gt;redis-write-{guid}&lt;/code&gt;, &lt;code&gt;redb:{name}&lt;/code&gt;, an offset, a deliveryTag. Uniqueness is critical: parallel fan-out branches write into &lt;strong&gt;one shared&lt;/strong&gt; bag (&lt;code&gt;Clone()&lt;/code&gt; copies &lt;code&gt;Properties&lt;/code&gt; shallowly → all clones share the dictionary), and without unique keys they'd clobber each other.&lt;/p&gt;

&lt;p&gt;At the &lt;code&gt;.Transacted()&lt;/code&gt; boundary, &lt;a href="//../../redb.Route/src/redb.Route/Transactions/TransactedProcessor.cs"&gt;&lt;code&gt;TransactedProcessor&lt;/code&gt;&lt;/a&gt; takes over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ContainsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TransactActionPropertyKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TransactActionPropertyKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ITransactedAction&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(...);&lt;/span&gt;

    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// System.Transactions.TransactionScope (AsyncFlow enabled)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CommitActions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// commit ALL deferred actions&lt;/span&gt;
        &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;RollbackActions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// on error — roll them all back&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CommitActions&lt;/code&gt; walks the bag &lt;strong&gt;sequentially&lt;/strong&gt; and commits each action — which bites us in the trade-off below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These transports &lt;strong&gt;never touch&lt;/strong&gt; &lt;code&gt;Transaction.Current&lt;/code&gt;. So parallel fan-out over them is safe: unique keys → no bag collisions → no concurrent enlistment in a single &lt;code&gt;System.Transactions&lt;/code&gt; transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;redb&lt;/strong&gt; joins this camp via &lt;a href="//../../redb.Route/src/redb.Route.Core/Transactions/RedbTransactedAction.cs"&gt;&lt;code&gt;RedbTransactedAction&lt;/code&gt;&lt;/a&gt; — it wraps a redb-native &lt;code&gt;IRedbTransaction&lt;/code&gt; (its own &lt;code&gt;BEGIN/COMMIT&lt;/code&gt; on its own connection) and drops it into the same bag under key &lt;code&gt;redb:{name}&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Interlocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exchange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;_completed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// single-use&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Camp 2: SQL — it enlists in &lt;code&gt;System.Transactions&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The SQL connector behaves &lt;strong&gt;fundamentally differently&lt;/strong&gt; (&lt;a href="//../../redb.Route/src/redb.Route.Sql/SqlProducer.cs"&gt;&lt;code&gt;SqlProducer&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateConnectionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// If an ambient TransactionScope exists (route-level .Transacted()), the connection auto-enlists —&lt;/span&gt;
&lt;span class="c1"&gt;// no local transaction needed. Otherwise always wrap in a local one (like EF SaveChanges).&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hasAmbientTx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;DbTransaction&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;hasAmbientTx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... execute ...&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;no&lt;/strong&gt; ambient → each execution is wrapped in a local &lt;code&gt;DbTransaction&lt;/code&gt; on its own connection (atomic, like EF's &lt;code&gt;SaveChanges&lt;/code&gt;, with no &lt;code&gt;?transacted=true&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;an&lt;/strong&gt; ambient &lt;code&gt;Transaction.Current&lt;/code&gt; present (opened by &lt;code&gt;.Transacted()&lt;/code&gt; / &lt;code&gt;.BeginTransaction()&lt;/code&gt;) → it skips the local one and the connection &lt;strong&gt;auto-enlists&lt;/strong&gt; in the ambient &lt;code&gt;TransactionScope&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SQL is the one in-box connector that genuinely participates in &lt;code&gt;System.Transactions&lt;/code&gt;. That's both a feature (one &lt;code&gt;.Transacted()&lt;/code&gt; wrapper around several SQL writes = one atomic transaction, perfect for an outbox write) and a thing whose edges you must understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why parallel Scatter-Gather doesn't blow up under a transaction
&lt;/h3&gt;

&lt;p&gt;The reasonable fear: parallel branches run on different threads, &lt;code&gt;Transaction.Current&lt;/code&gt; flows into each via &lt;code&gt;ExecutionContext&lt;/code&gt; — won't that be concurrent use of one transaction and a promotion to MSDTC (which on PG/Linux simply throws)?&lt;/p&gt;

&lt;p&gt;No. Here's why — from the code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Scopes are created per-exchange.&lt;/strong&gt; A named redb instance resolves through &lt;code&gt;GetRedbService(name, exchange)&lt;/code&gt; (&lt;a href="//../../redb.Route/src/redb.Route.Core/Extensions/RedbRouteExtensions.cs"&gt;&lt;code&gt;RedbRouteExtensions&lt;/code&gt;&lt;/a&gt;), which caches a DI scope under &lt;code&gt;exchange.Properties["__redb_scope:{name}"]&lt;/code&gt; and pulls the service from &lt;em&gt;that&lt;/em&gt; scope's provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cacheKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScopeCachePrefix&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cleanName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// "__redb_scope:orders-db"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;IServiceScope&lt;/span&gt; &lt;span class="n"&gt;cachedScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cachedScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IRedbService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                    &lt;span class="c1"&gt;// not cached — create our own&lt;/span&gt;
&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IRedbService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;Clone()&lt;/code&gt; deliberately does NOT copy scopes.&lt;/strong&gt; From &lt;a href="//../../redb.Route/src/redb.Route/Core/Exchange.cs"&gt;&lt;code&gt;Exchange.cs&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Named redb scopes are per-exchange; the child creates its own on first access.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"__redb_scope:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So each parallel branch spins up its &lt;strong&gt;own&lt;/strong&gt; scope → its &lt;strong&gt;own&lt;/strong&gt; &lt;code&gt;IRedbService&lt;/code&gt; → its &lt;strong&gt;own connection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. redb never enlists in &lt;code&gt;System.Transactions&lt;/code&gt;&lt;/strong&gt; — it uses its own &lt;code&gt;IRedbTransaction&lt;/code&gt;, parked in &lt;code&gt;TRANSACT_ACTION&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Net result: &lt;strong&gt;there is no single &lt;code&gt;System.Transactions&lt;/code&gt; transaction being hit concurrently by multiple connections → no MSDTC promotion, no "transaction context in use by another thread," no crash.&lt;/strong&gt; Different connections, not one transaction — that's the design, not a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same story for SQL.&lt;/strong&gt; In a Scatter-Gather &lt;em&gt;without&lt;/em&gt; a &lt;code&gt;.Transacted()&lt;/code&gt; wrapper, each branch opens its own connection (&lt;code&gt;CreateConnectionAsync&lt;/code&gt; per &lt;code&gt;Process&lt;/code&gt;) and its own local &lt;code&gt;DbTransaction&lt;/code&gt; → independent atomic writes → no crash. The only way to hit the multi-connection-enlistment-to-MSDTC crash is to &lt;em&gt;deliberately&lt;/em&gt; wrap a parallel fan-out in &lt;code&gt;.Transacted()&lt;/code&gt; (an ambient &lt;code&gt;System.Transactions&lt;/code&gt; scope) &lt;strong&gt;and&lt;/strong&gt; fan out to several SQL connections — on PG/Npgsql that escalates to a distributed transaction and throws. A narrow edge nobody walks into, because the deferred model exists for brokers/redb.&lt;/p&gt;

&lt;h3&gt;
  
  
  So what is the 3.2.0 &lt;code&gt;DependentTransactionBranch&lt;/code&gt; for, then?
&lt;/h3&gt;

&lt;p&gt;That 3.2.0 per-branch isolation fix is wired &lt;strong&gt;only into &lt;code&gt;Multicast&lt;/code&gt; and &lt;code&gt;Splitter&lt;/code&gt;&lt;/strong&gt; (plus the helper itself) — nowhere else. Here's its core (&lt;a href="//../../redb.Route/src/redb.Route/Transactions/DependentTransactionBranch.cs"&gt;&lt;code&gt;DependentTransactionBranch&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ambient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ambient&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// no transaction — zero overhead&lt;/span&gt;

    &lt;span class="c1"&gt;// Fork a dependent clone: this branch's enlistment is private to its thread.&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dependent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ambient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DependentClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DependentCloneOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BlockCommitUntilComplete&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TransactionScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dependent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TransactionScopeAsyncFlowOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;dependent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// release the parent commit (it waited on BlockCommitUntilComplete)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It matters &lt;strong&gt;only&lt;/strong&gt; for resources that genuinely enlist in &lt;code&gt;Transaction.Current&lt;/code&gt; — i.e. when parallel Multicast/Splitter branches do inline work on an enlisting connection. Scatter-Gather &lt;strong&gt;doesn't need it&lt;/strong&gt;: per-exchange scopes + deferred &lt;code&gt;ITransactedAction&lt;/code&gt; already give isolation without sharing a connection. Not a missing-feature asymmetry — two mechanisms for two situations.&lt;/p&gt;

&lt;h3&gt;
  
  
  The honest trade-off (a design choice, not a bug)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;TransactedProcessor.CommitActions&lt;/code&gt; commits the deferred actions &lt;strong&gt;sequentially in a loop&lt;/strong&gt;, and Scatter-Gather/Multicast branches are &lt;strong&gt;different connections, different transactions&lt;/strong&gt;. So: if branch A commits and branch B's commit throws, you get a &lt;strong&gt;partial commit&lt;/strong&gt;. No cross-branch DB atomicity here. That's the price of "no crash, no MSDTC promotion." If you need real cross-branch atomicity with enlistment, that's &lt;code&gt;Multicast&lt;/code&gt;/&lt;code&gt;Splitter&lt;/code&gt; under &lt;code&gt;.Transacted()&lt;/code&gt; with the 3.2.0 fix — not Scatter-Gather.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transaction policies
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.Transacted()&lt;/code&gt; is backed by &lt;a href="//../../redb.Route/src/redb.Route/Transactions/TransactionPolicy.cs"&gt;&lt;code&gt;TransactionPolicy&lt;/code&gt;&lt;/a&gt; — four ready-made:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;&lt;code&gt;ScopeOption&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Required&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Join the ambient or create a new one. 30s timeout, &lt;code&gt;ReadCommitted&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RequiresNew&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RequiresNew&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Always a new transaction; the ambient is suspended.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Suppress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Suppress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run with no transaction (ambient suppressed).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Mandatory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(marker)&lt;/td&gt;
&lt;td&gt;Requires an existing ambient, else &lt;code&gt;InvalidOperationException&lt;/code&gt; at scope creation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;TransactionDefinition&lt;/code&gt; also gives Camel-parity hooks: &lt;code&gt;.Retry(attempts, delay)&lt;/code&gt; (wraps the body in a &lt;code&gt;RetryProcessor&lt;/code&gt;) and &lt;code&gt;.DeadLetterChannel(uri)&lt;/code&gt; (on failure after rollback, send the exchange to the DLC). So a transactional block with retries and dead-letter is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders?brokers=kafka:9092&amp;amp;groupId=w"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Transacted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sql:INSERT INTO orders ..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders.done?brokers=kafka:9092&amp;amp;transacted=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  11. The parallel cousins: Splitter and Multicast
&lt;/h2&gt;

&lt;p&gt;Scatter-Gather isn't the only fan-out in the family. Next to it sit &lt;code&gt;Splitter&lt;/code&gt; (split the body into parts, process each) and &lt;code&gt;Multicast&lt;/code&gt; (send a copy to N processors). Both, like Scatter-Gather, have an &lt;strong&gt;optional&lt;/strong&gt; aggregation, parallelism, and &lt;code&gt;stopOnException&lt;/code&gt;. The difference is where the "branches" come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scatter-Gather&lt;/strong&gt;: branches = endpoints (producer URIs), aggregator &lt;strong&gt;mandatory&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multicast&lt;/strong&gt;: branches = processors / sub-pipelines, aggregator &lt;strong&gt;optional&lt;/strong&gt; (&lt;a href="//../../redb.Route/src/redb.Route/Processors/MulticastProcessor.cs"&gt;&lt;code&gt;MulticastProcessor&lt;/code&gt;&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splitter&lt;/strong&gt;: branches = body parts (&lt;a href="//../../redb.Route/src/redb.Route/Processors/SplitterProcessor.cs"&gt;&lt;code&gt;SplitterProcessor&lt;/code&gt;&lt;/a&gt;), aggregator &lt;strong&gt;optional&lt;/strong&gt;, with some Camel-compat nuances.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Splitter is actually the most feature-rich on aggregation: its strategy &lt;code&gt;(IExchange?, IExchange) → IExchange&lt;/code&gt; is called &lt;strong&gt;even for the first part&lt;/strong&gt; with &lt;code&gt;oldExchange == null&lt;/code&gt; (Camel's seed/wrap contract), and there are flags &lt;code&gt;parallelAggregate&lt;/code&gt; (aggregate inline under a lock from workers instead of a deterministic post-pass) and &lt;code&gt;aggregateOnException&lt;/code&gt; (include failed parts in the aggregate). And it's Splitter/Multicast's parallel path that carries the &lt;code&gt;DependentTransactionBranch.RunAsync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MulticastProcessor.ProcessParallel / SplitterProcessor.ProcessParallel&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;DependentTransactionBranch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_targets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why they have it and Scatter-Gather doesn't — covered above: Splitter/Multicast run &lt;strong&gt;inline processors&lt;/strong&gt; (which can open an enlisting connection right in the branch), whereas Scatter-Gather runs &lt;strong&gt;producers by URI&lt;/strong&gt; with per-exchange scopes. Different risks, different mechanisms.&lt;/p&gt;

&lt;p&gt;A simple Multicast from the demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct://demo-multicast"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Multicast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"direct://mcast-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"direct://mcast-b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"direct://mcast-c"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;parallelProcessing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[MCAST] ◀ All endpoints received the message"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Saga — "a slightly different one"
&lt;/h2&gt;

&lt;p&gt;redb.Route has a saga, but it's &lt;strong&gt;not&lt;/strong&gt; MassTransit's durable state machine. It's &lt;a href="//../../redb.Route/src/redb.Route/Processors/SagaProcessor.cs"&gt;&lt;code&gt;SagaProcessor&lt;/code&gt;&lt;/a&gt; — an in-process compensating saga within a single exchange:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IExchange&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;completedCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;_steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_steps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;completedCount&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Compensate completed steps in REVERSE order&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completedCount&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;--)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_steps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Compensate&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_steps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Compensate&lt;/span&gt;&lt;span class="p"&gt;!(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;compEx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compEx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Saga compensation for step {i} failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_onCompletion&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_onCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compensation failures are logged and &lt;strong&gt;don't abort&lt;/strong&gt; the rest of the rollback. The DSL (&lt;a href="//../../redb.Route/src/redb.Route/Definitions/SagaDefinition.cs"&gt;&lt;code&gt;SagaDefinition&lt;/code&gt;&lt;/a&gt;) comes in two styles, callback and fluent-scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:checkout"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Saga&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reserve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compensate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unreserve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="n"&gt;compensate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="c1"&gt;// forward-only, no compensation&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order placed"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bluntly: &lt;strong&gt;no&lt;/strong&gt; state persistence, &lt;strong&gt;no&lt;/strong&gt; correlation across messages/time, &lt;strong&gt;does not&lt;/strong&gt; survive a process restart. It's Camel-style "routing slip with compensation," not a persisted state machine. Only the name overlaps. There are metrics, by the way: &lt;code&gt;SagaCompleted&lt;/code&gt; / &lt;code&gt;SagaCompensated&lt;/code&gt; / &lt;code&gt;SagaFailed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Again — not a shortcoming, a different school. Need a durable saga with persistence over Kafka? Build it from &lt;code&gt;Aggregator&lt;/code&gt; + a SQL/Redis store + a correlation key. The bricks are there.&lt;/p&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Outbox — there isn't one, and that's correct
&lt;/h2&gt;

&lt;p&gt;Grep across &lt;code&gt;src&lt;/code&gt; for &lt;code&gt;Outbox&lt;/code&gt; — &lt;strong&gt;zero&lt;/strong&gt;. No built-in outbox. That's &lt;strong&gt;not a gap&lt;/strong&gt;, it's a stance: the transactional outbox is a &lt;em&gt;pattern&lt;/em&gt;, not a framework button. Baking it into the core is over-engineering; anyone who wants one wires their own in five lines for their own invariants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Write: business data + an outbox row under one transaction.&lt;/span&gt;
&lt;span class="c1"&gt;//    For SQL this is exactly where the System.Transactions enlistment works FOR you:&lt;/span&gt;
&lt;span class="c1"&gt;//    one .Transacted() wrapper → one ambient scope → atomic.&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:place-order"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Transacted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sql:INSERT INTO orders(id, payload) VALUES (@id, @payload)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sql:INSERT INTO outbox(id, topic, payload, sent) VALUES (@id, 'orders', @payload, false)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Deliver: a separate poller route reads the outbox and publishes.&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sql:SELECT * FROM outbox WHERE sent = false ORDER BY id?outputType=SelectList&amp;amp;delay=1000"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;)&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// one message per row&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka:orders?brokers=kafka:9092"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sql:UPDATE outbox SET sent = true WHERE id = @id"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want the inbox pattern, dedup by &lt;code&gt;messageId&lt;/code&gt;, a TTL on rows, a claim-check for big payloads? Add it. Nobody forces their outbox schema or retry semantics on you. That &lt;em&gt;is&lt;/em&gt; "moving toward Camel": &lt;strong&gt;less baked-in magic, more explicit assembly&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  14. Bottom line: a trade, not "better/worse"
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;MassTransit&lt;/th&gt;
&lt;th&gt;redb.Route (Camel school)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Saga&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;durable state machine, persisted, survives restarts&lt;/td&gt;
&lt;td&gt;in-process compensation within an exchange&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Outbox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a framework feature&lt;/td&gt;
&lt;td&gt;your own route from SQL/Redis in five lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transactions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;a framework abstraction&lt;/td&gt;
&lt;td&gt;two explicit models: deferred &lt;code&gt;ITransactedAction&lt;/code&gt; (brokers+redb) / &lt;code&gt;System.Transactions&lt;/code&gt; (SQL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kafka "transactional"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EOS out of the box&lt;/td&gt;
&lt;td&gt;idempotent producer + deferred commit (not EOS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Philosophy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;batteries included, do it their way&lt;/td&gt;
&lt;td&gt;EIP + connectors + DSL, compose it yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This isn't "redb.Route beats MassTransit." It's a &lt;strong&gt;different trade&lt;/strong&gt;: less baked-in magic, more explicit assembly from primitives. If your scenarios don't fit someone else's saga/outbox model, the Camel approach lets you assemble exactly yours. If they &lt;em&gt;do&lt;/em&gt; fit, maybe you don't need to leave at all.&lt;/p&gt;

&lt;p&gt;Scatter-Gather, for the record, has been rock-solid for us in production — one processor that fans out &lt;em&gt;and&lt;/em&gt; joins, in parallel, with a deterministic merge order and transaction semantics you can actually reason about. Exactly the case where an EIP primitive solves the real problem without a single line of infrastructure code.&lt;/p&gt;

&lt;p&gt;We run &lt;strong&gt;3.2.0&lt;/strong&gt; in production — &lt;a href="https://github.com/redbase-app/redb-route/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG here&lt;/a&gt;. War stories, things to re-verify (looking at you, transacted Kafka), which EIP to dissect next — drop them in the comments and we'll dig through the code together.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
      <category>kafka</category>
    </item>
    <item>
      <title>SQLite provider anatomy: moving away from EF Core — typed object storage for desktop, mobile, and Blazor WASM</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Mon, 29 Jun 2026 21:53:41 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/sqlite-provider-anatomy-moving-away-from-ef-core-typed-object-storage-for-desktop-mobile-and-13pc</link>
      <guid>https://dev.to/rinat_kozin/sqlite-provider-anatomy-moving-away-from-ef-core-typed-object-storage-for-desktop-mobile-and-13pc</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpqx471tjdaip7hi4jdoo.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpqx471tjdaip7hi4jdoo.webp" alt="redb.sqlite" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; redb — ecosystem, engineering teardown following the 3.2.0 SQLite announcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;Full anatomy here, recommend a quick look at redbase.app first for context: &lt;a href="https://redbase.app/" rel="noopener noreferrer"&gt;https://redbase.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When we &lt;a href="https://dev.to/rinat_kozin/sqlite-provider-for-redbase-is-coming-full-linq-typed-columns-same-api-as-postgresql-and-13ie"&gt;shipped the SQLite provider in redb 3.2.1&lt;/a&gt;, the pitch was two sentences: &lt;em&gt;same LINQ, one line in DI.&lt;/em&gt; This post is the opposite of that pitch. Not "what shipped" — &lt;strong&gt;how it's built and where it leaked&lt;/strong&gt;. Concretely: how redb's query engine moved into a native C extension on a database with no stored procedures; how we store &lt;code&gt;DateTimeOffset&lt;/code&gt; in a database that has no date type; and three real bugs from this release, each with the filter JSON, the generated SQL, and the fix.&lt;/p&gt;

&lt;p&gt;It's long and it has code. If you want the short version, the announcement is linked above. If you want what's under "one line in DI," pull up a chair.&lt;/p&gt;

&lt;p&gt;For context, if redb is new to you (the rest assumes you've seen this):&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin/two-tables-zero-migrations-a-different-way-to-model-data-in-net-2482"&gt;Two tables, zero migrations: a typed object store for .NET over Postgres/MSSQL — full LINQ, no Include&lt;/a&gt; — the what and why.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin/redb-inside-part-1-the-shape-of-the-data-18mf"&gt;redb inside, part 1: the 13 tables everything runs on&lt;/a&gt; — the storage model. &lt;strong&gt;Load-bearing for this post:&lt;/strong&gt; the SQLite provider had to reproduce it one-for-one.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin/why-our-free-was-so-far-behind-pro-and-what-we-just-shipped-in-redbase-300-with-the-actual-sql-i7b"&gt;Why our Free was so far behind Pro — and what we shipped in 3.0.0 (with the actual SQL)&lt;/a&gt; — what "Free = server-side functions" means, which is exactly what we had to port to C here.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin/sqlite-provider-for-redbase-is-coming-full-linq-typed-columns-same-api-as-postgresql-and-13ie"&gt;SQLite provider for RedBase is coming — the 3.2.1 teaser&lt;/a&gt; — the short version of what this post takes apart.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two disclaimers before we start, so nothing trips us up later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One — about a word.&lt;/strong&gt; Yes, redb's model is "flexible": a class is spread across rows in a values table. No, this is &lt;strong&gt;not EAV&lt;/strong&gt; in the dismissive sense people throw that acronym around. What lives in a redb database is &lt;strong&gt;RTTI&lt;/strong&gt; — real type information: schemes, structures, field types, references. The DB &lt;em&gt;knows&lt;/em&gt; that &lt;code&gt;EmployeeProps.HireDate&lt;/code&gt; is a &lt;code&gt;DateTime&lt;/code&gt;, that &lt;code&gt;Contacts&lt;/code&gt; is an array of objects, that &lt;code&gt;CurrentProject&lt;/code&gt; is a reference to another scheme. That's a runtime type system at the storage layer, not a blind bag of tuples. You'll see below why neither the materializer nor the query compiler could physically exist without it — at every step they need the type of the thing they're materializing or filtering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two — about fit, so nobody's disappointed later.&lt;/strong&gt; redb is for &lt;strong&gt;complex business classes&lt;/strong&gt;: object graphs, nesting, cross-scheme references, trees, dictionaries, arrays of objects. Pumping &lt;strong&gt;flat, high-volume streams&lt;/strong&gt; into it — a coordinate feed, sensor telemetry, thousands of metrics a second — technically works, but it's an &lt;strong&gt;anti-pattern&lt;/strong&gt;. That's what time-series and columnar stores exist for; redb pays for its flexibility and typing exactly where the data is rich and connected, not where you've got one &lt;code&gt;(timestamp, value)&lt;/code&gt; table at a billion rows. Short version: redb shines when you have a real domain model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 0. The constraint that shapes everything
&lt;/h2&gt;

&lt;p&gt;SQLite has &lt;strong&gt;no procedural language&lt;/strong&gt;. No PL/pgSQL, no T-SQL — nowhere to put server-side logic. And redb's "Free" tier on Postgres and MSSql is built exactly around having that: the heavy machinery (query compiler, materializer, soft-delete, permission views) lives &lt;strong&gt;inside the database&lt;/strong&gt; as server-side functions. That's literally the Free/Pro line: where JSON gets materialized and who generates the SQL.&lt;/p&gt;

&lt;p&gt;"No stored procedures" forks into exactly two paths, and we took both — one per tier:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro — pure C#.&lt;/strong&gt; &lt;code&gt;ProSqlBuilder&lt;/code&gt; generates the query SQL in code, props materialize in code, &lt;strong&gt;zero&lt;/strong&gt; DB-function calls. Consequence: it runs anywhere &lt;code&gt;Microsoft.Data.Sqlite&lt;/code&gt; runs — including &lt;strong&gt;Blazor WebAssembly&lt;/strong&gt; and &lt;strong&gt;mobile (MAUI/iOS/Android)&lt;/strong&gt;, where you flat-out cannot load a native SQLite extension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free — a native C extension.&lt;/strong&gt; Same shape as Postgres/MSSql: the engine lives &lt;em&gt;in the database&lt;/em&gt;. On SQLite, "in the database" means a loadable extension written in C (&lt;code&gt;redb.dll&lt;/code&gt; / &lt;code&gt;.so&lt;/code&gt; / &lt;code&gt;.dylib&lt;/code&gt;) on top of &lt;code&gt;sqlite3ext.h&lt;/code&gt;. It runs wherever native code loads: desktop, server, CI — and it lets a non-.NET host (Python, the &lt;code&gt;sqlite3&lt;/code&gt; CLI) talk to a redb database directly if it wants to.&lt;/p&gt;

&lt;p&gt;Both stories at full size below. Native first (it's the fun one), then dates (the important one), then the bugs, then the traps.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 0.5. What the provider reproduces: &lt;code&gt;_objects&lt;/code&gt; and &lt;code&gt;_values&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before diving into the native side — what it actually reads and writes. redb's model is ~13 tables (full teardown in the "13 tables" post), but two are load-bearing for the SQLite story, and the provider had to recreate both column-for-column.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_objects&lt;/code&gt; is the "header" of every object: identity, tree, ownership, dates, the reference to a scheme (i.e. to a type):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_id&lt;/span&gt;             &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_parent&lt;/span&gt;      &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- tree (parent)&lt;/span&gt;
    &lt;span class="n"&gt;_id_scheme&lt;/span&gt;      &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- RTTI: which CLASS this object is&lt;/span&gt;
    &lt;span class="n"&gt;_id_owner&lt;/span&gt;       &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_who_change&lt;/span&gt;  &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_date_create&lt;/span&gt;    &lt;span class="nb"&gt;REAL&lt;/span&gt;    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;  &lt;span class="c1"&gt;-- UTC Julian day (REAL)&lt;/span&gt;
    &lt;span class="n"&gt;_date_modify&lt;/span&gt;    &lt;span class="nb"&gt;REAL&lt;/span&gt;    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;  &lt;span class="c1"&gt;-- UTC Julian day (REAL)&lt;/span&gt;
    &lt;span class="n"&gt;_name&lt;/span&gt;           &lt;span class="nb"&gt;TEXT&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_hash&lt;/span&gt;           &lt;span class="nb"&gt;TEXT&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- hash of the props set (delta/cache)&lt;/span&gt;
    &lt;span class="c1"&gt;-- slots for RedbPrimitive&amp;lt;T&amp;gt; (when Props IS the primitive, no nested structure)&lt;/span&gt;
    &lt;span class="n"&gt;_value_long&lt;/span&gt;     &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_string&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_bool&lt;/span&gt;     &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- bool = 0/1&lt;/span&gt;
    &lt;span class="n"&gt;_value_double&lt;/span&gt;   &lt;span class="nb"&gt;REAL&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_numeric&lt;/span&gt;  &lt;span class="nb"&gt;REAL&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- NUMERIC(38,18): REAL by default&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_values&lt;/code&gt; is the row-per-property store. One row per (object, structure, [array index]). The key idea is &lt;strong&gt;typed column slots&lt;/strong&gt;: a value lives not in one "universal" text column, but in the column for its type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_id&lt;/span&gt;              &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_structure&lt;/span&gt;    &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- RTTI: WHICH field this is&lt;/span&gt;
    &lt;span class="n"&gt;_id_object&lt;/span&gt;       &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_String&lt;/span&gt;          &lt;span class="nb"&gt;TEXT&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Long&lt;/span&gt;            &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Guid&lt;/span&gt;            &lt;span class="nb"&gt;TEXT&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Double&lt;/span&gt;          &lt;span class="nb"&gt;REAL&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt;  &lt;span class="nb"&gt;REAL&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- DateTime/DateTimeOffset/DateOnly as UTC Julian&lt;/span&gt;
    &lt;span class="n"&gt;_Boolean&lt;/span&gt;         &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- 0/1&lt;/span&gt;
    &lt;span class="n"&gt;_ByteArray&lt;/span&gt;       &lt;span class="nb"&gt;BLOB&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Numeric&lt;/span&gt;         &lt;span class="nb"&gt;REAL&lt;/span&gt;    &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_ListItem&lt;/span&gt;        &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Object&lt;/span&gt;          &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- reference to another object&lt;/span&gt;
    &lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;-- arrays/dictionaries — relational&lt;/span&gt;
    &lt;span class="n"&gt;_array_index&lt;/span&gt;     &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two consequences that everything else rests on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typed slots, not "everything as a string."&lt;/strong&gt; &lt;code&gt;_Boolean&lt;/code&gt; is &lt;code&gt;0/1&lt;/code&gt;, &lt;code&gt;_DateTimeOffset&lt;/code&gt; is REAL Julian, &lt;code&gt;_Long&lt;/code&gt; is an integer. So comparisons and sorts in SQL run on the column's native type (and use indexes) instead of a string cast. That's the "not EAV" part: &lt;code&gt;_values&lt;/code&gt; is a typed props store, and &lt;code&gt;_id_structure&lt;/code&gt; → &lt;code&gt;_structures&lt;/code&gt; carries RTTI about which field this is and what type it is. The query compiler needs the field's type at every step — to pick the slot column for &lt;code&gt;MAX(...) FILTER&lt;/code&gt; in the pivot; without RTTI it wouldn't know which column &lt;code&gt;LastName&lt;/code&gt; comes out of.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrays and dictionaries are relational&lt;/strong&gt;, via &lt;code&gt;_array_parent_id&lt;/code&gt;/&lt;code&gt;_array_index&lt;/code&gt;, not a JSON blob. So the materializer assembles them with &lt;code&gt;GROUP BY&lt;/code&gt; over the index, and the compiler can filter through them (&lt;code&gt;_array_index IS NULL&lt;/code&gt; in the pivot is exactly what separates a field's scalar row from its array elements).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From here, "the engine in the database" = functions that read/write exactly these two tables, checking the metadata in &lt;code&gt;_schemes&lt;/code&gt;/&lt;code&gt;_structures&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1. The native extension: anatomy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Entry point
&lt;/h3&gt;

&lt;p&gt;A SQLite loadable extension is a &lt;code&gt;.so&lt;/code&gt;/&lt;code&gt;.dll&lt;/code&gt;/&lt;code&gt;.dylib&lt;/code&gt; with one exported init function. The default name is derived from the file basename: for &lt;code&gt;redb.dll&lt;/code&gt;, that's &lt;code&gt;sqlite3_redb_init&lt;/code&gt;. Inside, it registers all our SQL functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;SQLITE_EXTENSION_INIT1&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;sqlite3_redb_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;pzErrMsg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sqlite3_api_routines&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pApi&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
  &lt;span class="n"&gt;SQLITE_EXTENSION_INIT2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pApi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3_create_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get_object_json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SQLITE_UTF8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="n"&gt;getObjectJsonFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;SQLITE_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3_create_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get_object_json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SQLITE_UTF8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="n"&gt;getObjectJsonFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// overload: (_id, max_depth)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;SQLITE_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3_create_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"save_object_json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SQLITE_UTF8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="n"&gt;saveObjectJsonFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;SQLITE_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;rc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redbRegisterPvt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// the whole pvt_* compiler&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SQLITE_EXTENSION_INIT1&lt;/code&gt;/&lt;code&gt;INIT2&lt;/code&gt; are macros from &lt;code&gt;sqlite3ext.h&lt;/code&gt; that rewrite direct &lt;code&gt;sqlite3_*&lt;/code&gt; calls into calls through the &lt;code&gt;pApi&lt;/code&gt; pointer table. The gotcha that's easy to miss: after &lt;code&gt;INIT2&lt;/code&gt;, &lt;strong&gt;every&lt;/strong&gt; SQLite API call inside the extension goes through that table. Forget &lt;code&gt;INIT2&lt;/code&gt; and the extension compiles fine — then segfaults on the first &lt;code&gt;sqlite3_*&lt;/code&gt; call through a garbage pointer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading — on every connection
&lt;/h3&gt;

&lt;p&gt;The defining trait of loadable extensions: they are &lt;strong&gt;not persistent&lt;/strong&gt;. SQLite forgets registered functions on a new connection. And &lt;code&gt;Microsoft.Data.Sqlite&lt;/code&gt; pools connections. So you must load the extension &lt;strong&gt;on every&lt;/strong&gt; connection, after PRAGMAs. Our wrapper around &lt;code&gt;SqliteConnection&lt;/code&gt; does it: open → set &lt;code&gt;foreign_keys=ON&lt;/code&gt; and &lt;code&gt;busy_timeout&lt;/code&gt; → load the extension → hand it to the pool.&lt;/p&gt;

&lt;p&gt;The binary is located by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An explicit &lt;code&gt;SqliteDataSource.NativeExtensionPath&lt;/code&gt; (if set in code).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;REDB_SQLITE_EXTENSION&lt;/code&gt; environment variable.&lt;/li&gt;
&lt;li&gt;Otherwise &lt;code&gt;redb.{dll,so,dylib}&lt;/code&gt; from the NuGet &lt;code&gt;runtimes/&amp;lt;rid&amp;gt;/native/&lt;/code&gt; folder; in dev, by walking up the directory tree to &lt;code&gt;redb.SQLite/native/build&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pro never sets a path: it doesn't need the native code, and in WASM/mobile there wouldn't be any.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shipping the binary: why it cross-compiles for free
&lt;/h3&gt;

&lt;p&gt;The extension is a loadable module, and that has a packaging payoff people don't expect: it links against &lt;strong&gt;nothing&lt;/strong&gt;. &lt;code&gt;sqlite3ext.h&lt;/code&gt; hands you the API as a pointer table resolved from the host at load time (that's what &lt;code&gt;SQLITE_EXTENSION_INIT2&lt;/code&gt; wires up) — so there's no &lt;code&gt;libsqlite3&lt;/code&gt; to find, no import library, no target sysroot. Cross-compiling collapses to "point CMake at a cross compiler":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# linux-arm64 from an x64 box, in a throwaway container — no sysroot needed&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;:/work"&lt;/span&gt; debian:bookworm bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
  apt-get update &amp;amp;&amp;amp; apt-get install -y cmake make gcc-aarch64-linux-gnu
  cd /work/redb.SQLite/native
  cmake -S . -B build-linux-arm64 -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc
  cmake --build build-linux-arm64'&lt;/span&gt;
&lt;span class="c"&gt;# → build-linux-arm64/redb.so: ELF 64-bit LSB shared object, ARM aarch64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A native lib that &lt;em&gt;linked&lt;/em&gt; libsqlite3 would need the arm64 build of that library to link against; this one doesn't, because it only ever calls SQLite through the host's pointer table. One cross-gcc, one valid arm64 binary.&lt;/p&gt;

&lt;p&gt;Delivery has its own twist. SQLite loads the extension by an &lt;strong&gt;explicit path&lt;/strong&gt; (&lt;code&gt;conn.LoadExtension(...)&lt;/code&gt;), not like a P/Invoke native lib the host resolves from the NuGet cache — so the file must physically sit in the app output. A RID-targeted &lt;code&gt;dotnet publish -r &amp;lt;rid&amp;gt;&lt;/code&gt; flattens &lt;code&gt;runtimes/&amp;lt;rid&amp;gt;/native/&lt;/code&gt; for you; a framework-dependent build (no RID) doesn't, so the package ships a &lt;code&gt;buildTransitive&lt;/code&gt; .targets that copies the OS-matching binary into the consumer's output, &lt;code&gt;Exists&lt;/code&gt;-gated per RID. That's why "ship one more platform" = "produce one more &lt;code&gt;redb.{so,dylib}&lt;/code&gt; and drop it in &lt;code&gt;native/build-&amp;lt;rid&amp;gt;/&lt;/code&gt;" — the csproj and .targets already enumerate all five RIDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  War story #5: the &lt;code&gt;.targets&lt;/code&gt; that broke every consumer (3.2.0 → 3.2.1)
&lt;/h3&gt;

&lt;p&gt;That delivery &lt;code&gt;.targets&lt;/code&gt; is also where this release drew blood — and since this is an honest teardown, here it is. We shipped &lt;strong&gt;3.2.1&lt;/strong&gt; with a banner comment in &lt;code&gt;redb.SQLite.targets&lt;/code&gt; that had a row of &lt;code&gt;-&lt;/code&gt; characters &lt;em&gt;inside&lt;/em&gt; &lt;code&gt;&amp;lt;!-- … --&amp;gt;&lt;/code&gt;. XML comments cannot contain &lt;code&gt;--&lt;/code&gt;, so MSBuild refused to even load the file: every consumer that pulled the package got&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error MSB4024: An XML comment cannot contain '--', and '-' cannot be the last character.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and couldn't build at all. And it rode transitively — &lt;code&gt;redb.SQLite.Pro&lt;/code&gt; depends on &lt;code&gt;redb.SQLite&lt;/code&gt;, and &lt;code&gt;buildTransitive&lt;/code&gt; assets flow to dependents, so the Pro package was just as broken.&lt;/p&gt;

&lt;p&gt;Why didn't the 200/200 suite catch it? Because &lt;strong&gt;&lt;code&gt;buildTransitive&lt;/code&gt; only imports when the package is consumed as a NuGet package.&lt;/strong&gt; Our own solution references the projects by &lt;code&gt;ProjectReference&lt;/code&gt;, which skips the &lt;code&gt;build/&lt;/code&gt; import entirely — so the malformed file sat in every green build, invisible, until the first real &lt;code&gt;dotnet add package redb.SQLite&lt;/code&gt;. We found it the moment we built a &lt;em&gt;packaged&lt;/em&gt; consumer (a sample app in the public repo), not a second earlier.&lt;/p&gt;

&lt;p&gt;The fix is a one-line deletion (and yes — I reproduced the identical bug in a csproj comment &lt;em&gt;while writing the fix&lt;/em&gt;; &lt;code&gt;--&lt;/code&gt; is a persistent little landmine). Because you can't overwrite a published version, the corrected packages went out as &lt;strong&gt;&lt;code&gt;redb.SQLite&lt;/code&gt; / &lt;code&gt;redb.SQLite.Pro&lt;/code&gt; 3.2.1&lt;/strong&gt;, and the broken &lt;strong&gt;3.2.1&lt;/strong&gt; pair was unlisted. So, to be exact about versions: the engine, the date handling, the bugs above — all &lt;strong&gt;3.2.1&lt;/strong&gt;; the two SQLite NuGet packages are &lt;strong&gt;3.2.1&lt;/strong&gt;. The unglamorous lesson: a package's &lt;code&gt;build/&lt;/code&gt; and &lt;code&gt;buildTransitive&lt;/code&gt; assets are shipped code too — validate them from a &lt;em&gt;packaged&lt;/em&gt; consumer in CI, because &lt;code&gt;ProjectReference&lt;/code&gt; will happily lie to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's inside: a materializer and a compiler
&lt;/h3&gt;

&lt;p&gt;Two big subsystems live in the extension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_object_json(_id [, max_depth])&lt;/code&gt;&lt;/strong&gt; — a recursive materializer. Takes an &lt;code&gt;_id&lt;/code&gt;, reads &lt;code&gt;_values&lt;/code&gt;, assembles the JSON object that System.Text.Json then deserializes into a typed &lt;code&gt;RedbObject&amp;lt;TProps&amp;gt;&lt;/code&gt;. It assembles everything: base fields from &lt;code&gt;_objects&lt;/code&gt;, scalars, arrays and dictionaries (relationally, via &lt;code&gt;_array_index&lt;/code&gt; / &lt;code&gt;_array_parent_id&lt;/code&gt;), nested &lt;code&gt;Class&lt;/code&gt; fields, object references, &lt;code&gt;ListItem&lt;/code&gt; (including a &lt;code&gt;ListItem&lt;/code&gt; that itself carries an &lt;code&gt;Object&lt;/code&gt;). This is exactly where RTTI is required: to assemble a nested object, the materializer must know its scheme, its field types, that &lt;em&gt;this&lt;/em&gt; field is an array and &lt;em&gt;that&lt;/em&gt; one is a reference. Without type metadata, it'd just be a pile of rows in &lt;code&gt;_values&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;pvt_*&lt;/code&gt; compiler&lt;/strong&gt; — ~9k lines of PL/pgSQL logic ported into C as a SQL-string generator: &lt;code&gt;pvt_build_query_sql&lt;/code&gt;, &lt;code&gt;pvt_build_aggregate_sql&lt;/code&gt;, &lt;code&gt;pvt_build_groupby_sql&lt;/code&gt;, &lt;code&gt;pvt_build_window_sql&lt;/code&gt;, &lt;code&gt;pvt_build_projection_sql&lt;/code&gt;, &lt;code&gt;pvt_build_array_groupby_sql&lt;/code&gt;. The pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LINQ expression
   → (C#) facet filter as JSON + a field list
   → (native) pvt_build_query_sql(scheme, filterJson, ...)
   → a finished SQL SELECT string
   → (C#) execute it, materialize via get_object_json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the native side is a &lt;strong&gt;filter-JSON-to-SQL translator&lt;/strong&gt;. C# doesn't build the SQL itself (Pro does that) — it builds a filter JSON and asks the database to assemble the SQL. It sounds inside-out, but it's exactly what buys parity: the same filter JSON, on Postgres &lt;em&gt;and&lt;/em&gt; on SQLite Free, runs through the same engine — just emitting different dialects.&lt;/p&gt;

&lt;h3&gt;
  
  
  A bit deeper on &lt;code&gt;get_object_json&lt;/code&gt;: recursion, arrays, references
&lt;/h3&gt;

&lt;p&gt;"Assembles JSON" sounds simple until you remember it's assembling a graph. The materializer walks the scheme's structure and, for each field, decides &lt;strong&gt;what it is&lt;/strong&gt; from the RTTI in &lt;code&gt;_structures&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;scalar&lt;/strong&gt; — reads the value from the matching &lt;code&gt;_values&lt;/code&gt; slot column (for a date, wraps it in &lt;code&gt;strftime&lt;/code&gt; so an ISO string goes out);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;array/dictionary&lt;/strong&gt; — gathers all rows with the same &lt;code&gt;_array_parent_id&lt;/code&gt;, ordered by &lt;code&gt;_array_index&lt;/code&gt;, into a JSON array/object (this is why the pivot uses &lt;code&gt;_array_index IS NULL&lt;/code&gt; — to separate a field's "scalar" row from its elements);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nested &lt;code&gt;Class&lt;/code&gt;&lt;/strong&gt; — recurses into the subtree assembly;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reference (&lt;code&gt;_Object&lt;/code&gt;)&lt;/strong&gt; — by &lt;code&gt;max_depth&lt;/code&gt;, either expands the target object (recursion with decremented depth) or leaves an id reference;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ListItem&lt;/code&gt;&lt;/strong&gt; — a lookup-list entry; and a &lt;code&gt;ListItem&lt;/code&gt; can itself carry an &lt;code&gt;Object&lt;/code&gt;, so that's one more level of expansion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;max_depth&lt;/code&gt; parameter (the second overload of the function) is a guard against infinite recursion on cyclic references and, at the same time, a budget for how deep to pull related objects into one JSON. None of these transitions is possible without RTTI: the materializer must &lt;em&gt;know&lt;/em&gt; that this field is an array and that one is a reference, or it's just rows in &lt;code&gt;_values&lt;/code&gt; with no meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other direction: &lt;code&gt;save_object_json&lt;/code&gt; — the write path
&lt;/h3&gt;

&lt;p&gt;Reading has a symmetric twin — &lt;code&gt;save_object_json(json)&lt;/code&gt;. It takes an object's JSON and, by the scheme, lays it back out into &lt;code&gt;_objects&lt;/code&gt; (base) and &lt;code&gt;_values&lt;/code&gt; (props). Two SQLite-specific subtleties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dates on write.&lt;/strong&gt; In the incoming JSON, dates are ISO strings (that's how System.Text.Json serializes a &lt;code&gt;DateTimeOffset&lt;/code&gt;). The native write wraps them in &lt;code&gt;julianday('&amp;lt;iso&amp;gt;')&lt;/code&gt; so &lt;code&gt;_DateTimeOffset&lt;/code&gt; gets a REAL Julian. It's the mirror of the read-side &lt;code&gt;strftime&lt;/code&gt; — and exactly what got fixed this release under "fix save_object_json too": before the fix, a date went in as a string and didn't agree with the REAL column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The props-save strategy.&lt;/strong&gt; On Free it's &lt;code&gt;PropsSaveStrategy.DeleteInsert&lt;/code&gt;: a save = delete the object's existing &lt;code&gt;_values&lt;/code&gt; rows and insert the new set (ChangeTracking — a delta off &lt;code&gt;_hash&lt;/code&gt; — is a Pro feature). Simple, predictable, no tracking layer — at the cost of extra rewrites; fine for an embedded load.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Loading the right type: the CLR registry and polymorphism
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;get_object_json&lt;/code&gt; hands back JSON — but deserialize it into &lt;strong&gt;what&lt;/strong&gt;? For &lt;code&gt;LoadAsync&amp;lt;EmployeeProps&amp;gt;(id)&lt;/code&gt; the answer is in the generic. But for &lt;strong&gt;polymorphic&lt;/strong&gt; loads (&lt;code&gt;GetChildren&lt;/code&gt; on a tree where the children are objects of different schemes; &lt;code&gt;LoadDynamicObject&lt;/code&gt;) the type is unknown at compile time: you have to map the object's &lt;code&gt;_id_scheme&lt;/code&gt; to the &lt;code&gt;*Props&lt;/code&gt; class to materialize into.&lt;/p&gt;

&lt;p&gt;This got rebuilt this same cycle into a two-layer CLR registry (we were fixing a polymorphic &lt;code&gt;LoadAsync&lt;/code&gt; that returned the wrong type out of nowhere):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;a global &lt;code&gt;scheme name ↔ Type&lt;/code&gt; index&lt;/strong&gt; — self-healing: subscribed to &lt;code&gt;AppDomain.AssemblyLoad&lt;/code&gt;, so loading an assembly with new &lt;code&gt;*Props&lt;/code&gt; bumps a "generation" and the index rebuilds lazily (no "register all your types at startup");&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a per-domain &lt;code&gt;scheme_id → Type&lt;/code&gt; cache&lt;/strong&gt; — where "domain" = &lt;code&gt;SHA256(sanitized connection string)&lt;/code&gt; or an explicit &lt;code&gt;CacheDomain&lt;/code&gt;. Per-domain partitioning matters because one process can hold several databases (including several SQLite files), and &lt;code&gt;scheme_id=1000010&lt;/code&gt; in one is not the same class as in another.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ties straight to SQLite: the materializer hands back JSON + &lt;code&gt;_id_scheme&lt;/code&gt;, and the registry turns &lt;code&gt;_id_scheme&lt;/code&gt; into a &lt;code&gt;Type&lt;/code&gt; to deserialize a polymorphic child into. Get it wrong and a tree of mixed-type children all materializes into one (wrong) type.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Postgres → SQLite phrasebook
&lt;/h3&gt;

&lt;p&gt;A port isn't "retype it in C," it's a dialect translation. The most frequent swaps (all real lines from &lt;code&gt;SqliteDialect&lt;/code&gt;/the native side):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Postgres&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;th&gt;where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;array_agg(x) FILTER (WHERE …)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;json_group_array(x) FILTER (WHERE …)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pivoting arrays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x = ANY($1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x IN (SELECT value FROM json_each($1))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IN-lists (SQLite can't bind array params)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXTRACT(year FROM x)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CAST(strftime('%Y', x) AS INTEGER)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;date parts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;col ILIKE $1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;col LIKE $1 ESCAPE '\'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;case-insensitive LIKE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE … WHERE _id = ANY($1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE … WHERE _id IN (SELECT value FROM json_each($1))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;delete-by-id-list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DISTINCT ON (col)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ROW_NUMBER() OVER (PARTITION BY col)&lt;/code&gt; + &lt;code&gt;WHERE rn=1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;DistinctBy (its own section below)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That &lt;code&gt;ESCAPE '\'&lt;/code&gt; deserves a sentence, because it's a classic landmine. Postgres uses &lt;code&gt;\&lt;/code&gt; as the default &lt;code&gt;LIKE&lt;/code&gt; escape character. SQLite (and MSSQL) &lt;strong&gt;do not&lt;/strong&gt;. Meanwhile &lt;code&gt;UserProviderBase&lt;/code&gt; escapes user input with a backslash (&lt;code&gt;50%&lt;/code&gt; → &lt;code&gt;50\%&lt;/code&gt;), assuming PG semantics. On SQLite, without an explicit &lt;code&gt;ESCAPE '\'&lt;/code&gt;, that escaped &lt;code&gt;\%&lt;/code&gt; starts matching a literal backslash followed by anything — a silent search corruption. So the SQLite dialect always emits &lt;code&gt;LIKE&lt;/code&gt; with an explicit &lt;code&gt;ESCAPE '\'&lt;/code&gt;. Small thing, easy half-day to lose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minimum version, and why
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;SQLite 3.44.0+&lt;/strong&gt; (Nov 2023). We deliberately lean on &lt;code&gt;FILTER (WHERE …)&lt;/code&gt;, modern window functions, &lt;code&gt;RETURNING&lt;/code&gt;, JSON1, and recursive CTEs. The goal is to keep the SQLite SQL &lt;strong&gt;structurally close&lt;/strong&gt; to the Postgres SQL, not rewrite it from scratch. The closer the dialects, the fewer places they diverge in &lt;em&gt;behavior&lt;/em&gt; (not just syntax) — and behavioral divergences are the ones you find in prod, not in the compiler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identifiers: AUTOINCREMENT instead of sequences
&lt;/h3&gt;

&lt;p&gt;SQLite has no sequences. redb needs globally-unique ids that both the .NET key generator and the native side (for non-.NET hosts) can hand out. The solution: a native &lt;code&gt;AUTOINCREMENT&lt;/code&gt; table with &lt;code&gt;sqlite_sequence&lt;/code&gt; as a shared high-water mark. Both the C extension and the C# key generator &lt;strong&gt;advance the same&lt;/strong&gt; counter and reserve id blocks from it. The result: ids are unique no matter who handed them out — .NET or a Python process writing to the same file.&lt;/p&gt;

&lt;h3&gt;
  
  
  War story #1: &lt;code&gt;%%&lt;/code&gt; in &lt;code&gt;sqlite3_mprintf&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;So that "porting PL/pgSQL to C" doesn't sound sterile — here's the kind of bug the port hands you for free.&lt;/p&gt;

&lt;p&gt;The materializer has a column-list macro &lt;code&gt;VCOLS&lt;/code&gt; that gets interpolated into an &lt;code&gt;sqlite3_mprintf&lt;/code&gt; format string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3_mprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT"&lt;/span&gt; &lt;span class="n"&gt;VCOLS&lt;/span&gt; &lt;span class="s"&gt;"FROM _values WHERE _id_structure=?1 "&lt;/span&gt;
                            &lt;span class="s"&gt;"AND _id_object=?2 AND %s LIMIT 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I switched the datetime columns to emit via &lt;code&gt;strftime('%Y-%m-%dT%H:%M:%fZ', _DateTimeOffset)&lt;/code&gt;, I dutifully put that &lt;code&gt;strftime&lt;/code&gt; right into &lt;code&gt;VCOLS&lt;/code&gt;. Everything fell over &lt;strong&gt;silently&lt;/strong&gt;: objects started loading with empty &lt;code&gt;properties&lt;/code&gt;, while base dates were fine.&lt;/p&gt;

&lt;p&gt;I spent half an hour looking in the wrong place. The reveal: &lt;code&gt;VCOLS&lt;/code&gt; flows into the &lt;strong&gt;format string&lt;/strong&gt; of &lt;code&gt;sqlite3_mprintf&lt;/code&gt;, and &lt;code&gt;%Y %m %d %H %M %f&lt;/code&gt; are format specifiers to mprintf. It started eating the &lt;code&gt;cond&lt;/code&gt; argument at the first &lt;code&gt;%Y&lt;/code&gt;, the formatting derailed, the SQL came out malformed, &lt;code&gt;sqlite3_prepare_v2&lt;/code&gt; &lt;strong&gt;silently&lt;/strong&gt; returned an error code, and the props function returned empty. Base dates survived because their SELECT goes through &lt;code&gt;prepare_v2&lt;/code&gt; directly, no mprintf.&lt;/p&gt;

&lt;p&gt;The fix is escaping — &lt;code&gt;%%&lt;/code&gt; (mprintf collapses &lt;code&gt;%%&lt;/code&gt;→&lt;code&gt;%&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; SQLite ever sees the string):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// was: ... strftime('%Y-%m-%dT%H:%M:%fZ',_DateTimeOffset) ...   // breaks mprintf&lt;/span&gt;
&lt;span class="c1"&gt;// now:&lt;/span&gt;
&lt;span class="cp"&gt;#define VCOLS " _id,_String,_Long,_Guid,_Double, " \
  "strftime('%%Y-%%m-%%dT%%H:%%M:%%fZ',_DateTimeOffset), " \
  "_Boolean,_ByteArray,_Numeric,_ListItem,_Object,_array_parent_id,_array_index "
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lasting lesson: &lt;strong&gt;when you generate SQL through a printf-style formatter, any &lt;code&gt;%&lt;/code&gt; in the data is a landmine.&lt;/strong&gt; What makes it nasty is that the failure is silent: &lt;code&gt;prepare&lt;/code&gt; doesn't throw, it returns an error code that's easy to skip, and it surfaces as "why are my props empty."&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2. SQLite has no date type. How we lived with that for a release
&lt;/h2&gt;

&lt;p&gt;This is the central engineering story of the release, and it didn't exist yet at announcement time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three storage classes, zero date types
&lt;/h3&gt;

&lt;p&gt;In SQLite, datetime is &lt;strong&gt;a convention over three storage classes&lt;/strong&gt;, not a type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TEXT&lt;/strong&gt; — an ISO-8601 string (&lt;code&gt;'2024-06-15 13:45:30'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REAL&lt;/strong&gt; — a Julian day, a floating-point number (astronomical day count).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INTEGER&lt;/strong&gt; — Unix epoch seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A .NET &lt;code&gt;DateTimeOffset&lt;/code&gt; doesn't drop into any of these on its own. You pick a representation and hold it &lt;strong&gt;everywhere&lt;/strong&gt;: on write (the parameter binder), on read (the materializer + scalar converters), in filter comparisons, in aggregates. One miss in any of those points and dates "kind of work" but lie at the boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not TEXT (even though that's how it started)
&lt;/h3&gt;

&lt;p&gt;The first version stored dates as ISO text. It broke exactly the way string-comparing dates breaks.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;datetime('now')&lt;/code&gt; in SQLite returns a string with a &lt;strong&gt;space&lt;/strong&gt; between date and time: &lt;code&gt;2024-06-15 13:45:30&lt;/code&gt;. But the literal the C# layer interpolates into a comparison arrives with a &lt;strong&gt;&lt;code&gt;T&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;2024-06-15T13:45:30&lt;/code&gt;. TEXT comparison in SQLite is &lt;strong&gt;lexicographic, byte-by-byte&lt;/strong&gt;. Here's what happens at position 10:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'2024-06-15 13:45:30'   byte[10] = 0x20 (space)
'2024-06-15T13:45:30'   byte[10] = 0x54 ('T')
0x20 &amp;lt; 0x54  →  the space string is ALWAYS "less than" the 'T' string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So any "stored (with a space) ⟷ literal (with a T)" comparison goes one way &lt;strong&gt;always&lt;/strong&gt;, regardless of the actual time. Range filters started lying silently. In a cluster, this once marked a live node dead: the heartbeat comparison &lt;code&gt;last_seen &amp;lt; cutoff&lt;/code&gt; was "always true" because the stored &lt;code&gt;last_seen&lt;/code&gt; (with a space) sorts lexicographically below the cutoff literal (with a T). Node's alive; monitoring thinks it's a corpse.&lt;/p&gt;

&lt;p&gt;We could normalize the separator. But that treats the symptom: TEXT comparison stays lexicographic, and any other format drift (milliseconds, a timezone suffix, leading zeros) reopens the same hole. We had to leave strings.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: REAL Julian day, everything in UTC
&lt;/h3&gt;

&lt;p&gt;We moved to &lt;strong&gt;REAL Julian day, everything in UTC&lt;/strong&gt; — exactly how Postgres keeps &lt;code&gt;timestamptz&lt;/code&gt; in UTC. Three reasons this is a &lt;em&gt;right&lt;/em&gt; choice, not "yet another convention":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. SQLite's native functions eat the Julian number directly.&lt;/strong&gt; &lt;code&gt;julianday()&lt;/code&gt;, &lt;code&gt;strftime()&lt;/code&gt;, &lt;code&gt;datetime()&lt;/code&gt;, &lt;code&gt;date()&lt;/code&gt; take a REAL Julian as-is, no wrapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;sqlite&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%Y-%m-%dT%H:%M:%fZ'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2460477&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0732638887&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="n"&gt;T13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="n"&gt;Z&lt;/span&gt;
&lt;span class="n"&gt;sqlite&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2460477&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0732638887&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;06&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="n"&gt;sqlite&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2024-06-15T13:45:30Z'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;2460477&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0732638887&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So to emit a date into JSON, the materializer just wraps the column in &lt;code&gt;strftime&lt;/code&gt; and gets ISO that System.Text.Json parses natively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Comparison becomes numeric.&lt;/strong&gt; &lt;code&gt;col &amp;lt; X&lt;/code&gt; on a &lt;code&gt;double&lt;/code&gt; is correct and unambiguous. No lexicographic surprise, because you're comparing numbers, not bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. And, crucially for prod — it's sargable (index-friendly).&lt;/strong&gt; Here's the subtlety the whole thing was for. Wrap the &lt;strong&gt;column&lt;/strong&gt; in a function — &lt;code&gt;julianday(col) &amp;lt; X&lt;/code&gt; — and the index on &lt;code&gt;col&lt;/code&gt; dies: the optimizer can't use an index over an expression of the column. But comparing the &lt;strong&gt;raw REAL column to a constant&lt;/strong&gt; — &lt;code&gt;col &amp;lt; julianday('2024-06-15')&lt;/code&gt; — is indexable: bare column on the left, constant on the right. So in SQL generation we put &lt;code&gt;julianday(...)&lt;/code&gt; on the &lt;strong&gt;literal side&lt;/strong&gt;, never on the column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- NOT this (kills the index on _date_create):&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_date_create&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2023-01-01'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- this (sargable):&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_date_create&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2023-01-01'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column stores a Julian number → compare it to a Julian number computed from the literal, on the constant side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversion: no magic, built-in .NET
&lt;/h3&gt;

&lt;p&gt;Converting &lt;code&gt;DateTime&lt;/code&gt;/&lt;code&gt;DateTimeOffset&lt;/code&gt; ⟷ Julian is arithmetic on the built-in &lt;code&gt;ToOADate&lt;/code&gt;/&lt;code&gt;FromOADate&lt;/code&gt;. An OLE Automation date (epoch 1899-12-30) differs from a Julian day by exactly the constant &lt;code&gt;2415018.5&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SqliteJulian&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Julian = OADate + 2415018.5. ToOADate/FromOADate are built-in and lossless&lt;/span&gt;
    &lt;span class="c1"&gt;// within double precision — the same precision SQLite's julianday() lives in.&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;OADateToJulianOffset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2415018.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// DateTimeOffset → UTC Julian. .UtcDateTime APPLIES the offset → the true UTC instant.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;ToJulian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToOADate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OADateToJulianOffset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// DateTime → UTC Julian. The clock value is treated as UTC per redb's contract&lt;/span&gt;
    &lt;span class="c1"&gt;// (NormalizeForStorage sets Kind=Utc without converting). ToOADate ignores Kind — they agree.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;ToJulian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToOADate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OADateToJulianOffset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// REAL Julian → DateTimeOffset(+00:00)&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="nf"&gt;FromJulian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;julian&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;utc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SpecifyKind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromOADate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;julian&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;OADateToJulianOffset&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;DateTimeKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Where it plugs in: four points
&lt;/h3&gt;

&lt;p&gt;For dates not to lie, the REAL-Julian representation has to hold at every point where a value crosses the C# ⟷ SQLite boundary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write — the central parameter binder.&lt;/strong&gt; Every write (base dates and props alike) goes through one place — &lt;code&gt;CreateCommand&lt;/code&gt; in &lt;code&gt;SqliteRedbConnection&lt;/code&gt;. There, &lt;code&gt;DateTimeOffset&lt;/code&gt;/&lt;code&gt;DateTime&lt;/code&gt; turn into &lt;code&gt;double&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;// REAL Julian day (UTC) — SQLite's native date format.&lt;/span&gt;
        &lt;span class="n"&gt;sqliteParam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SqliteJulian&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToJulian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;dt2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sqliteParam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SqliteJulian&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToJulian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Read scalars — &lt;code&gt;ConvertScalar&lt;/code&gt;.&lt;/strong&gt; A value from SQLite arrives as &lt;code&gt;double&lt;/code&gt;; for a temporal target we convert back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;jdo&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;SqliteJulian&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromJulian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jdo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()!,&lt;/span&gt; &lt;span class="p"&gt;...));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Read rows — &lt;code&gt;MapRow&lt;/code&gt;.&lt;/strong&gt; The same &lt;code&gt;double → DateTimeOffset/DateTime/DateOnly&lt;/code&gt; when mapping columns to properties.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native.&lt;/strong&gt; &lt;code&gt;get_object_json&lt;/code&gt; emits the date via &lt;code&gt;strftime(ISO, col)&lt;/code&gt; (see the &lt;code&gt;%%&lt;/code&gt; story), and pvt comparisons wrap the literal in &lt;code&gt;julianday('&amp;lt;iso&amp;gt;')&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three CLR types on one column
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;_values._DateTimeOffset&lt;/code&gt; column (REAL) serves &lt;strong&gt;three&lt;/strong&gt; CLR types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DateTimeOffset&lt;/code&gt; — directly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DateTime&lt;/code&gt; — clock value as UTC (redb's contract).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DateOnly&lt;/code&gt; — UTC midnight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(&lt;code&gt;TimeOnly&lt;/code&gt;/&lt;code&gt;TimeSpan&lt;/code&gt; go to &lt;code&gt;_String&lt;/code&gt;.) The scheme's RTTI disambiguates them: the field's db-type tells the materializer what to unfold the &lt;code&gt;double&lt;/code&gt; into.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timezones: why &lt;code&gt;+4&lt;/code&gt; resolves correctly
&lt;/h3&gt;

&lt;p&gt;A recurring comment-section question: &lt;em&gt;"if I write a &lt;code&gt;DateTimeOffset&lt;/code&gt; with a &lt;code&gt;+04:00&lt;/code&gt; zone in LINQ, does it compare correctly against what's in the DB?"&lt;/em&gt; Yes, for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C# side:&lt;/strong&gt; &lt;code&gt;ToJulian(DateTimeOffset)&lt;/code&gt; takes &lt;code&gt;dto.UtcDateTime&lt;/code&gt; — and &lt;code&gt;.UtcDateTime&lt;/code&gt; &lt;strong&gt;applies the offset&lt;/strong&gt; and yields the true UTC instant. So &lt;code&gt;+04:00&lt;/code&gt; collapses to UTC before Julian.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native side:&lt;/strong&gt; even if an offset leaks into the literal, the comparison wraps it in &lt;code&gt;julianday('&amp;lt;iso-with-offset&amp;gt;')&lt;/code&gt;, and &lt;strong&gt;&lt;code&gt;julianday()&lt;/code&gt; parses the offset itself&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;sqlite&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-25T20:00:00+04:00'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;julianday&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-25T16:00:00Z'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stored UTC → compared by UTC instant → it agrees with any incoming offset.&lt;/p&gt;

&lt;h3&gt;
  
  
  War story #2: analytics and a &lt;code&gt;FormatException&lt;/code&gt; from nowhere
&lt;/h3&gt;

&lt;p&gt;The trickiest part of the datetime story.&lt;/p&gt;

&lt;p&gt;A normal object load goes through &lt;code&gt;get_object_json&lt;/code&gt; — it emits the date as an &lt;strong&gt;ISO string&lt;/strong&gt; (via &lt;code&gt;strftime&lt;/code&gt;), and C# parses it normally. But &lt;strong&gt;analytics&lt;/strong&gt; — &lt;code&gt;MinRedbAsync&lt;/code&gt;/&lt;code&gt;MaxRedbAsync&lt;/code&gt;, &lt;code&gt;AggregateRedbAsync&lt;/code&gt;, windows, group-bys — &lt;strong&gt;bypass&lt;/strong&gt; &lt;code&gt;get_object_json&lt;/code&gt;. They pull the date column straight into the &lt;code&gt;SELECT&lt;/code&gt; and hand the &lt;strong&gt;raw Julian number&lt;/strong&gt; to the core converter. The converter expected a string or a &lt;code&gt;DateTime&lt;/code&gt;. The result: a &lt;code&gt;FormatException&lt;/code&gt; out of nowhere (and funnier still: &lt;code&gt;elem.GetInt64()&lt;/code&gt; on a fractional &lt;code&gt;2460477.07&lt;/code&gt; — because the code assumed an integer Unix timestamp).&lt;/p&gt;

&lt;p&gt;The fix was a design fork. We could patch a SQLite hack right into the core — but &lt;code&gt;redb.Core&lt;/code&gt; has no knowledge (and shouldn't) of Julian: that's a SQLite storage detail, and the core serves three dialects. PG/MSSql return dates fine, and dragging the word "Julian" into the shared converter would leak one provider's detail into all of them.&lt;/p&gt;

&lt;p&gt;We did it through a &lt;strong&gt;neutral extension point&lt;/strong&gt;. In the core, an optional "number → temporal type" hook, empty by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// redb.Core: the core does NOT know the word "Julian". Only: "if a NUMBER targets a&lt;/span&gt;
&lt;span class="c1"&gt;// date and a decoder is registered, ask it."&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TemporalDecoder&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;NumericDecoder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsTemporal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Convert.ChangeType that first lets a number-to-date pass through the decoder.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="nf"&gt;ChangeType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;targetType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;TryDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChangeType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the SQLite provider itself &lt;strong&gt;registers&lt;/strong&gt; the decoder — in &lt;code&gt;SqliteDataSource&lt;/code&gt;'s static constructor, which runs for both Free and Pro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;TemporalDecoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumericDecoder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;julian&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SqliteJulian&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromJulian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;julian&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DateOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcDateTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcDateTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// DateTime&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then two places in the core, through which &lt;strong&gt;all&lt;/strong&gt; analytics materialization flows (for both Free and Pro, because Pro has no materializer of its own — it reuses the core converters):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;JsonValueConverter&lt;/code&gt; — the &lt;code&gt;Number&lt;/code&gt; branch → temporal type (group-by, window, projections).&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;TemporalDecoder.ChangeType&lt;/code&gt; wrapper at scalar points (&lt;code&gt;MinRedbAsync&lt;/code&gt;/&lt;code&gt;MaxRedbAsync&lt;/code&gt;, &lt;code&gt;AggregateResult.Get&amp;lt;T&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PG/MSSql never return a number for a date — their &lt;code&gt;NumericDecoder&lt;/code&gt; stays &lt;code&gt;null&lt;/code&gt;, behavior unchanged to the byte. The constant &lt;code&gt;2415018.5&lt;/code&gt; and the word "Julian" stay &lt;strong&gt;inside &lt;code&gt;redb.SQLite&lt;/code&gt;&lt;/strong&gt;, and the core stays storage-agnostic.&lt;/p&gt;

&lt;p&gt;That, to my taste, is the shape of a right fix: the problem is local (SQLite stores dates as numbers), and the cure isn't smearing SQLite specifics across the core — it's one generic extension point that only the party who needs it ever pulls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3. The pvt compiler: how a filter becomes SQL
&lt;/h2&gt;

&lt;p&gt;Since the engine is a filter-JSON-to-SQL translator, let's take apart a piece of that translator. This is the most "a database inside the database" part.&lt;/p&gt;

&lt;h3&gt;
  
  
  The filter JSON
&lt;/h3&gt;

&lt;p&gt;C# folds a LINQ &lt;code&gt;Where&lt;/code&gt; into a facet filter — JSON the native side understands. Say &lt;code&gt;Where(e =&amp;gt; e.LastName == "NullableTest")&lt;/code&gt; on a prop field gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"LastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$eq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NullableTest"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;WhereRedb(o =&amp;gt; o.ParentId == null)&lt;/code&gt; on a base field (the &lt;code&gt;0$:&lt;/code&gt; marker means "this is base, not a prop"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"0$:ParentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combining the two conditions is an implicit &lt;code&gt;$and&lt;/code&gt; over the object's keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"0$:ParentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"LastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"$eq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NullableTest"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The split: push vs residual
&lt;/h3&gt;

&lt;p&gt;The key function is &lt;code&gt;pvtSplitFilter&lt;/code&gt;. It divides the filter into two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;push&lt;/strong&gt; — conditions on base fields and props that can be pushed &lt;em&gt;inside&lt;/em&gt; the CTE (into the &lt;code&gt;_objects&lt;/code&gt;/&lt;code&gt;_values&lt;/code&gt; subquery).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;residual&lt;/strong&gt; — what's applied &lt;em&gt;outside&lt;/em&gt;, on top of the assembled pivot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split decides which "shape" the query takes. There are three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shape A — pure-base flat:&lt;/strong&gt; the filter is base-only, no props. No CTE at all: &lt;code&gt;SELECT _id FROM _objects o WHERE o._id_scheme=? AND &amp;lt;push&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;narrow&lt;/strong&gt; — props exist, the filter reduces to the pivot: build a &lt;code&gt;_pvt_cte&lt;/code&gt; (pivoting the needed structures via &lt;code&gt;MAX(...) FILTER (WHERE _id_structure=? AND _array_index IS NULL)&lt;/code&gt;), join &lt;code&gt;_objects&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;non-narrow&lt;/strong&gt; — there are non-pushable checks (e.g. presence), an outer WHERE is needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The actual assembled SQL for &lt;code&gt;Where(LastName) + WhereRedb(ParentId IS NULL)&lt;/code&gt; (narrow shape):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;_pvt_cte&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000012&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"LastName"&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000012&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000010&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;   &lt;span class="c1"&gt;-- ← pushed base condition&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_pvt_cte&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_pvt_cte&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"LastName"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'NullableTest'&lt;/span&gt;                                    &lt;span class="c1"&gt;-- ← residual prop condition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: the base condition &lt;code&gt;o._id_parent IS NULL&lt;/code&gt; went &lt;strong&gt;inside&lt;/strong&gt; the &lt;code&gt;_objects&lt;/code&gt; subquery (push), and the prop condition on &lt;code&gt;LastName&lt;/code&gt; stayed &lt;strong&gt;outside&lt;/strong&gt; (residual). That's not incidental — it's exactly what &lt;code&gt;pvtSplitFilter&lt;/code&gt; does, and exactly where we had a bug (next).&lt;/p&gt;

&lt;h3&gt;
  
  
  War story #3: a multi-key filter dropped &lt;code&gt;null&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;WhereRedb(o =&amp;gt; o.ParentId == null).Where(e =&amp;gt; e.LastName == "X")&lt;/code&gt; on Free returned rows &lt;strong&gt;that have a parent&lt;/strong&gt;. The &lt;code&gt;IS NULL&lt;/code&gt; condition silently vanished — but only when combined with a prop filter. Base-only &lt;code&gt;WhereRedb(o =&amp;gt; o.ParentId == null)&lt;/code&gt; worked.&lt;/p&gt;

&lt;p&gt;Diagnosis. Calling the native function directly on three filters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1) base IS NULL only — WORKS:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pvt_build_query_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"0$:ParentId":null}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000010&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;

&lt;span class="c1"&gt;-- 2) base equality — WORKS:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pvt_build_query_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"0$:ParentId":5}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000010&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

&lt;span class="c1"&gt;-- 3) base IS NULL + prop — the base condition VANISHED:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pvt_build_query_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000010&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"0$:ParentId":null,"LastName":{"$eq":"X"}}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;only&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;CTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;no&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;{"0$:ParentId":null}&lt;/code&gt; on its own is fine, but as part of multiple keys it's lost. The root is in &lt;code&gt;pvtSplitFilter&lt;/code&gt;'s multi-key branch. To split each key separately, it reassembles a single-key filter object per key via &lt;code&gt;pvtSingleton&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pvtSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v_json&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
  &lt;span class="n"&gt;sqlite3_stmt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;sqlite3_prepare_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SELECT json_object(?1, json(?2))"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the loop took each key's value from &lt;code&gt;json_each&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WAS:&lt;/span&gt;
&lt;span class="s"&gt;"SELECT key, value FROM json_each(?1)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap: for a JSON &lt;strong&gt;null&lt;/strong&gt;, the &lt;code&gt;value&lt;/code&gt; column in &lt;code&gt;json_each&lt;/code&gt; is &lt;strong&gt;SQL NULL&lt;/strong&gt;. So &lt;code&gt;v_json&lt;/code&gt; arrived as an empty string, &lt;code&gt;json("")&lt;/code&gt; is a parse error, &lt;code&gt;json_object(...)&lt;/code&gt; returned NULL, the singleton came out NULL → &lt;code&gt;pvtSplitFilter&lt;/code&gt; on a NULL filter returned "nothing" → the condition silently disappeared. (The same would happen with a bare text value: &lt;code&gt;json_each.value&lt;/code&gt; returns text &lt;em&gt;without&lt;/em&gt; quotes, and &lt;code&gt;json("NullableTest")&lt;/code&gt; is again a parse error.) The base-only path worked because it takes the value's type from a separate &lt;code&gt;type&lt;/code&gt; column, where null is detected correctly.&lt;/p&gt;

&lt;p&gt;The fix — reassemble the value into a &lt;strong&gt;valid JSON atom by type&lt;/strong&gt;, in SQL, before &lt;code&gt;pvtSingleton&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NOW:&lt;/span&gt;
&lt;span class="s"&gt;"SELECT key, CASE type "&lt;/span&gt;
&lt;span class="s"&gt;"  WHEN 'text'  THEN json_quote(value) "&lt;/span&gt;   &lt;span class="c1"&gt;// "X" with quotes&lt;/span&gt;
&lt;span class="s"&gt;"  WHEN 'null'  THEN 'null' "&lt;/span&gt;              &lt;span class="c1"&gt;// valid JSON null&lt;/span&gt;
&lt;span class="s"&gt;"  WHEN 'true'  THEN 'true' "&lt;/span&gt;
&lt;span class="s"&gt;"  WHEN 'false' THEN 'false' "&lt;/span&gt;
&lt;span class="s"&gt;"  ELSE value END "&lt;/span&gt;                        &lt;span class="c1"&gt;// integer/real/object/array — already valid JSON&lt;/span&gt;
&lt;span class="s"&gt;"FROM json_each(?1)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After re-encoding, &lt;code&gt;{"0$:ParentId":null}&lt;/code&gt; stays valid JSON, the singleton assembles, and the condition reaches &lt;code&gt;push&lt;/code&gt; and attaches to the &lt;code&gt;_objects&lt;/code&gt; subquery. Lesson: &lt;code&gt;json_each.value&lt;/code&gt; is a lossy source — it drops type (null → SQL NULL, text → unquoted); if you reconstruct JSON from it, do it &lt;strong&gt;off the &lt;code&gt;type&lt;/code&gt; column&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  DistinctBy: emulating &lt;code&gt;DISTINCT ON&lt;/code&gt; with &lt;code&gt;ROW_NUMBER()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Postgres has &lt;code&gt;DISTINCT ON (col)&lt;/code&gt; — "one row per value of col." SQLite has none. Pro already solved it in &lt;code&gt;ProSqlBuilder&lt;/code&gt; via &lt;code&gt;ROW_NUMBER()&lt;/code&gt;; on Free the native &lt;code&gt;distinct_on&lt;/code&gt; was &lt;strong&gt;ignored&lt;/strong&gt; (an explicit TODO), and &lt;code&gt;DistinctBy(e =&amp;gt; e.Department)&lt;/code&gt; returned duplicates.&lt;/p&gt;

&lt;p&gt;Bringing it to parity. &lt;code&gt;pvt_build_query_sql&lt;/code&gt; accepts a 12th argument &lt;code&gt;distinct_on&lt;/code&gt; — but the wrapper function only read up to the 11th, so C# sent the param and the native side dropped it. The fix is threefold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the 12th argument and thread it through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull the distinct field into the pivot.&lt;/strong&gt; If the field isn't mentioned in the filter/sort, it's not in the collected fields → not in the CTE → nothing to partition on. So the distinct field gets mixed into field collection (&lt;code&gt;pvtCollectFields&lt;/code&gt;) by the same mechanism as &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap the result in a &lt;code&gt;_ranked&lt;/code&gt; CTE with &lt;code&gt;ROW_NUMBER()&lt;/code&gt;&lt;/strong&gt; and keep &lt;code&gt;rn=1&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;_pvt_cte&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;pivot&lt;/span&gt; &lt;span class="n"&gt;Department&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;_ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;_pvt_cte&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"Department"&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;_rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_pvt_cte&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_pvt_cte&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_ranked&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The partition expression resolves from field metadata: base → &lt;code&gt;o.&amp;lt;column&amp;gt;&lt;/code&gt;, prop → &lt;code&gt;_pvt_cte."&amp;lt;FieldName&amp;gt;"&lt;/code&gt; (the pivot column). The group representative is the row with the minimum &lt;code&gt;o._id&lt;/code&gt; (same as Pro). All of this engages &lt;strong&gt;only&lt;/strong&gt; when &lt;code&gt;distinct_on&lt;/code&gt; is present; ordinary queries take the old path — zero regression risk for 99% of queries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4. Free vs Pro: where Pro accidentally reached into Free
&lt;/h2&gt;

&lt;p&gt;Architecturally Free and Pro share the base providers (&lt;code&gt;redb.Core&lt;/code&gt;) but diverge at materialization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; calls &lt;code&gt;get_object_json&lt;/code&gt; (native) to assemble objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro&lt;/strong&gt; materializes in C# (&lt;code&gt;ProLazyPropsLoader&lt;/code&gt;, &lt;code&gt;ProSqlBuilder&lt;/code&gt;) and must &lt;strong&gt;never&lt;/strong&gt; call a native function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here a principled bug surfaced. &lt;code&gt;DeleteSubtreeAsync&lt;/code&gt; (subtree delete) collects descendant ids via the base &lt;code&gt;TreeProviderBase.CollectDescendantIds&lt;/code&gt;. Pro overrides the &lt;em&gt;loading&lt;/em&gt; tree methods (&lt;code&gt;GetChildren&lt;/code&gt;, &lt;code&gt;GetPolymorphicChildren&lt;/code&gt;, &lt;code&gt;LoadDynamicObject&lt;/code&gt;) with C# materialization — but it did &lt;strong&gt;not&lt;/strong&gt; override &lt;code&gt;CollectDescendantIds&lt;/code&gt;, and that one used a recipe with &lt;code&gt;get_object_json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Tree_SelectPolymorphicChildren — the recipe that called the native function:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;SchemeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_object_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;JsonData&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On PG/MSSql this passes silently: &lt;code&gt;get_object_json&lt;/code&gt; there is a &lt;strong&gt;server-side function present in every tier&lt;/strong&gt;. On &lt;strong&gt;SQLite Pro&lt;/strong&gt; the function isn't there (Pro doesn't load the native code) → a hard crash, &lt;code&gt;no such function: get_object_json&lt;/code&gt;. And on PG/MSSql Pro it was silent waste: materializing each subtree node's full JSON only to throw the JSON away and take the &lt;code&gt;_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix isn't a Pro override (that would leave the base calling &lt;code&gt;get_object_json&lt;/code&gt; for a method that doesn't need JSON); it's removing JSON from the base method itself — it only needs an id list. We added an id-only recipe to all three dialects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ISqlDialect + PostgreSqlDialect / MsSqlDialect / SqliteDialect:&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Tree_SelectChildrenIds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;"SELECT o._id FROM _objects o WHERE o._id_parent = $1 ORDER BY o._name, o._id"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CollectDescendantIds — was QueryAsync&amp;lt;ChildObjectInfo&amp;gt;(Tree_SelectPolymorphicChildren), now:&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;childIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryScalarListAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;Sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tree_SelectChildrenIds&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;parentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;childId&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;childIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;childId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;CollectDescendantIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;childId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the leak is closed &lt;strong&gt;at the source for every tier&lt;/strong&gt;: Free doesn't materialize extra JSON for an id list, Pro doesn't reach into the native code, PG/MSSql Pro stop materializing for nothing. Pro source now has exactly zero &lt;code&gt;get_object_json&lt;/code&gt; calls. And, incidentally, &lt;strong&gt;SQLite Pro turned out to be the perfect detector&lt;/strong&gt; for these leaks: it crashes on any native call out of Pro — the thing PG/MSSql silently tolerate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Also: &lt;code&gt;DeleteSubtree&lt;/code&gt; and cascade
&lt;/h3&gt;

&lt;p&gt;The same &lt;code&gt;DeleteSubtree&lt;/code&gt; returned the wrong delete count. On SQLite the schema has &lt;code&gt;FK _id_parent ... ON DELETE CASCADE&lt;/code&gt; — delete a parent and the children go by cascade. But &lt;code&gt;changes()&lt;/code&gt; (rows-affected) does &lt;strong&gt;not&lt;/strong&gt; count cascade-deleted rows. So &lt;code&gt;DELETE WHERE _id IN (parent, children)&lt;/code&gt; could return &lt;code&gt;1&lt;/code&gt; (only the parent deleted directly; children by cascade). We fixed the semantics: the method returns the &lt;strong&gt;size of the collected subtree&lt;/strong&gt; (&lt;code&gt;objectIds.Count&lt;/code&gt;), not the cascade-dependent rows-affected. On PG/MSSql (no cascade on &lt;code&gt;_id_parent&lt;/code&gt;) it's the same number — no divergence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bool is an INTEGER
&lt;/h3&gt;

&lt;p&gt;One more small one, surfaced in group-bys. SQLite has no boolean type — it stores &lt;code&gt;0&lt;/code&gt;/&lt;code&gt;1&lt;/code&gt; as INTEGER. In a pivot/projection, a bool value reaches the shared converter as a &lt;strong&gt;JSON number&lt;/strong&gt;, and the &lt;code&gt;bool&lt;/code&gt; branch in &lt;code&gt;JsonValueConverter&lt;/code&gt; only caught &lt;code&gt;true&lt;/code&gt;/string → the number &lt;code&gt;1&lt;/code&gt; produced &lt;code&gt;false&lt;/code&gt;. Group-by on a bool key collapsed (everything "false"). The fix — accept Number in the bool branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueKind&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;JsonValueKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueKind&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;JsonValueKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetDouble&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bn&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueKind&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;JsonValueKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bl&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PG/MSSql send &lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; — untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5. How to poke at it yourself
&lt;/h2&gt;

&lt;p&gt;This isn't article pseudocode. The repo ships two tools you verify all of this with by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;redb.Examples&lt;/code&gt;&lt;/strong&gt; — ~150 runnable examples that run on &lt;strong&gt;any&lt;/strong&gt; provider, SQLite included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; redb.Examples &lt;span class="nt"&gt;--&lt;/span&gt; E021 E146 E148   &lt;span class="c"&gt;# date filter, aggregates, windows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flip &lt;code&gt;AddRedb&lt;/code&gt;/&lt;code&gt;AddRedbPro&lt;/code&gt; + &lt;code&gt;UseSqlite&lt;/code&gt; and the same suite runs live on SQLite Free or Pro. The same code runs on Postgres/MSSql unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;redb.CLI&lt;/code&gt;&lt;/strong&gt; — a global .NET tool for schema and data management, supports sqlite across every command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redb schema &lt;span class="nt"&gt;-p&lt;/span&gt; sqlite &lt;span class="nt"&gt;-o&lt;/span&gt; redb_sqlite.sql            &lt;span class="c"&gt;# dump the full schema SQL (review/CI)&lt;/span&gt;
redb init   &lt;span class="nt"&gt;-p&lt;/span&gt; sqlite &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Data Source=app.db"&lt;/span&gt;       &lt;span class="c"&gt;# create the tables in an empty DB&lt;/span&gt;
redb &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; sqlite &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Data Source=app.db"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; data.redb &lt;span class="nt"&gt;--compress&lt;/span&gt;
redb import &lt;span class="nt"&gt;-p&lt;/span&gt; sqlite &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Data Source=app.db"&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; data.redb &lt;span class="nt"&gt;--clean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the part that matters — trust is verified by tests. SQLite Free and Pro each pass the integration suite &lt;strong&gt;200/200&lt;/strong&gt; — the same suite that gates Postgres and MSSql. For a brand-new provider that's worth more than adjectives: same suite, same bar.&lt;/p&gt;

&lt;h3&gt;
  
  
  War story #4: the CLI that "supported" sqlite — but silently didn't
&lt;/h3&gt;

&lt;p&gt;While preparing this post, I wanted to show &lt;code&gt;redb schema -p sqlite&lt;/code&gt; — and walked straight into my own tooling landmine. &lt;code&gt;redb.CLI&lt;/code&gt;'s &lt;em&gt;code&lt;/em&gt; did support sqlite: a full &lt;code&gt;SqliteProvider&lt;/code&gt;, the &lt;code&gt;ProviderFactory.Create("sqlite")&lt;/code&gt; factory, the &lt;code&gt;redbSqlite.sql&lt;/code&gt; schema resource. The csproj did not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- redb.CLI.csproj — pulled the engine from an old NuGet version: --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"redb.SQLite"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.2.*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;1.2.*&lt;/code&gt; is a version where the SQLite provider &lt;strong&gt;didn't exist at all&lt;/strong&gt; (it's new, at 3.2.1). So &lt;code&gt;typeof(redb.SQLite.RedbService).Assembly&lt;/code&gt; and the embedded &lt;code&gt;redbSqlite.sql&lt;/code&gt; resource resolved against an assembly that doesn't have them, and any &lt;code&gt;-p sqlite&lt;/code&gt; command silently drifted from the real code. The fix — point the references at the local projects (as &lt;code&gt;redb.Examples&lt;/code&gt; already does):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\redb.SQLite\redb.SQLite.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same moral as the &lt;code&gt;%%&lt;/code&gt; story: &lt;strong&gt;"the code supports it" ≠ "the build sees it."&lt;/strong&gt; A version pin is part of the contract too, and a stale pin breaks a feature as quietly as a typo in SQL. It's trivially checkable — by running the command itself: &lt;code&gt;redb schema -p sqlite&lt;/code&gt; now dumps the real schema (REAL Julian, &lt;code&gt;_DateTimeOffset REAL&lt;/code&gt;) instead of failing on an empty resource.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6. Traps you will hit
&lt;/h2&gt;

&lt;p&gt;This series is honest about "what's not done and what you'll trip over," so no glossing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;.db&lt;/code&gt; path follows the working directory.&lt;/strong&gt; A relative string (&lt;code&gt;Data Source=app.db&lt;/code&gt;) creates the file relative to the &lt;strong&gt;process cwd&lt;/strong&gt;, not the project folder. I lost a couple of hours to this myself: &lt;code&gt;dotnet run&lt;/code&gt; from different directories wrote to different files, and tests "passed/failed" against different DBs. Use an absolute path or pin the cwd.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;:memory:&lt;/code&gt; is per-connection.&lt;/strong&gt; For a connection pool to see one in-memory DB you need &lt;code&gt;Mode=Memory;Cache=Shared&lt;/code&gt; plus one &lt;strong&gt;held-open&lt;/strong&gt; connection. That's SQLite's lifecycle, not redb's: close the last connection and the DB evaporates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NUMERIC&lt;/code&gt; → &lt;code&gt;REAL&lt;/code&gt; by default.&lt;/strong&gt; Fast, but lossy past double range. An exact path via &lt;code&gt;TEXT&lt;/code&gt; is a planned setting. A known SQLite weak spot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite is single-writer.&lt;/strong&gt; One writer per file; redb sets &lt;code&gt;busy_timeout&lt;/code&gt; for concurrent writes, but don't expect Postgres-grade parallelism. For embedded/local, that's normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free's native binaries ship for Windows x64, Linux x64 and Linux arm64.&lt;/strong&gt; All three are packed into &lt;code&gt;runtimes/&amp;lt;rid&amp;gt;/native/&lt;/code&gt; and delivered to framework-dependent builds by a &lt;code&gt;buildTransitive&lt;/code&gt; .targets — the extension is loaded by an explicit path, so the file must physically land in your output, and NuGet doesn't flatten &lt;code&gt;runtimes/&lt;/code&gt; for a no-RID build. macOS (&lt;code&gt;osx-x64&lt;/code&gt;/&lt;code&gt;osx-arm64&lt;/code&gt; &lt;code&gt;.dylib&lt;/code&gt;) builds from the same CMake project but needs a macOS runner — the one remaining gap, slated for the CI matrix. &lt;strong&gt;Pro has no native dependency — it's already everywhere today&lt;/strong&gt;, which is exactly what WASM/mobile need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bool&lt;/code&gt; in raw form is &lt;code&gt;0&lt;/code&gt;/&lt;code&gt;1&lt;/code&gt;.&lt;/strong&gt; Remember it when debugging &lt;code&gt;_Boolean&lt;/code&gt;/&lt;code&gt;_value_bool&lt;/code&gt;: &lt;code&gt;true&lt;/code&gt; is stored as &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these poke through ordinary code — but in a debug session each one saves an evening.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7. Pro on mobile and in the browser — and yes, free
&lt;/h2&gt;

&lt;p&gt;Back to what the whole SQLite effort was for.&lt;/p&gt;

&lt;p&gt;Writing &lt;strong&gt;Blazor WebAssembly, MAUI, or a standalone client&lt;/strong&gt;? You want &lt;strong&gt;SQLite Pro&lt;/strong&gt;: pure C#, loads no native code, runs in the browser sandbox and on a phone. A typed LINQ store in a single file inside your app.&lt;/p&gt;

&lt;p&gt;And the part that draws skepticism, so plainly: &lt;strong&gt;Pro for this is free.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;a href="https://redbase.app" rel="noopener noreferrer"&gt;redbase.app&lt;/a&gt;, register, and &lt;strong&gt;email a key request&lt;/strong&gt; — you get a &lt;strong&gt;free license key&lt;/strong&gt; back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No bank/payment details.&lt;/strong&gt; No card asked. Registration is the key-issuing mechanism, not a sales funnel.&lt;/li&gt;
&lt;li&gt;The key goes in &lt;code&gt;.WithLicense(...)&lt;/code&gt;; the wiring instructions are right there after you register.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The barrier to a client-side scenario is zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;The SQLite provider made us do two non-obvious things and catch three bugs that would've cost more in prod than any review.&lt;/p&gt;

&lt;p&gt;The non-obvious things: move the whole query engine into a &lt;strong&gt;C extension&lt;/strong&gt; where the database has no stored procedures (and where a &lt;code&gt;%&lt;/code&gt; in your data breaks &lt;code&gt;mprintf&lt;/code&gt;), and re-answer &lt;strong&gt;"how do you store a date"&lt;/strong&gt; for a database that has no date type — REAL Julian in UTC, sargable comparisons, &lt;code&gt;julianday()&lt;/code&gt; on the literal side, and a neutral core hook instead of leaking SQLite specifics.&lt;/p&gt;

&lt;p&gt;The bugs: a silently-dropped &lt;code&gt;IS NULL&lt;/code&gt; in a multi-key filter (&lt;code&gt;json_each.value&lt;/code&gt; is lossy by type), &lt;code&gt;DISTINCT ON&lt;/code&gt; via &lt;code&gt;ROW_NUMBER()&lt;/code&gt; instead of an ignore, and Pro accidentally reaching into the Free-only &lt;code&gt;get_object_json&lt;/code&gt; on the subtree-delete path.&lt;/p&gt;

&lt;p&gt;These are exactly the spots where the "one LINQ for every database" abstraction either holds or leaks. Ours holds: Free and Pro are green at &lt;strong&gt;200/200&lt;/strong&gt; on the same suite as the other dialects, and you can poke at it with &lt;code&gt;redb.Examples&lt;/code&gt; and &lt;code&gt;redb.CLI&lt;/code&gt; from the repo.&lt;/p&gt;

&lt;p&gt;Repo, docs, packages — &lt;a href="https://redbase.app" rel="noopener noreferrer"&gt;redbase.app&lt;/a&gt;. The stack is at &lt;strong&gt;3.2.1&lt;/strong&gt;; the two SQLite NuGet packages (&lt;code&gt;redb.SQLite&lt;/code&gt; / &lt;code&gt;redb.SQLite.Pro&lt;/code&gt;) are &lt;strong&gt;3.2.1&lt;/strong&gt; — a &lt;code&gt;buildTransitive&lt;/code&gt; .targets hotfix, see War story #5. Questions like "does it do X on SQLite," bug reports — bring them; the provider's new and the feedback channel is open.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>mobile</category>
      <category>opensource</category>
      <category>sqlite</category>
    </item>
    <item>
      <title>SQLite provider for RedBase is coming.
full LINQ, typed columns — same API as PostgreSQL and MSSQL.
Free tier: native extension (.so / .dll / .dylib).

Pro tier: pure C# — works in Blazor WASM.
SQLite 3.44.0+.
Follow if you want to know when it drops.</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Thu, 18 Jun 2026 19:00:50 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/sqlite-provider-for-redbase-is-coming-full-linq-typed-columns-same-api-as-postgresql-and-13ie</link>
      <guid>https://dev.to/rinat_kozin/sqlite-provider-for-redbase-is-coming-full-linq-typed-columns-same-api-as-postgresql-and-13ie</guid>
      <description></description>
      <category>csharp</category>
      <category>database</category>
      <category>dotnet</category>
      <category>showdev</category>
    </item>
    <item>
      <title>.net</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Thu, 18 Jun 2026 16:38:21 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/net-28f4</link>
      <guid>https://dev.to/rinat_kozin/net-28f4</guid>
      <description></description>
    </item>
    <item>
      <title>Apache Camel for .NET, dissected: the HTTP connector with no ASP.NET MVC + the Content-Based Router pattern</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Thu, 18 Jun 2026 15:48:31 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/a-homegrown-apache-camel-for-net-dissected-the-http-connector-with-no-aspnet-mvc-the-56bd</link>
      <guid>https://dev.to/rinat_kozin/a-homegrown-apache-camel-for-net-dissected-the-http-connector-with-no-aspnet-mvc-the-56bd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgygk0batzkfx9431zdl3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgygk0batzkfx9431zdl3.jpg" alt="redb route http" width="784" height="1168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Series:&lt;/strong&gt; redb ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This continues the redb.Route series. Earlier on dev.to:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/i-spent-a-year-building-apache-camel-for-net-heres-the-honest-state-of-it-150e"&gt;I spent a year building Apache Camel for .NET. Here's the honest state of it.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/redbroute-apache-camel-for-net-22-transports-30-eip-patterns-compiled-dsl-11m0"&gt;redb.Route — Apache Camel for .NET: 22 transports, 30+ EIP patterns, compiled DSL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/enterprise-integration-patterns-in-net-the-deep-dive-series-part-1-the-four-in-memory-channels-3e24"&gt;Enterprise Integration Patterns in .NET, the deep-dive series — Part 1: the four in-memory channels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/redbroute-301-flat-dsl-navigation-crtp-refactor-and-a-silent-null-fix-3m7n"&gt;redb.Route 3.0.1 — flat DSL navigation, CRTP refactor, and a silent null fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/redbroute-310-llmai-as-just-another-connector-tollmclaude-and-tools-as-routes-4fcg"&gt;redb.Route 3.1.0 — LLM(AI) as just another connector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/enterprise-grade-ai-integration-embedding-llms-into-the-business-processes-of-large-companies--3a36"&gt;Enterprise-grade AI integration: embedding LLMs into the business processes — redb.Route.Llm 3.1.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full list on the &lt;a href="https://dev.to/rinat_kozin/"&gt;author's dev.to profile&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/"&gt;redb.Route&lt;/a&gt; — our &lt;a href="https://camel.apache.org/" rel="noopener noreferrer"&gt;Apache Camel&lt;/a&gt;-style ESB for .NET — a route always reads the same way: &lt;code&gt;From(source) → [processors] → To(sink)&lt;/code&gt;. This installment takes &lt;strong&gt;one simple integration pattern&lt;/strong&gt; and &lt;strong&gt;one connector&lt;/strong&gt; and dissects both all the way down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The pattern:&lt;/strong&gt; the &lt;a href="https://www.enterpriseintegrationpatterns.com/patterns/messaging/ContentBasedRouter.html" rel="noopener noreferrer"&gt;Content-Based Router&lt;/a&gt; — the most basic of the routing patterns from &lt;a href="https://www.enterpriseintegrationpatterns.com/" rel="noopener noreferrer"&gt;Hohpe &amp;amp; Woolf&lt;/a&gt;: look inside a message and decide where it goes next. In the DSL it's &lt;code&gt;.Choice().When(...).Otherwise()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The connector:&lt;/strong&gt; &lt;code&gt;redb.Route.Http&lt;/code&gt; — built-in HTTP/HTTPS. On one side it's a &lt;strong&gt;producer&lt;/strong&gt; (an &lt;code&gt;HttpClient&lt;/code&gt;-based caller); on the other, a &lt;strong&gt;consumer&lt;/strong&gt; (an embedded &lt;a href="https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel" rel="noopener noreferrer"&gt;Kestrel&lt;/a&gt; server). No controllers, no &lt;code&gt;[ApiController]&lt;/code&gt;, no ASP.NET middleware pipeline you wire up by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a long, technical piece. You'll get the five-line "hello world", but then we go into how the connector works internally: how a single Kestrel is shared across routes, how request headers and route values flow into the &lt;code&gt;Exchange&lt;/code&gt; and back, how CORS actually works on a shared server, what happens with streaming, and why there isn't a single &lt;code&gt;app.UseCors()&lt;/code&gt; in the codebase.&lt;/p&gt;

&lt;p&gt;Every snippet is verified against &lt;code&gt;redb.Route/src/redb.Route.Http&lt;/code&gt;; every example is lifted from the real &lt;code&gt;redb.Route.Demo&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 0. The scenario everything hangs on
&lt;/h2&gt;

&lt;p&gt;Take a down-to-earth task: an HTTP gateway. A &lt;code&gt;POST /api/demo&lt;/code&gt; comes in, and inside we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;take the body,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;look at the &lt;code&gt;mode&lt;/code&gt; header&lt;/strong&gt; and pick a processing branch accordingly — that's the Content-Based Router;&lt;/li&gt;
&lt;li&gt;reply synchronously over the same HTTP request (request/reply).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the skeleton (full version at the end), from &lt;code&gt;redb.Route.Demo/Routes/MainPipelineRoutes.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/demo?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo-http-entry"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stamp.dsl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"full-branch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"short"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stamp.dsl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"short-branch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stamp.dsl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"default-branch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BuildResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;From&lt;/code&gt; brings up an HTTP server on port 5088, one &lt;code&gt;.Choice()&lt;/code&gt; decides the message's fate, one &lt;code&gt;.SetBody(...)&lt;/code&gt; builds the reply. Now let's see how it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1. Content-Based Router — the simple pattern, honestly dissected
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What it actually is
&lt;/h3&gt;

&lt;p&gt;The Content-Based Router answers one question — &lt;em&gt;"where next?"&lt;/em&gt; — by looking at the message itself, not at external configuration. The textbook example: orders with &lt;code&gt;region=EU&lt;/code&gt; go to one handler, &lt;code&gt;region=US&lt;/code&gt; to another, everything else to a default.&lt;/p&gt;

&lt;p&gt;In redb.Route this is the &lt;code&gt;ChoiceProcessor&lt;/code&gt; (&lt;code&gt;redb.Route/src/redb.Route/Processors/ChoiceProcessor.cs&lt;/code&gt;), and in the DSL it's a &lt;code&gt;.Choice()&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// branch 1&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="n"&gt;processors&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// branch 2&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="n"&gt;processors&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;         &lt;span class="c1"&gt;// default branch (optional)&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="n"&gt;processors&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Semantics are exactly a &lt;code&gt;switch&lt;/code&gt;: predicates are checked &lt;strong&gt;top to bottom&lt;/strong&gt;, the &lt;strong&gt;first&lt;/strong&gt; branch whose predicate returns &lt;code&gt;true&lt;/code&gt; runs, the rest are skipped. If none match and there's an &lt;code&gt;.Otherwise()&lt;/code&gt;, it runs; if there's no &lt;code&gt;.Otherwise()&lt;/code&gt;, the message passes through untouched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two ways to express a predicate
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. A lambda&lt;/strong&gt; — when the condition is easier to write as code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"short"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Fluent predicates over the expression engine&lt;/strong&gt; — when you want it declarative. From &lt;code&gt;redb.Route.Demo/Routes/DataObservabilityRoutes.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isGreaterThanOrEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gold"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;999&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"silver"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isLessThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bronze"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Header("amount").isBetween(500, 999)&lt;/code&gt; isn't a closure — it's a real &lt;code&gt;IPredicate&lt;/code&gt;, compiled once and cached as a delegate forever after. Under the hood it's the series' compiled expression engine (&lt;code&gt;Tokenizer → Parser → AST → System.Linq.Expressions → IL&lt;/code&gt;), but that's a whole article of its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why pair it with HTTP
&lt;/h3&gt;

&lt;p&gt;The Content-Based Router and an HTTP gateway are made for each other. On the way in, the HTTP consumer decomposes the request into &lt;code&gt;Exchange&lt;/code&gt; headers (more below): method, path, query params, route params, every HTTP header. Any of them is ready material for &lt;code&gt;.When(...)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"redbHttp.Method"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"DELETE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;// by method&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"X-Tenant"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"acme"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;// by header&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"redbHttp.QueryParam.debug"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;// by query&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router never touches HTTP itself — it works on an already-decoded &lt;code&gt;Exchange&lt;/code&gt;. That's the whole point of a connector: turn transport into a message so the integration patterns know nothing about transport.&lt;/p&gt;

&lt;h3&gt;
  
  
  Straight from production
&lt;/h3&gt;

&lt;p&gt;Here's a &lt;strong&gt;production&lt;/strong&gt; route from the TsUM system (delivery monitoring), verbatim. HTTP entry + Content-Based Router by method — GET and POST on one path fan out to different handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5090/api/tsum/user-filters?inOut=true&amp;amp;cors=true&amp;amp;corsOrigins=*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tsum-api-user-filters"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;// JWT auth — just a processor&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.Method"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandlePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see everything we're about to cover in one shot: the HTTP consumer on port 5090 (&lt;code&gt;inOut=true&lt;/code&gt;, &lt;code&gt;cors=true&amp;amp;corsOrigins=*&lt;/code&gt;), the Content-Based Router over &lt;code&gt;redbHttp.Method&lt;/code&gt;, and authentication as &lt;strong&gt;an ordinary processor&lt;/strong&gt; in the chain — no &lt;code&gt;[Authorize]&lt;/code&gt; attributes. Now let's dissect how each piece works inside.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2. The HTTP connector from 10,000 feet
&lt;/h2&gt;

&lt;p&gt;The same &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; scheme yields two fundamentally different roles depending on whether it sits in &lt;code&gt;From(...)&lt;/code&gt; or &lt;code&gt;To(...)&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Built on&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Consumer&lt;/strong&gt; (&lt;code&gt;From&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HttpConsumer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Kestrel&lt;/td&gt;
&lt;td&gt;Brings up an embedded HTTP server and accepts inbound requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Producer&lt;/strong&gt; (&lt;code&gt;To&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HttpProducer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HttpClient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sends outbound HTTP requests to a remote address&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The DSL entry points are the static &lt;code&gt;Http&lt;/code&gt; and &lt;code&gt;Https&lt;/code&gt; classes (&lt;code&gt;redb.Route.Http/Fluent/HttpDsl.cs&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Consumer — listen for inbound&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/webhook"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Cors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://app.example.com"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;InOut&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// Producer — send outbound&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/orders"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;BearerAuth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the raw URI string (the builder compiles to exactly this):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:8080/webhook?cors=true&amp;amp;corsOrigins=https://app.example.com&amp;amp;inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:api.example.com/orders?method=POST&amp;amp;timeout=5000"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fei0rvjjjkq0o6b2log14.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fei0rvjjjkq0o6b2log14.png" alt="tsak route http" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP methods: the consumer listens, the producer sends
&lt;/h3&gt;

&lt;p&gt;Methods (GET/POST/PUT/…) are set differently for the two roles — and there's more than one way. First the &lt;strong&gt;producer&lt;/strong&gt; (&lt;code&gt;To&lt;/code&gt;) — which method to &lt;strong&gt;send&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fluent — the method is chosen by the factory method:&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/users"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;           &lt;span class="c1"&gt;// GET&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/users"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;          &lt;span class="c1"&gt;// POST&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/users/42"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;        &lt;span class="c1"&gt;// PUT&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/users/42"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;// DELETE&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/users/42"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;      &lt;span class="c1"&gt;// PATCH&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/users/42"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;       &lt;span class="c1"&gt;// HEAD&lt;/span&gt;

&lt;span class="c1"&gt;// the same as a URI string (singular parameter — method):&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:api.example.com/users?method=POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// the method can be overridden per-message by a header — it wins over the option:&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.Method"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"PUT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/users/42"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;        &lt;span class="c1"&gt;// actually sends PUT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the &lt;strong&gt;consumer&lt;/strong&gt; (&lt;code&gt;From&lt;/code&gt;) — which methods to &lt;strong&gt;accept&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// by default ALL methods are accepted:&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/webhook"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// restrict the allowed set (anything else → 405 Method Not Allowed):&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/webhook"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Methods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Methods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST,PUT"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// the same as a URI string (plural parameter — methods):&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:8080/webhook?methods=POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// shorthand: a method prefix right in the path (for the consumer):&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:POST:0.0.0.0:8080/webhook"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:GET:/health"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminology difference is easy to trip over:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Producer (&lt;code&gt;To&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Consumer (&lt;code&gt;From&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;URI parameter&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;method&lt;/code&gt; (singular)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;methods&lt;/code&gt; (plural, comma-separated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meaning&lt;/td&gt;
&lt;td&gt;which method to &lt;strong&gt;send&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;which methods to &lt;strong&gt;accept&lt;/strong&gt; (empty = all)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;all methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-message override&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;redbHttp.Method&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;— (the filter is static)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The prefix shorthand (&lt;code&gt;http:POST:/...&lt;/code&gt;) is a special case: it sets &lt;strong&gt;both&lt;/strong&gt; values at once (&lt;code&gt;method&lt;/code&gt; and &lt;code&gt;methods&lt;/code&gt;), because the same string can serve as either a producer or a consumer.&lt;/p&gt;

&lt;p&gt;And the typical move when &lt;strong&gt;different&lt;/strong&gt; methods hit one path: accept several and fan them out with a Content-Based Router on &lt;code&gt;redbHttp.Method&lt;/code&gt; (this is exactly the production example from Part 1):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/tsum/user-filters"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5090&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Methods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET,POST"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Cors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;InOut&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"redbHttp.Method"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandlePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// write&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessWithRedb&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;    &lt;span class="c1"&gt;// read&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll dissect both roles separately — but first, the big question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3. "Where's ASP.NET?" — there isn't any, on purpose
&lt;/h2&gt;

&lt;p&gt;When a .NET developer hears "embedded HTTP server", they picture &lt;code&gt;WebApplication&lt;/code&gt;, controllers, &lt;code&gt;[HttpPost]&lt;/code&gt;, filters, model binding, &lt;code&gt;app.UseRouting()&lt;/code&gt;, &lt;code&gt;app.UseCors()&lt;/code&gt;, a DI middleware pipeline. &lt;strong&gt;The redb.Route HTTP connector has none of that.&lt;/strong&gt; There's Kestrel — bare, with no MVC layer on top.&lt;/p&gt;

&lt;p&gt;Here's how the server comes up (&lt;code&gt;SharedHttpServerManager.StartServer&lt;/code&gt;, abridged):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSlimBuilder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// slim — no MVC, no extra services&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureKestrel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kestrel&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;kestrel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPAddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listenOptions&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;listenOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Protocols&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;protocols&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// HTTP/1, /2, /3 — see below&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ssl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;listenOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHttps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SslCertPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SslCertPassword&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;kestrel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Limits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRequestBodySize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRequestBodySize&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRequestBodySize&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClearProviders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ONE catch-all endpoint — we route from here ourselves&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{**path}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleCatchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleCatchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleCatchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// ... PUT/DELETE/PATCH/HEAD/OPTIONS on "/"&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WebApplication.CreateSlimBuilder()&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;CreateBuilder()&lt;/code&gt;. The &lt;a href="https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/webapplication" rel="noopener noreferrer"&gt;slim builder&lt;/a&gt; doesn't drag in MVC, Razor, ASP.NET auth, or the rest of the scaffolding — only what Kestrel needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exactly one catch-all route&lt;/strong&gt; &lt;code&gt;/{**path}&lt;/code&gt;. ASP.NET routing is used solely to intercept &lt;em&gt;everything&lt;/em&gt; and hand it to our own dispatcher, &lt;code&gt;HandleCatchAll&lt;/code&gt;. No controller matching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;builder.Logging.ClearProviders()&lt;/code&gt;&lt;/strong&gt; — the server stays quiet on the host console; logs go through the route's logger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why? Because redb.Route is an integration engine, and HTTP is just transport to it — the same as Kafka or RabbitMQ. A route shouldn't know Kestrel is behind it: it gets an &lt;code&gt;Exchange&lt;/code&gt;. ASP.NET controllers would impose their own model (attributes, model binding, &lt;code&gt;ActionResult&lt;/code&gt;) that's redundant and alien inside a DSL route.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Kestrel per process, not per route — and Tsak relies on it
&lt;/h3&gt;

&lt;p&gt;A frequent question: &lt;em&gt;"if my app already runs on ASP.NET/Kestrel (say, inside a Tsak worker), does the connector reuse the server or spawn new ones?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;First, what the connector does &lt;strong&gt;not&lt;/strong&gt; do: it doesn't graft onto an external ASP.NET pipeline. The &lt;code&gt;redb.Route.Http&lt;/code&gt; project has zero integration points with an external host (no &lt;code&gt;IApplicationBuilder&lt;/code&gt;, no &lt;code&gt;UseEndpoints&lt;/code&gt;, no &lt;code&gt;IServer&lt;/code&gt; — none of it). It stands up its own Kestrel via &lt;code&gt;WebApplication.CreateSlimBuilder()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But "stands up its own" ≠ "spawns instances." The key is that &lt;code&gt;SharedHttpServerManager&lt;/code&gt; is registered in DI as a &lt;strong&gt;singleton&lt;/strong&gt; (&lt;code&gt;redb.Route.Http/Extensions/ServiceCollectionExtensions.cs&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="nf"&gt;AddRedbRouteHttp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SharedHttpServerManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// ← one server manager per process&lt;/span&gt;
    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpComponent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SharedHttpServerManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpsComponent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One manager per process means &lt;strong&gt;one Kestrel pool, shared across all route contexts&lt;/strong&gt;. And Tsak leans on exactly this. Its worker is a plain &lt;code&gt;Host.CreateDefaultBuilder&lt;/code&gt; (&lt;strong&gt;not&lt;/strong&gt; a &lt;code&gt;WebApplication&lt;/code&gt;) — it has &lt;strong&gt;no Kestrel of its own&lt;/strong&gt;. Even Tsak's own REST admin API (the &lt;code&gt;_system&lt;/code&gt; context, port 9090 by default) is not a separate web server — it's an ordinary redb.Route HTTP route brought up through that same singleton manager (&lt;code&gt;redb.Tsak.Core/Services/SystemContextBuilder.cs&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Tsak registers the HTTP connector itself...&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRedbRouteHttp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// redb.Tsak.Core/Extensions/ServiceCollectionExtensions.cs&lt;/span&gt;

&lt;span class="c1"&gt;// ...and brings up its admin API as a regular route on the shared manager:&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;listenUri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"http:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{{**&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;?host=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;port=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;inOut=true"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;routeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listenUri&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* bridge → auth → dispatch */&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So nothing is "forwarded from the host's Kestrel" — on the contrary, &lt;strong&gt;the host (Tsak) brings up Kestrel through the connector and reuses it&lt;/strong&gt;. Any route targeting the same &lt;code&gt;(host, port)&lt;/code&gt; &lt;strong&gt;joins the already-running&lt;/strong&gt; server instead of starting a second one. Tsak even mounts its &lt;code&gt;system-echo&lt;/code&gt; route on the admin port — and they don't collide: the specificity ordering from Part 4 separates the concrete &lt;code&gt;/api/echo&lt;/code&gt; from the catch-all &lt;code&gt;{**path}&lt;/code&gt; (called out in a comment in &lt;code&gt;SystemContextBuilder&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Without Tsak — same thing, you just wire the manager yourself
&lt;/h3&gt;

&lt;p&gt;Tsak isn't magic here: Kestrel is &lt;strong&gt;always&lt;/strong&gt; brought up by &lt;code&gt;SharedHttpServerManager&lt;/code&gt;; Tsak is merely a host that registers that manager and routes through it. In a standalone app (no Tsak) you wire the manager by hand. Here's a bare &lt;code&gt;RouteContext&lt;/code&gt; from the &lt;code&gt;Llm.HttpShell&lt;/code&gt; demo — no DI, no Tsak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RouteContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"llm-http-shell"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ServerManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SharedHttpServerManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/llm/shell?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the same through DI — &lt;code&gt;AddRedbRouteHttp()&lt;/code&gt; registers that same singleton manager for you. Either way, the first &lt;code&gt;From("http:host:port/...")&lt;/code&gt; that starts brings up a fresh Kestrel via &lt;code&gt;CreateSlimBuilder()&lt;/code&gt; for that &lt;code&gt;(host, port)&lt;/code&gt; pair, and the rest of the routes on the same &lt;code&gt;(host, port)&lt;/code&gt; join it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Subtlety:&lt;/strong&gt; pooling works &lt;strong&gt;within a single&lt;/strong&gt; &lt;code&gt;SharedHttpServerManager&lt;/code&gt; instance. Create two separate managers and point both at one port and you get a socket-bind conflict, not sharing. The "one Kestrel per &lt;code&gt;(host, port)&lt;/code&gt;" guarantee comes from a &lt;em&gt;shared&lt;/em&gt; manager — a singleton out of the box under Tsak, and your responsibility standalone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The practical takeaway: within a process (and one manager) there are exactly as many Kestrels as there are distinct &lt;code&gt;(host, port)&lt;/code&gt; pairs. Mounting a route on a port another redb route already listens on (including Tsak's admin port) is fine — that's the whole "don't multiply" point. A socket-bind conflict only happens if a &lt;strong&gt;foreign&lt;/strong&gt;, non-redb server already grabbed the port.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why bus frameworks like MassTransit don't have this
&lt;/h3&gt;

&lt;p&gt;It's worth contrasting with &lt;a href="https://masstransit.io/" rel="noopener noreferrer"&gt;MassTransit&lt;/a&gt; — one of the most popular .NET messaging frameworks. It has &lt;strong&gt;no&lt;/strong&gt; HTTP consumer, no embedded server, no "http as a transport." And that's not an omission; it follows from the architecture.&lt;/p&gt;

&lt;p&gt;MassTransit is a &lt;strong&gt;message bus&lt;/strong&gt; over brokers: RabbitMQ, Azure Service Bus, Amazon SQS, plus Kafka/Event Hubs as "riders." Its model is asynchronous broker-mediated delivery with guarantees, retries, and sagas; consumers are keyed to a &lt;strong&gt;message type&lt;/strong&gt; (&lt;code&gt;IConsumer&amp;lt;TMessage&amp;gt;&lt;/code&gt;), not a URI endpoint. HTTP doesn't fit that picture: synchronous request/reply contradicts the async/durable bus model. So MassTransit leaves HTTP ingress to ASP.NET — you stand up a controller or minimal API and &lt;code&gt;Publish&lt;/code&gt;/&lt;code&gt;Send&lt;/code&gt; to the bus from there. The "HTTP → message" boundary lives &lt;strong&gt;outside&lt;/strong&gt; the framework, by hand, in your host code.&lt;/p&gt;

&lt;p&gt;redb.Route (like &lt;a href="https://camel.apache.org/" rel="noopener noreferrer"&gt;Apache Camel&lt;/a&gt;, which it follows) is built differently: it's a &lt;strong&gt;mediation engine&lt;/strong&gt;, and to it HTTP is just another transport, the same as Kafka or Rabbit. An HTTP request is normalized into the same &lt;code&gt;Exchange&lt;/code&gt; a broker message becomes, and flows through the same EIP processors. That's why &lt;code&gt;From("http:...")&lt;/code&gt; exists as a first-class route source, the connector owns Kestrel itself, and an "HTTP → Kafka → SQL → reply" bridge is one DSL chain without leaving the framework.&lt;/p&gt;

&lt;p&gt;These are different tools for different jobs, not "better/worse": MassTransit shines at reliable broker delivery and sagas over queues; Camel-style engines shine at stitching together heterogeneous transports and routing by content. An embedded HTTP server is a natural part of the second approach and fundamentally alien to the first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3½. "But I love controllers" — &lt;code&gt;redb.Route.Controllers&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is where an indignant voice usually pipes up: &lt;em&gt;"Attributes, &lt;code&gt;[HttpGet]&lt;/code&gt;, model binding — I like that, I don't want to write a &lt;code&gt;.Choice()&lt;/code&gt; per endpoint!"&lt;/em&gt; Fair. That's why there's a separate package: &lt;code&gt;redb.Route.Controllers&lt;/code&gt;. It hands you back the familiar MVC-controller ergonomics, but it does &lt;strong&gt;not&lt;/strong&gt; hand you back the ASP.NET hosting model. Here's the trick.&lt;/p&gt;

&lt;h3&gt;
  
  
  A controller that looks like ASP.NET — but isn't
&lt;/h3&gt;

&lt;p&gt;Here's a working controller (from the &lt;code&gt;redb.Route.Tests.Controllers&lt;/code&gt; tests):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"modules"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ModulesController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RedbController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"module1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"module2"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;FromRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"module-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateModuleRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;FromRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateModuleRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;FromRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Painfully familiar: &lt;code&gt;[Route]&lt;/code&gt; on the class, &lt;code&gt;[HttpGet]/[HttpPost]/[HttpPut]/[HttpDelete]/[HttpPatch]&lt;/code&gt; on methods (with an optional sub-template &lt;code&gt;"{id}"&lt;/code&gt;), parameter binding via &lt;code&gt;[FromBody]&lt;/code&gt;, &lt;code&gt;[FromRoute]&lt;/code&gt;, &lt;code&gt;[FromQuery]&lt;/code&gt;, &lt;code&gt;[FromHeader]&lt;/code&gt;, &lt;code&gt;[FromProperty]&lt;/code&gt;. Return an object and it goes out as JSON.&lt;/p&gt;

&lt;p&gt;But two differences are fundamental:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The base class is &lt;strong&gt;&lt;code&gt;RedbController&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;ControllerBase&lt;/code&gt;. No &lt;code&gt;HttpContext&lt;/code&gt;, no &lt;code&gt;IActionResult&lt;/code&gt;, no &lt;code&gt;[ApiController]&lt;/code&gt;. Instead, two properties: &lt;code&gt;Context&lt;/code&gt; (the route context) and &lt;code&gt;Exchange&lt;/code&gt; (the current message). The controller sees an &lt;code&gt;Exchange&lt;/code&gt;, not HTTP.&lt;/li&gt;
&lt;li&gt;The controller is &lt;strong&gt;transport-agnostic&lt;/strong&gt;. It knows nothing about HTTP. That matters one paragraph from now.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How a controller enters a route
&lt;/h3&gt;

&lt;p&gt;A controller isn't an endpoint — it's a &lt;strong&gt;processor inside a route&lt;/strong&gt;. You mount it on an HTTP entry via &lt;code&gt;.RedbHttpController&amp;lt;T&amp;gt;()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/{**path}?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"modules-api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RedbHttpController&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ModulesController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via a registry with several controllers (or an assembly scan):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ControllerRegistry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModulesController&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ContextsController&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// or: registry.RegisterAssembly(typeof(ModulesController).Assembly);&lt;/span&gt;

&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/{**path}?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RedbHttpController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;{**path}&lt;/code&gt; — the HTTP consumer from Part 5 catches &lt;strong&gt;everything&lt;/strong&gt; under &lt;code&gt;/api&lt;/code&gt;, drops &lt;code&gt;redbHttp.Method&lt;/code&gt;, &lt;code&gt;redbHttp.Path&lt;/code&gt;, &lt;code&gt;redbHttp.RouteParam.*&lt;/code&gt;, &lt;code&gt;redbHttp.QueryParam.*&lt;/code&gt; into the &lt;code&gt;Exchange&lt;/code&gt;, and the &lt;code&gt;HttpControllerDispatcher&lt;/code&gt; parses those and finds the right action. No manual header translation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// HttpControllerDispatcher.Process (abridged)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.Method"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.Path"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalizedPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;routeParams&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;WriteError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"NotFound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Routing and binding — its own, not ASP.NET's
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ControllerRegistry.Resolve&lt;/code&gt; matches &lt;code&gt;(method, path)&lt;/code&gt; to an action segment by segment, picking the &lt;strong&gt;most specific&lt;/strong&gt; one (literals beat &lt;code&gt;{param}&lt;/code&gt;): &lt;code&gt;GET /me/sessions/current&lt;/code&gt; beats &lt;code&gt;GET /me/sessions/{id}&lt;/code&gt;. Same specificity principle as the shared server in Part 4, just at the controller level.&lt;/p&gt;

&lt;p&gt;Parameter binding (&lt;code&gt;ResolveHttpParameter&lt;/code&gt;) is exactly what the attributes promise: &lt;code&gt;[FromBody]&lt;/code&gt; is JSON-deserialized from the &lt;code&gt;byte[]&lt;/code&gt;, &lt;code&gt;[FromRoute]&lt;/code&gt; comes from the template, &lt;code&gt;[FromQuery]&lt;/code&gt; from &lt;code&gt;redbHttp.QueryParam.*&lt;/code&gt;, &lt;code&gt;[FromHeader]&lt;/code&gt; / &lt;code&gt;[FromProperty]&lt;/code&gt; from the &lt;code&gt;Exchange&lt;/code&gt; headers/properties. With no attribute, it tries a route param by name, otherwise a complex type is bound from the body.&lt;/p&gt;

&lt;p&gt;The response is assembled like this: return an object → JSON (camelCase, with &lt;code&gt;UnsafeRelaxedJsonEscaping&lt;/code&gt; so Cyrillic and emoji don't become &lt;code&gt;А&lt;/code&gt;), status &lt;code&gt;200&lt;/code&gt;; return &lt;code&gt;null&lt;/code&gt;/&lt;code&gt;void&lt;/code&gt; → &lt;code&gt;204&lt;/code&gt;; throw → an error envelope and &lt;code&gt;500&lt;/code&gt; (a route miss → &lt;code&gt;404&lt;/code&gt;). The dispatcher sets &lt;code&gt;status.code&lt;/code&gt; and &lt;code&gt;redbHttp.ResponseCode&lt;/code&gt;, and the HTTP consumer from Part 5 picks them up. The loop closes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this way, not "the ASP.NET way"
&lt;/h3&gt;

&lt;p&gt;Here's the whole point. The same &lt;code&gt;ModulesController&lt;/code&gt;, without changing a line, can be invoked &lt;strong&gt;over something other than HTTP&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// same controller — over gRPC&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grpcConsumer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;RedbGrpcController&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ModulesController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// same controller — over SignalR&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signalRConsumer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;RedbSignalRController&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ModulesController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RedbController&lt;/code&gt; is transport-agnostic precisely because it works with an &lt;code&gt;Exchange&lt;/code&gt;, not an &lt;code&gt;HttpContext&lt;/code&gt;. An ASP.NET controller can't do that — it's welded to the HTTP pipeline. And since &lt;code&gt;.RedbHttpController&amp;lt;T&amp;gt;()&lt;/code&gt; is just a processor, it &lt;strong&gt;composes&lt;/strong&gt; with the rest of the DSL: put &lt;code&gt;.Throttle()&lt;/code&gt; before it, &lt;code&gt;.WireTap()&lt;/code&gt; after it, wrap it in &lt;code&gt;OnException&lt;/code&gt;/&lt;code&gt;TryCatch&lt;/code&gt;, combine it with the &lt;code&gt;.Choice()&lt;/code&gt; from Part 1.&lt;/p&gt;

&lt;p&gt;So: love controllers? Use controllers. Just know that under them is not ASP.NET MVC but the same &lt;code&gt;Exchange&lt;/code&gt; and the same route pipeline. You get the ergonomics without inheriting the hosting model.&lt;/p&gt;

&lt;h3&gt;
  
  
  And this isn't theory — Tsak itself runs on it
&lt;/h3&gt;

&lt;p&gt;The best proof the approach is battle-tested: &lt;strong&gt;Tsak's entire REST admin API is built exactly this way.&lt;/strong&gt; &lt;code&gt;ContextsController&lt;/code&gt;, &lt;code&gt;RoutesController&lt;/code&gt;, &lt;code&gt;ModulesController&lt;/code&gt;, &lt;code&gt;AuthController&lt;/code&gt;, &lt;code&gt;UsersController&lt;/code&gt;, &lt;code&gt;SchedulerController&lt;/code&gt;, &lt;code&gt;LogsController&lt;/code&gt;, and a dozen more (&lt;code&gt;redb.Tsak.Core/Controllers&lt;/code&gt;) all inherit &lt;code&gt;RedbController&lt;/code&gt; and carry the same &lt;code&gt;[Route]&lt;/code&gt; / &lt;code&gt;[HttpGet]&lt;/code&gt; / &lt;code&gt;[HttpPost]&lt;/code&gt; / &lt;code&gt;[FromRoute]&lt;/code&gt; / &lt;code&gt;[FromQuery]&lt;/code&gt;. Here's a slice of the real &lt;code&gt;ContextsController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/contexts"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContextsController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RedbController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;ListContexts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{name}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;GetContext&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;FromRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{name}/stop"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;StopContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FromRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FromQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"timeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;timeoutSeconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tsak registers their assembly (&lt;code&gt;ControllerRegistry.RegisterAssembly&lt;/code&gt;) and dispatches them via &lt;code&gt;ControllerDispatcherProcessor&lt;/code&gt; in the &lt;code&gt;_system&lt;/code&gt; context — on the same HTTP connector as everything else. So the controllers are as production-hardened as the HTTP consumer itself: the Tsak dashboard and CLI talk to exactly these.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A full treatment of &lt;code&gt;redb.Route.Controllers&lt;/code&gt; (the registry, the &lt;code&gt;IControllerActionFilter&lt;/code&gt; action filters, the gRPC/SignalR dispatchers, the error envelope) is its own article in the series. Here it's just to put the "where are my controllers" question to rest.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 4. One Kestrel per (host, port) — &lt;code&gt;SharedHttpServerManager&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The least obvious part of the consumer is that &lt;strong&gt;multiple routes can listen on one port&lt;/strong&gt;. If you have three &lt;code&gt;From(...)&lt;/code&gt; on &lt;code&gt;0.0.0.0:5088&lt;/code&gt; with different paths (&lt;code&gt;/api/demo&lt;/code&gt;, &lt;code&gt;/api/echo&lt;/code&gt;, &lt;code&gt;/api/llm/ask&lt;/code&gt;), you get &lt;strong&gt;one&lt;/strong&gt; Kestrel, not three.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SharedHttpServerManager&lt;/code&gt; owns this. The key is the &lt;code&gt;(host, port)&lt;/code&gt; pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServerEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;BuildKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Registering a route
&lt;/h3&gt;

&lt;p&gt;When a consumer starts, it doesn't "create a server" — it &lt;strong&gt;registers a route&lt;/strong&gt; on the server for its &lt;code&gt;(host, port)&lt;/code&gt;. The server is created lazily, on the first registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// HttpConsumer.Start (abridged)&lt;/span&gt;
&lt;span class="n"&gt;_registration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_serverManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Methods&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HandleRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SslCertPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SslCertPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;corsOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRequestBodySize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_serverManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureStarted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;BaseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_serverManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBaseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// e.g. "http://localhost:5088"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RegisterRoute&lt;/code&gt; drops a &lt;code&gt;RouteRegistration&lt;/code&gt; (path template + methods + handler + CORS) into the &lt;code&gt;ServerEntry&lt;/code&gt;'s route list. &lt;code&gt;EnsureStarted&lt;/code&gt; brings up Kestrel if it isn't running; if it already is, it's a no-op.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dispatch: a route table of its own
&lt;/h3&gt;

&lt;p&gt;Every request lands in one &lt;code&gt;HandleCatchAll&lt;/code&gt;, which finds the matching route itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleCatchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServerEntry&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MatchRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Registration&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// path matched but method didn't → 405; otherwise → 404&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PathMatched&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status405MethodNotAllowed&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status404NotFound&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RouteValues&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"__redbRouteValues"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RouteValues&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// {id} etc. — into the Exchange next&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two subtleties worth knowing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. 404 vs 405.&lt;/strong&gt; If the path matched but the method isn't allowed, you get an honest &lt;code&gt;405 Method Not Allowed&lt;/code&gt;, not a &lt;code&gt;404&lt;/code&gt;. Small, but correct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Path templates and specificity ordering.&lt;/strong&gt; Paths are parsed via &lt;code&gt;TemplateParser&lt;/code&gt;/&lt;code&gt;TemplateMatcher&lt;/code&gt; from ASP.NET routing — &lt;code&gt;{id}&lt;/code&gt; parameters and &lt;code&gt;{**rest}&lt;/code&gt; catch-alls are supported. But the check order is &lt;strong&gt;not&lt;/strong&gt; registration order; it's by descending specificity (&lt;code&gt;ServerEntry.GetCompiled&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_compiled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;built&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasCatchAll&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// concrete paths first, catch-all last&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Literals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// more literal segments = more specific&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;// fewer parameters = more specific&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                        &lt;span class="c1"&gt;// ties: stable, by registration order&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why? So a concrete &lt;code&gt;/api/echo&lt;/code&gt; always beats a catch-all &lt;code&gt;/{**path}&lt;/code&gt; registered on the same port, even if the catch-all was registered first. Without this rule, a catch-all would swallow every later route. The behavior matches what you intuitively expect from ASP.NET routing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lifecycle: reference counting
&lt;/h3&gt;

&lt;p&gt;A server lives exactly as long as it has at least one route. When a consumer stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// HttpConsumer.Stop&lt;/span&gt;
&lt;span class="n"&gt;_serverManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UnregisterRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_registration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_serverManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopIfEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;StopIfEmpty&lt;/code&gt; stops and unloads Kestrel &lt;strong&gt;only if no routes remain&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;StopIfEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// someone's still listening — leave it&lt;/span&gt;
    &lt;span class="n"&gt;_servers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryRemove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;StopServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// graceful stop with a 5s timeout&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if you stop one of three routes on port 5088, the server keeps running for the other two. Stop the last one, and Kestrel goes down. This lets you add and remove routes on the fly (e.g. from a dashboard: &lt;code&gt;tsak route start demo-http-echo&lt;/code&gt; / &lt;code&gt;stop&lt;/code&gt;) without bouncing the whole server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guard against mismatched schemes
&lt;/h3&gt;

&lt;p&gt;You can't bind both HTTP and HTTPS on the same &lt;code&gt;(host, port)&lt;/code&gt; — the manager throws on registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ssl&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;$"Server on &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is already registered as &lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ssl&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"HTTPS"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"HTTP"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 5. Consumer: how an HTTP request becomes an &lt;code&gt;Exchange&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the heart of the inbound side — &lt;code&gt;HttpConsumer.BuildExchange&lt;/code&gt;. Let's see exactly what reaches a route.&lt;/p&gt;

&lt;h3&gt;
  
  
  The request body
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentLength&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StreamRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// the stream as-is (passthrough)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CopyToAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                        &lt;span class="c1"&gt;// buffer into a byte[]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default the body is &lt;strong&gt;buffered into a &lt;code&gt;byte[]&lt;/code&gt;&lt;/strong&gt;. That's why you almost always see &lt;code&gt;.ConvertBody&amp;lt;string&amp;gt;()&lt;/code&gt; right after &lt;code&gt;From&lt;/code&gt; — to turn bytes into a string. With &lt;code&gt;streamRequest=true&lt;/code&gt; the body stays a &lt;code&gt;Stream&lt;/code&gt; (for large uploads), and an important detail from the code comment: that stream is owned by Kestrel, &lt;code&gt;Exchange.DisposeAsync&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; close it, but it stays valid until the response is written.&lt;/p&gt;

&lt;h3&gt;
  
  
  Headers: what the connector puts into the &lt;code&gt;Exchange&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is probably the single most useful reference table in the article. The consumer decomposes the request into &lt;code&gt;Exchange&lt;/code&gt; headers under the &lt;code&gt;redbHttp.&lt;/code&gt; prefix (&lt;code&gt;HttpHeaders.cs&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exchange header&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbHttp.Method&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP method&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbHttp.Path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;request path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/demo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbHttp.Url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;full URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:5088/api/demo?x=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbHttp.Port&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;server port (int)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5088&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbHttp.Query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;raw query string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x=1&amp;amp;y=2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbHttp.QueryParam.&amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one query parameter&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;redbHttp.QueryParam.x&lt;/code&gt; = &lt;code&gt;1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbHttp.RouteParam.&amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one path-template value&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/users/{id}&lt;/code&gt; → &lt;code&gt;redbHttp.RouteParam.id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbHttp.RemoteAddress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;client IP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;any HTTP header&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;as-is&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Content-Type&lt;/code&gt;, &lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;X-Chat-Id&lt;/code&gt;…&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few implementation details that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-value query/headers.&lt;/strong&gt; If a parameter repeats (&lt;code&gt;?tag=a&amp;amp;tag=b&lt;/code&gt;), the values are joined with a comma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;$"redbHttp.QueryParam.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]!,&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Route values from the template.&lt;/strong&gt; Remember &lt;code&gt;ctx.Items["__redbRouteValues"]&lt;/code&gt; from the dispatcher? Here's where they're unpacked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"__redbRouteValues"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rvObj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rvObj&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;RouteValueDictionary&lt;/span&gt; &lt;span class="n"&gt;routeValues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;routeValues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;$"redbHttp.RouteParam.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;From("http:0.0.0.0:8080/users/{id}")&lt;/code&gt; gives you &lt;code&gt;${header.redbHttp.RouteParam.id}&lt;/code&gt; in the route.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP/2 pseudo-headers are filtered out.&lt;/strong&gt; Headers like &lt;code&gt;:method&lt;/code&gt;, &lt;code&gt;:path&lt;/code&gt; (HTTP/2) are skipped — &lt;code&gt;if (header.Key.StartsWith(':')) continue;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remembering inbound header names.&lt;/strong&gt; Subtle but important. The connector collects the names of all inbound headers into a set and stores it in the exchange properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.RequestHeaderNames"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requestHeaderNames&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why — becomes clear on the response side: so request headers are &lt;strong&gt;not reflected back into the response&lt;/strong&gt;. Without it, an inbound &lt;code&gt;Host&lt;/code&gt; or &lt;code&gt;User-Agent&lt;/code&gt; could accidentally ride back to the client in the reply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exchange pattern: InOnly vs InOut
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InOut&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ExchangePattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InOut&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExchangePattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InOnly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;InOnly&lt;/strong&gt; (default) — fire-and-forget. The server replies with an empty &lt;code&gt;200 OK&lt;/code&gt; immediately, and the route runs "in the background" relative to the response. This is a webhook receiver: "got it, thanks."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;InOut&lt;/strong&gt; (&lt;code&gt;?inOut=true&lt;/code&gt;) — request/reply. The server &lt;strong&gt;waits&lt;/strong&gt; for the route to finish and returns the result as the HTTP response. This is an API endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice: to return a body in the response you need &lt;code&gt;inOut=true&lt;/code&gt;. Otherwise the body you assembled with &lt;code&gt;.SetBody(...)&lt;/code&gt; goes nowhere (see &lt;code&gt;WriteResponse&lt;/code&gt; — all body writing is under &lt;code&gt;if (_options.InOut)&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  The response: how an &lt;code&gt;Exchange&lt;/code&gt; becomes HTTP again
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;WriteResponse&lt;/code&gt; assembles the HTTP response. &lt;strong&gt;Status code&lt;/strong&gt; resolution order (by descending priority):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;statusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// 3. options default (200)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseMsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.ResponseCode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;   &lt;span class="c1"&gt;// 1. explicit header&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseMsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status.code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;        &lt;span class="c1"&gt;// 2. transport-neutral fallback&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So from a route you can return, say, &lt;code&gt;404&lt;/code&gt; just by setting a header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.ResponseCode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Response Content-Type&lt;/strong&gt; follows a similar chain: &lt;code&gt;redbHttp.ResponseContentType&lt;/code&gt; → &lt;code&gt;Message.ContentType&lt;/code&gt; → options default (&lt;code&gt;application/json&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Carrying headers into the response.&lt;/strong&gt; This is where that &lt;code&gt;RequestHeaderNames&lt;/code&gt; set earns its keep. A message header makes it into the HTTP response only if it clears several filters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;responseMsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestHeaderNames&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// don't reflect request headers&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NonBridgedHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// hop-by-hop and internal&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsRedbHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// redbHttp.* — internal&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsInternalHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// redb*/Camel* — internal&lt;/span&gt;
    &lt;span class="c1"&gt;// ... set with multi-value support&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a special touch for &lt;code&gt;Set-Cookie&lt;/code&gt; and other multi-value headers — &lt;code&gt;StringValues&lt;/code&gt; is used so ASP.NET emits &lt;strong&gt;multiple header lines&lt;/strong&gt; instead of one stringified array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;StringValues&lt;/span&gt; &lt;span class="n"&gt;sv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IEnumerable&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cast&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ContainsInvalidHeaderValueCharacters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Kestrel rejects control / non-ASCII&lt;/span&gt;
&lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A gotcha from the repo's history.&lt;/strong&gt; Response headers are copied &lt;strong&gt;always&lt;/strong&gt;, even when the body is empty. Otherwise body-less responses (a 302 redirect, a 204 No Content, a &lt;code&gt;Set-Cookie&lt;/code&gt;-only reply) would silently drop &lt;code&gt;Location&lt;/code&gt;/&lt;code&gt;Set-Cookie&lt;/code&gt;. It's called out in a code comment.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 6. CORS on a shared server — without &lt;code&gt;app.UseCors()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;CORS&lt;/a&gt; in ASP.NET means middleware and named policies. Here there's no ASP.NET CORS middleware at all. Instead there's &lt;strong&gt;one&lt;/strong&gt; dispatch layer per server that selects the policy &lt;strong&gt;by the matched route&lt;/strong&gt;. Why: a single &lt;code&gt;(host, port)&lt;/code&gt; hosts different routes, and each can carry its own CORS policy. A classic &lt;code&gt;UseCors&lt;/code&gt; with one policy per server can't do that.&lt;/p&gt;

&lt;h3&gt;
  
  
  The CORS parameters
&lt;/h3&gt;

&lt;p&gt;At the endpoint level (&lt;code&gt;HttpEndpointOptions&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URI parameter&lt;/th&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cors=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;enable CORS for the route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;corsOrigins=...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CorsOrigins&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;comma-separated origin whitelist, or &lt;code&gt;*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;corsCredentials=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CorsCredentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;allow &lt;code&gt;Access-Control-Allow-Credentials&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;— (code only)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CorsOriginsResolver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;HttpRequest → string?&lt;/code&gt; delegate for dynamic origin selection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the fluent DSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Cors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://app.example.com"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;CorsCredentials&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus &lt;strong&gt;global&lt;/strong&gt; defaults for the whole component, via DI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRedbRouteHttp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cors&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Origins&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Endpoint parameters always override globals (&lt;code&gt;HttpComponent.ApplyCorsDefaults&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  No implicit &lt;code&gt;*&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A deliberate decision: if &lt;code&gt;cors=true&lt;/code&gt;, you &lt;strong&gt;must&lt;/strong&gt; supply either &lt;code&gt;corsOrigins&lt;/code&gt; (including an explicit &lt;code&gt;"*"&lt;/code&gt; for public endpoints) or a resolver. Otherwise it throws at startup (&lt;code&gt;HttpEndpointOptions.Validate&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Cors&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CorsOrigins&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;CorsOriginsResolver&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"Cors=true requires CorsOrigins (use \"*\" for public endpoints) or CorsOriginsResolver to be set."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why: the old implicit &lt;code&gt;*&lt;/code&gt; is a classic footgun. Combined with credentials, the browser silently rejects it, and the developer burns an afternoon wondering why "CORS doesn't work." Far better to fail fast at startup with a clear message.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the dispatcher works (&lt;code&gt;CorsDispatchMiddleware&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The layer is installed once per server, and only if at least one route declared CORS (&lt;code&gt;entry.CorsEnabled&lt;/code&gt;). For each request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MatchByPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// NOTE: by path, method-agnostic&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Cors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cors&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;    &lt;span class="c1"&gt;// a route without CORS — the layer is transparent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matching &lt;strong&gt;by path, ignoring method&lt;/strong&gt; is deliberate — so an &lt;code&gt;OPTIONS&lt;/code&gt; preflight finds the route's policy even when &lt;code&gt;OPTIONS&lt;/code&gt; isn't in the route's allowed-methods list.&lt;/p&gt;

&lt;p&gt;Then origin resolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ResolveOrigin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requestOrigin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// wildcard+credentials footgun: the browser rejects it anyway → fail closed&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowCredentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Allow-Origin"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;AppendVary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Origin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// mandatory, else caches mix up policies&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowCredentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Allow-Credentials"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... preflight reflection below&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ResolveOrigin&lt;/code&gt; behaves like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;a resolver is set&lt;/strong&gt; → its word is final (may return a specific origin, &lt;code&gt;"*"&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;static &lt;code&gt;"*"&lt;/code&gt;&lt;/strong&gt; → emit &lt;code&gt;*&lt;/code&gt; verbatim;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;static whitelist&lt;/strong&gt; → reflect the request's &lt;code&gt;Origin&lt;/code&gt;, &lt;strong&gt;only if&lt;/strong&gt; it's in the list (browsers don't understand a CSV in &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;, so we single-select);&lt;/li&gt;
&lt;li&gt;otherwise → &lt;code&gt;null&lt;/code&gt; (origin not allowed, no CORS headers emitted).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Preflight (OPTIONS).&lt;/strong&gt; On preflight, the layer reflects the method and headers the browser asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;requestedMethod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Request-Method"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Allow-Methods"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestedMethod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;requestedMethod&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowedMethods&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;Methods&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;requestedHeaders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Request-Headers"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Allow-Headers"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestedHeaders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;requestedHeaders&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AllowCredentials&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"Content-Type, Authorization"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Access-Control-Max-Age"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// default 86400&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// OPTIONS always short-circuits to 204 — even when the origin was rejected&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status204NoContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the whole of CORS is a careful hand-rolled implementation of the spec over Kestrel, with a correct &lt;code&gt;Vary: Origin&lt;/code&gt;, proper preflight handling, and a guard against wildcard+credentials. No &lt;code&gt;app.UseCors()&lt;/code&gt; anywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7. Streaming the response: SSE and chunked
&lt;/h2&gt;

&lt;p&gt;An InOut consumer can return more than a finished body — it can return a &lt;strong&gt;stream&lt;/strong&gt;, an &lt;code&gt;IAsyncEnumerable&amp;lt;string&amp;gt;&lt;/code&gt;. This is used, for example, in the LLM connector to stream tokens. The logic lives in &lt;code&gt;WriteResponse&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;asyncStrings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;useSse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;responseContentType&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"text/event-stream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Cache-Control"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"no-cache, no-transform"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"X-Accel-Buffering"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// don't buffer at nginx/LB&lt;/span&gt;

    &lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IHttpResponseBodyFeature&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()?.&lt;/span&gt;&lt;span class="nf"&gt;DisableBuffering&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// turn off ASP.NET buffering&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;useSse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;WriteSseStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;          &lt;span class="c1"&gt;// text/event-stream → SSE framing&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;WriteChunkedTextStreamAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;  &lt;span class="c1"&gt;// otherwise → chunked plain text&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framing choice is driven by the &lt;strong&gt;response Content-Type&lt;/strong&gt; (the canonical end-to-end signal, &lt;code&gt;Accept ↔ Content-Type&lt;/code&gt;), not a private header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;text/event-stream&lt;/code&gt;&lt;/strong&gt; → &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events" rel="noopener noreferrer"&gt;Server-Sent Events&lt;/a&gt;: each yield → one &lt;code&gt;data:&lt;/code&gt; frame; at the end, an &lt;code&gt;event: done&lt;/code&gt; carrying late-bound summary headers (&lt;code&gt;llm.tokens.in/out&lt;/code&gt;, &lt;code&gt;llm.stop_reason&lt;/code&gt;, …) that the producer sets only &lt;strong&gt;after&lt;/strong&gt; iteration finishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;everything else&lt;/strong&gt; → chunked plain text: each yield is one chunk, with a &lt;code&gt;FlushAsync&lt;/code&gt; after each.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;DisableBuffering()&lt;/code&gt; is critical: without it, ASP.NET would accumulate chunks and flush them in a batch, killing the whole point of streaming.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8. Producer: an HTTP client with every knob
&lt;/h2&gt;

&lt;p&gt;Now the outbound side — &lt;code&gt;HttpProducer&lt;/code&gt; over &lt;code&gt;HttpClient&lt;/code&gt;. The client is created once in &lt;code&gt;ConnectAsync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;_handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpClientHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AllowAutoRedirect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FollowRedirects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// followRedirects (default true)&lt;/span&gt;
    &lt;span class="n"&gt;MaxAutomaticRedirections&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRedirects&lt;/span&gt;      &lt;span class="c1"&gt;// maxRedirects (default 50)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;_httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// timeout (default 30000)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InfiniteTimeSpan&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nf"&gt;ConfigureAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Building the URL — four layers
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ResolveUrl&lt;/code&gt; assembles the target address layer by layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Base URL from the endpoint, with ${...} expressions resolved&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResolveOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildProducerUrl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Substitute {name} parameters from .Param(...)&lt;/span&gt;
&lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ResolveNamedParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Query string from the redbHttp.Query header, if present&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.Query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'?'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"?"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;sep&lt;/span&gt;&lt;span class="p"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base URL is built by &lt;code&gt;HttpEndpoint.BuildProducerUrl()&lt;/code&gt; — simply &lt;code&gt;scheme + host[:port]/path&lt;/code&gt;. And &lt;code&gt;{name}&lt;/code&gt; parameters are substituted with URL escaping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ResolveNamedParams&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;valueTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResolveOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valueTemplate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;valueTemplate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;valueTemplate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{{{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EscapeDataString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api.example.com/users/{id}/orders"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;turns at runtime into &lt;code&gt;http://api.example.com/users/42/orders&lt;/code&gt;, where &lt;code&gt;42&lt;/code&gt; comes from the &lt;code&gt;userId&lt;/code&gt; header and is escaped.&lt;/p&gt;

&lt;h3&gt;
  
  
  The request method
&lt;/h3&gt;

&lt;p&gt;The method comes from options (&lt;code&gt;?method=POST&lt;/code&gt;) but can be &lt;strong&gt;overridden by the&lt;/strong&gt; &lt;code&gt;redbHttp.Method&lt;/code&gt; &lt;strong&gt;header&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.Method"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;hm&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;methodStr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SysHttpMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;methodStr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpperInvariant&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A body is set only for &lt;code&gt;POST/PUT/PATCH&lt;/code&gt; (&lt;code&gt;HasBody&lt;/code&gt;). &lt;code&gt;byte[]&lt;/code&gt; → &lt;code&gt;ByteArrayContent&lt;/code&gt;, &lt;code&gt;Stream&lt;/code&gt; → &lt;code&gt;StreamContent&lt;/code&gt;, anything else → &lt;code&gt;StringContent&lt;/code&gt; in UTF-8.&lt;/p&gt;

&lt;h3&gt;
  
  
  Header bridging
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;bridgeHeaders=true&lt;/code&gt; (default), &lt;code&gt;Exchange&lt;/code&gt; headers ride out into the HTTP request — except internal and hop-by-hop ones (&lt;code&gt;NonBridgedHeaders&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NonBridgedHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Connection, TE, redbHttp.*, Content-* etc.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryAddWithoutValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strValue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryAddWithoutValidation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// else try as a content header&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NonBridgedHeaders&lt;/code&gt; is the union of internal &lt;code&gt;redbHttp.*&lt;/code&gt;, hop-by-hop (&lt;code&gt;Connection&lt;/code&gt;, &lt;code&gt;Keep-Alive&lt;/code&gt;, &lt;code&gt;Transfer-Encoding&lt;/code&gt;, &lt;code&gt;TE&lt;/code&gt;, &lt;code&gt;Trailer&lt;/code&gt;, &lt;code&gt;Upgrade&lt;/code&gt;, &lt;code&gt;Proxy-*&lt;/code&gt;), and the content headers managed by &lt;code&gt;HttpClient&lt;/code&gt; (&lt;code&gt;Content-Type&lt;/code&gt;, &lt;code&gt;Content-Length&lt;/code&gt;, &lt;code&gt;Content-Encoding&lt;/code&gt;…).&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Three schemes (&lt;code&gt;HttpAuthScheme&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Basic&lt;/strong&gt; — &lt;code&gt;username&lt;/code&gt;/&lt;code&gt;password&lt;/code&gt;, the &lt;code&gt;Authorization: Basic ...&lt;/code&gt; header is set once on the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bearer, static&lt;/strong&gt; — a constant token, also set once on the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bearer, dynamic&lt;/strong&gt; — a token expression (&lt;code&gt;DynamicValue&amp;lt;string&amp;gt;.IsDynamic&lt;/code&gt;), resolved &lt;strong&gt;per request&lt;/strong&gt; from the &lt;code&gt;Exchange&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ConfigurePerRequestAuth&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthScheme&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HttpAuthScheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bearer&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsDynamic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;.BearerAuth().AuthToken(Header("jwt"))&lt;/code&gt; injects a fresh JWT from the header on every outbound call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Response → &lt;code&gt;Exchange&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MapResponse&lt;/code&gt; drops the response body into &lt;code&gt;Out&lt;/code&gt; and sets the standard headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;outMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.StatusCode"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;outMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.StatusText"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReasonPhrase&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;outMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"redbHttp.Url"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestMessage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;RequestUri&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;copyResponseHeaders=true&lt;/code&gt; (default) it copies the response headers plus &lt;code&gt;Content-Type&lt;/code&gt;/&lt;code&gt;Content-Length&lt;/code&gt;. With &lt;code&gt;streamResponse=true&lt;/code&gt; the body becomes a &lt;code&gt;Stream&lt;/code&gt;, not a &lt;code&gt;byte[]&lt;/code&gt; — and then the &lt;code&gt;HttpResponseMessage&lt;/code&gt; is deliberately &lt;strong&gt;not&lt;/strong&gt; disposed right away: &lt;code&gt;Exchange.DisposeAsync&lt;/code&gt; closes the stream.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;throwOnError&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;By default &lt;code&gt;throwOnError=true&lt;/code&gt; — on &lt;code&gt;4xx/5xx&lt;/code&gt; the producer throws an &lt;code&gt;HttpRequestException&lt;/code&gt; (which your &lt;code&gt;OnException&lt;/code&gt;/&lt;code&gt;TryCatch&lt;/code&gt; will catch). Turn it off with &lt;code&gt;.NoThrowOnError()&lt;/code&gt; if you'd rather handle statuses by hand via &lt;code&gt;${header.redbHttp.StatusCode}&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 9. Putting it all together — a real route
&lt;/h2&gt;

&lt;p&gt;Back to &lt;code&gt;MainPipelineRoutes.cs&lt;/code&gt;, here's the full HTTP entry with the Content-Based Router and request/reply (&lt;code&gt;redb.Route.Demo&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/demo?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo-http-entry"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;              &lt;span class="c1"&gt;// byte[] → string&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;// no more than 10 req/sec&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[1-HTTP] ▶ body=${body}, contentType=${contentType}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"N"&lt;/span&gt;&lt;span class="p"&gt;)[..&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[1-HTTP]   traceId=${header.traceId}, mode=${header.mode}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateJsonSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MessageSchema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// body must be JSON with a "message" field&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IdempotentConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"traceId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IdempotentRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// ── Content-Based Router: route by the mode header ──&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fastTrack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"high"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stamp.dsl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"full-branch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"short"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fastTrack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stamp.dsl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"short-branch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stamp.dsl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"default-branch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// ... in the demo: a cross-transport pipeline, SQL, WireTaps ...&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BuildResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;     &lt;span class="c1"&gt;// inOut=true → this is the HTTP response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit it with three curls — three different branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# full-branch&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5088/api/demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"mode: full"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"priority: high"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"message":"hello"}'&lt;/span&gt;

&lt;span class="c"&gt;# short-branch&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5088/api/demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"mode: short"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"message":"hi"}'&lt;/span&gt;

&lt;span class="c"&gt;# default-branch (no mode)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5088/api/demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"message":"yo"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And right next to it, on the same port 5088, lives an echo route (&lt;code&gt;EchoRoutes.cs&lt;/code&gt;) — a separate &lt;code&gt;From&lt;/code&gt;, the same shared Kestrel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/echo?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo-http-echo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AutoStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// dormant until you start it by hand&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* mirror the request's Content-Type */&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[ECHO] ◀ Echoing back: ${body}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.AutoStart(false)&lt;/code&gt; is a perfect illustration of the lifecycle from Part 4: the route is registered but doesn't start with the module. Start it by hand (&lt;code&gt;tsak route start demo-http-echo&lt;/code&gt;) and it registers on the &lt;strong&gt;already-running&lt;/strong&gt; Kestrel for port 5088. Stop it, and the server stays up — because &lt;code&gt;/api/demo&lt;/code&gt; is still listening.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha cheat sheet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No body in the response?&lt;/strong&gt; Check for &lt;code&gt;inOut=true&lt;/code&gt;. Without it the consumer returns an empty &lt;code&gt;200 OK&lt;/code&gt;, no matter what you do with &lt;code&gt;.SetBody(...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS "doesn't work"?&lt;/strong&gt; With &lt;code&gt;cors=true&lt;/code&gt; you must supply &lt;code&gt;corsOrigins&lt;/code&gt; (or a resolver) — otherwise it throws at startup. Wildcard &lt;code&gt;*&lt;/code&gt; + credentials is rejected by browsers, and the connector honestly returns the response with no CORS headers (fail closed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port already in use?&lt;/strong&gt; Within a process Kestrel is shared by &lt;code&gt;(host, port)&lt;/code&gt; through the singleton &lt;code&gt;SharedHttpServerManager&lt;/code&gt; — routes on one port share one server (under Tsak you can even sit on the admin API's port). A bind conflict only happens if a &lt;strong&gt;foreign&lt;/strong&gt;, non-redb server already took the port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request headers leaking into the response?&lt;/strong&gt; They shouldn't — the connector tracks them (&lt;code&gt;RequestHeaderNames&lt;/code&gt;) and won't reflect them. But if you set a header with the same name yourself, it will go out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream arrives as one batch?&lt;/strong&gt; SSE needs &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt; on the response; otherwise it's chunked plain text. Buffering is disabled automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;{id}&lt;/code&gt; didn't substitute on the producer?&lt;/strong&gt; The parameter is supplied via &lt;code&gt;.Param("id", ...)&lt;/code&gt;; without &lt;code&gt;.Param&lt;/code&gt;, the &lt;code&gt;{id}&lt;/code&gt; placeholder stays in the URL as-is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;405 instead of 404?&lt;/strong&gt; That's a feature: the path matched, the method didn't. Narrow &lt;code&gt;methods=&lt;/code&gt; or check which method you're calling with.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;redb.Route.Http&lt;/code&gt; isn't a "wrapper over a controller" — it's a transport in its own right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;consumer&lt;/strong&gt; brings up its own Kestrel (&lt;code&gt;CreateSlimBuilder&lt;/code&gt;, no MVC), shares it across routes by &lt;code&gt;(host, port)&lt;/code&gt; through the &lt;code&gt;SharedHttpServerManager&lt;/code&gt; singleton, keeps its own route table with proper specificity ordering, ref-counts for lifecycle, and implements CORS by hand — one policy per route.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;producer&lt;/strong&gt; is &lt;code&gt;HttpClient&lt;/code&gt; with every knob exposed: layered URL building, &lt;code&gt;{name}&lt;/code&gt; parameters, header bridging, Basic/Bearer (including a dynamic per-request token), response streaming, and a controllable &lt;code&gt;throwOnError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP ↔ Exchange&lt;/strong&gt; is a two-way bridge through &lt;code&gt;redbHttp.*&lt;/code&gt; headers, with honest handling of multi-values, route parameters, status codes, and body-less responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the &lt;strong&gt;Content-Based Router&lt;/strong&gt; (&lt;code&gt;.Choice().When().Otherwise()&lt;/code&gt;) shows what it's all for: a route makes decisions on the content of an already-decoded message and knows nothing about whether Kestrel, Kafka, or RabbitMQ sits beneath it.&lt;/p&gt;

&lt;p&gt;Next in the series we take the next connector and the next EIP cluster. Want a specific one? Say so in the comments.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every snippet is verified against the sources&lt;/strong&gt; in &lt;code&gt;redb.Route/src/redb.Route.Http&lt;/code&gt; (&lt;code&gt;HttpConsumer&lt;/code&gt;, &lt;code&gt;HttpProducer&lt;/code&gt;, &lt;code&gt;HttpComponent&lt;/code&gt;, &lt;code&gt;SharedHttpServerManager&lt;/code&gt;, &lt;code&gt;HttpEndpointOptions&lt;/code&gt;, &lt;code&gt;HttpHeaders&lt;/code&gt;, &lt;code&gt;Fluent/HttpDsl&lt;/code&gt;) and &lt;code&gt;redb.Route.Controllers&lt;/code&gt;. Examples come from the &lt;code&gt;redb.Route.Demo/Routes&lt;/code&gt; demo project (&lt;code&gt;MainPipelineRoutes&lt;/code&gt;, &lt;code&gt;EchoRoutes&lt;/code&gt;, &lt;code&gt;DataObservabilityRoutes&lt;/code&gt;) and from the &lt;strong&gt;production&lt;/strong&gt; TsUM system (&lt;code&gt;tsum.Api/Routes&lt;/code&gt;, &lt;code&gt;tsum.Api/Auth&lt;/code&gt;) — the same routes run in production on port 5090. The controllers section is production-backed: Tsak's entire REST admin API (&lt;code&gt;redb.Tsak.Core/Controllers&lt;/code&gt; — &lt;code&gt;Contexts&lt;/code&gt;, &lt;code&gt;Routes&lt;/code&gt;, &lt;code&gt;Modules&lt;/code&gt;, &lt;code&gt;Auth&lt;/code&gt;, &lt;code&gt;Users&lt;/code&gt;, … — ~14 controllers) is built on &lt;code&gt;RedbController&lt;/code&gt; and runs in production via &lt;code&gt;ControllerDispatcherProcessor&lt;/code&gt;. The application-level TsUM API prefers &lt;code&gt;.Process&lt;/code&gt;/&lt;code&gt;.Choice&lt;/code&gt; — both approaches coexist and both are battle-tested.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>http</category>
      <category>opensource</category>
    </item>
    <item>
      <title>redb.Route.Llm 3.1.1 — per-message audit fields for LLM compliance / replay</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Thu, 11 Jun 2026 22:48:57 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redbroutellm-311-per-message-audit-fields-for-llm-compliance-replay-4mi1</link>
      <guid>https://dev.to/rinat_kozin/redbroutellm-311-per-message-audit-fields-for-llm-compliance-replay-4mi1</guid>
      <description>&lt;p&gt;When an auditor asks "reproduce this exact answer" six months later, you need more than (messages, model). You need the sampling parameters that were actually applied, the prompt template version, the tool set the model saw, and ideally the provider's backend fingerprint — because closed-source providers silently re-release weights.&lt;/p&gt;

&lt;p&gt;Just shipped this in redb.Route.Llm 3.1.1: 7 nullable audit fields on every persisted message.&lt;/p&gt;

&lt;p&gt;Set on assistant rows:&lt;/p&gt;

&lt;p&gt;Temperature, MaxTokens, TopP — effective values (request override → factory default)&lt;br&gt;
ToolSetHash — SHA-256 of canonical {name, description, InputSchema} set, sorted by name. Tool added/renamed/schema-changed → hash changes&lt;br&gt;
ProviderSystemFingerprint — system_fingerprint from the response (OpenAI / xAI / Together echo it; Anthropic / Gemini-compat / Ollama leave it null)&lt;br&gt;
Set on every row of the run:&lt;/p&gt;

&lt;p&gt;PromptTemplateName, PromptTemplateVersion — when the caller used a managed template&lt;br&gt;
Honest limitation: bit-exact replay is only possible with self-hosted (Ollama / vLLM / llama.cpp). Anthropic doesn't expose a fingerprint, OpenAI rotates them silently. For closed providers we record (model_id, params, tool_set_hash, prompt_template) and label it best-effort.&lt;/p&gt;

&lt;p&gt;Zero migration. REDB stores props schemalessly — added 7 fields to the C# class, redeployed, done. No ALTER TABLE, no version bump on the storage layer.&lt;/p&gt;

&lt;p&gt;Code &amp;amp; details: CHANGELOG.md → 3.1.1 — section per-message audit fields on MessageProps.&lt;/p&gt;

&lt;p&gt;Triggered by feedback from a compliance auditor on Habr — turns out "we log conversations" doesn't cut it when the courtroom asks which exact prompt drove this.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Enterprise-grade AI integration: embedding LLMs into the business processes of large companies — redb.Route.Llm 3.1.1</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Wed, 10 Jun 2026 21:05:20 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/enterprise-grade-ai-integration-embedding-llms-into-the-business-processes-of-large-companies--3a36</link>
      <guid>https://dev.to/rinat_kozin/enterprise-grade-ai-integration-embedding-llms-into-the-business-processes-of-large-companies--3a36</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ssuz2slbrels1zh17p2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ssuz2slbrels1zh17p2.png" alt="redb route llm AI" width="659" height="797"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; redb ecosystem (LLM, part 2)&lt;/p&gt;

&lt;p&gt;A few days ago I announced &lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/"&gt;&lt;code&gt;redb.Route.Llm&lt;/code&gt;&lt;/a&gt; — the 24th transport in &lt;code&gt;redb.Route&lt;/code&gt;, where calling an LLM is &lt;code&gt;.To("llm://claude")&lt;/code&gt; and an agent tool is just a route with &lt;code&gt;.AsLlmTool("shell")&lt;/code&gt; on it. The whole pitch was: &lt;strong&gt;stop bolting an "AI framework" onto an integration framework that already has retry, throttle, breaker, audit, observability&lt;/strong&gt;. Plug the LLM in as one more endpoint, get those primitives for free.&lt;/p&gt;

&lt;p&gt;I closed that announcement with a deliberately uncomfortable section called the &lt;strong&gt;"honest skip-list"&lt;/strong&gt; — features I hadn't shipped yet: streaming end-to-end, tool cache, RAG knowledge store, async batch + callback consumer, eval-run store, sliding-window memory, sandboxed tool execution. I'd rather under-promise in writing than over-promise in screenshots.&lt;/p&gt;

&lt;p&gt;Most of that list is shipped. But that's not what this post is about. This post is about the thing I didn't quite see at announcement time: &lt;strong&gt;shipping the skip-list turned the whole thing into something different from a chat library&lt;/strong&gt;. It turns out that once you put an LLM into an ESB and start adding the boring enterprise plumbing, the result isn't "a chat framework with extras". It's a &lt;strong&gt;runway from a 6-line demo to a multi-tenant, audited, budgeted, human-in-the-loop agent platform — with no rewrite in the middle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the story I want to tell. Three angles: deep dive on the technically-tricky bits (streaming, persistence, RAG), eight enterprise patterns that collapse into one DSL line each, and a project-to-platform timeline I've watched live more than once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Coming in cold?&lt;/strong&gt; Earlier in this series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/"&gt;redb.Route 3.1.0 — LLM as just another connector&lt;/a&gt; (announcement)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redbroute-apache-camel-for-net-22-transports-30-eip-patterns-compiled-dsl-11m0"&gt;redb.Route — Apache Camel for .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/enterprise-integration-patterns-in-net-the-deep-dive-series-part-1-the-four-in-memory-channels-3e24"&gt;Enterprise Integration Patterns in .NET, part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A quick word on EIP.&lt;/strong&gt; Throughout this post I lean on Enterprise Integration Patterns — &lt;code&gt;Multicast&lt;/code&gt;, &lt;code&gt;Aggregator&lt;/code&gt;, &lt;code&gt;Scatter-Gather&lt;/code&gt;, &lt;code&gt;Wire-Tap&lt;/code&gt;, &lt;code&gt;Choice&lt;/code&gt;, &lt;code&gt;Aggregate-by-window&lt;/code&gt; — without unpacking each of them in detail. That's deliberate: the "EIP in .NET via redb.Route" series with per-pattern deep-dives (diagrams, code, side-by-sides) hasn't shipped yet, I'm writing those in parallel. So I'm getting a little ahead of myself here.&lt;/p&gt;

&lt;p&gt;If you've worked with &lt;strong&gt;Apache Camel&lt;/strong&gt; or &lt;strong&gt;WSO2 Micro Integrator&lt;/strong&gt;, you'll spot the patterns on the fly — the names and semantics are identical. I've personally shipped and maintained integration projects on WSO2 MI for years (and on its ESB ancestor), but architecturally &lt;code&gt;redb.Route&lt;/code&gt; sits closer to Camel — a compiled DSL over a typed Exchange, not an XML config. Side-by-side comparisons "Camel ↔ redb.Route" and "WSO2 MI ↔ redb.Route" are their own posts, also in flight. If anything EIP-flavoured in the code below trips you up, drop a question in the comments — I'll answer on the spot, and the answer goes straight into those upcoming pieces.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why this isn't "yet another agent framework"
&lt;/h2&gt;

&lt;p&gt;The agent-framework shelf is crowded. LangChain, LangChain4j, Semantic Kernel, AutoGen, LlamaIndex — each one solves the same single problem: &lt;em&gt;how does my model call functions and remember a conversation&lt;/em&gt;. Each one leaves you to solve the &lt;strong&gt;other eighteen problems&lt;/strong&gt; yourself: retry, idempotency, audit, multi-tenant, budget, observability, governance, approval, batch, scheduling, replay, dead-letter, throttling, circuit-breaking, correlation, timeouts, cost attribution, metric exporters.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;redb.Route.Llm&lt;/code&gt;'s entire reason for existing is to make those eighteen problems be already solved at the framework layer. Not "we have an integration library, plus an LLM library, glue them yourself" — same DSL, same runtime, same governance hooks. If you already have a &lt;code&gt;redb.Route&lt;/code&gt; policy interceptor that writes every message to Kafka, it automatically writes every tool call from your agent to Kafka too — because they're the same Exchange.&lt;/p&gt;

&lt;p&gt;This isn't theoretical elegance. It cuts through the single biggest production headache with agents: &lt;strong&gt;what happens the day your model decides to drop a production table?&lt;/strong&gt; In the LangChain-plus-hooks shape, the answer is "wire up our approval callbacks and don't forget". In the LLM-as-transport shape, the answer is "intercept the Exchange before &lt;code&gt;.To("exec://...")&lt;/code&gt; with the same &lt;code&gt;.Process(...)&lt;/code&gt; you use for any other potentially-destructive step in any other route". Not a new pattern for AI. The same pattern as for FTP, Kafka, JMS.&lt;/p&gt;

&lt;p&gt;Same people, same tools, same review gates. That's the actual win.&lt;/p&gt;




&lt;h2&gt;
  
  
  What got shipped from the skip-list
&lt;/h2&gt;

&lt;p&gt;I said I would, and I'm saying I did.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skip-list item&lt;/th&gt;
&lt;th&gt;Status in 3.1.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Streaming end-to-end (HTTP SSE + WS per-frame)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Shipped.&lt;/strong&gt; &lt;code&gt;IAsyncEnumerable&amp;lt;string&amp;gt;&lt;/code&gt; lives in &lt;code&gt;Out.Body&lt;/code&gt;, the HTTP consumer flips into SSE, the WS consumer dispatches one frame per message.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ToolCacheStore on REDB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Shipped.&lt;/strong&gt; &lt;code&gt;ToolCacheProps&lt;/code&gt;, opt-in via &lt;code&gt;ToolCachingPolicy.Memoize&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KnowledgeStore — RAG chunks in REDB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Shipped (partial).&lt;/strong&gt; &lt;code&gt;KnowledgeChunkProps&lt;/code&gt; with metadata + tenant + ACL; embeddings land in a follow-up release.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BatchStore + LlmCallbackProcessor&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Shipped.&lt;/strong&gt; Anthropic Message Batches and OpenAI Batch, async webhook consumer, idempotent dispatch, retry-aware.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EvalRunStore&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Shipped.&lt;/strong&gt; Eval runs are first-class objects with trace ids and prompt-version pinning.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PromptTemplateStore&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Shipped.&lt;/strong&gt; Versioned prompt registry, referenced from DSL with &lt;code&gt;#name&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sliding-window memory&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not shipped.&lt;/strong&gt; Different bet placed instead: tree-branching conversations via REDB-tree (more on this below).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandboxed tools (per-call container)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Not shipped&lt;/strong&gt; in container form. Shipped &lt;code&gt;redb.Route.Exec&lt;/code&gt; with allowlist + working-dir + timeout + byte-cap — the practical enterprise minimum.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bonus, not in the skip-list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-exchange &lt;code&gt;?redb=&amp;lt;name&amp;gt;&lt;/code&gt; hint&lt;/strong&gt; — pick a named REDB instance per Exchange, so one route serves N tenants without N route registrations or N &lt;code&gt;IServiceScope&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DSL/tool package split&lt;/strong&gt; — &lt;code&gt;redb.Route.Llm.Abstractions&lt;/code&gt; (contracts only) and &lt;code&gt;redb.Route.Llm.Tools&lt;/code&gt; (six utility tools), so 22 connectors can register &lt;code&gt;.AsLlmTool()&lt;/code&gt; without bumping a minor on every change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six utility tools out of the box&lt;/strong&gt;: &lt;code&gt;HttpFetch&lt;/code&gt;, &lt;code&gt;JsonPath&lt;/code&gt;, &lt;code&gt;XPath&lt;/code&gt;, &lt;code&gt;RegexExtract&lt;/code&gt;, &lt;code&gt;MathEval&lt;/code&gt;, &lt;code&gt;TavilyWebSearch&lt;/code&gt; — each as both a DSL extension and a standalone tool route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug fixes that only show up in real traffic&lt;/strong&gt;: orphan &lt;code&gt;tool_use&lt;/code&gt; (model asks for a tool, provider 5xxs mid-turn, leaves the conversation in an invalid state) and OEM codepage in &lt;code&gt;Process.StandardOutput&lt;/code&gt; (Windows &lt;code&gt;cmd /c&lt;/code&gt; returns cp866 bytes, breaks the UTF-8 contract every tool implicitly assumes).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eleven REDB schemas now sit behind all of this: &lt;code&gt;ConversationProps&lt;/code&gt;, &lt;code&gt;MessageProps&lt;/code&gt;, &lt;code&gt;ApprovalProps&lt;/code&gt;, &lt;code&gt;CostBudgetProps&lt;/code&gt;, &lt;code&gt;ToolCacheProps&lt;/code&gt;, &lt;code&gt;ToolAuditProps&lt;/code&gt;, &lt;code&gt;KnowledgeChunkProps&lt;/code&gt;, &lt;code&gt;PromptTemplateProps&lt;/code&gt;, &lt;code&gt;EvalRunProps&lt;/code&gt;, &lt;code&gt;LlmBatchProps&lt;/code&gt;, &lt;code&gt;ToolIdempotencyProps&lt;/code&gt;. That's not a database for your chat history. That's the &lt;strong&gt;operational layer of an agent platform&lt;/strong&gt;, opt-in via one line: &lt;code&gt;AddRedbLlmStorage()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ten enterprise patterns — each one a single DSL line
&lt;/h2&gt;

&lt;p&gt;Here's where it gets useful. None of these are pseudocode. They all run today.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Hard budget per conversation as a circuit breaker
&lt;/h3&gt;

&lt;p&gt;In a side project, a "token budget" is a log line. In production, it's a &lt;strong&gt;safety primitive&lt;/strong&gt; — your model must not be allowed to burn $10k because of a bad prompt. &lt;code&gt;CostBudgetProps&lt;/code&gt; isn't an observability artifact; it's a &lt;strong&gt;preventive rule&lt;/strong&gt;. Every provider call, the agent engine sums spend by conversation id and adds the worst-case for the pending request (via &lt;code&gt;max_tokens&lt;/code&gt;). If the total would breach the cap, &lt;code&gt;LlmBudgetExceededException&lt;/code&gt; fires &lt;strong&gt;before the network call&lt;/strong&gt;, not after.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://support-tickets"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Conversation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"ticket-"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"TicketId"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// hard ceiling per conversation id&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudgetExceeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BudgetPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FailFast&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://support-replies"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same primitive scales to "budget per tenant", "budget per prompt template", "budget per model" — they're all just different keys in &lt;code&gt;CostBudgetProps&lt;/code&gt;. Same shape, different &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Approval gates with a human in the loop
&lt;/h3&gt;

&lt;p&gt;The most under-appreciated production pattern in agent-land. If your model can refund payments or delete records, &lt;strong&gt;a human stands between the tool call and the actual side effect&lt;/strong&gt;. Not a webhook duct-taped on later — a native runtime concept.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:tool-payments-refund"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLlmTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"issue_refund"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Refund a payment by id and reason."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* JSON Schema */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SideEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolSideEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutating&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// governance hook reads this&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolCostClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expensive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApprovalGate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                        &lt;span class="c1"&gt;// suspend exchange,&lt;/span&gt;
                                                     &lt;span class="c1"&gt;// write ApprovalProps,&lt;/span&gt;
                                                     &lt;span class="c1"&gt;// notify Slack,&lt;/span&gt;
                                                     &lt;span class="c1"&gt;// wait for HTTP callback&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://payments.refund.commands"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ApprovalProps&lt;/code&gt; records the exchange id, conversation id, input arguments, the human approver, the wait deadline, and the outcome. The Slack bot, the email handler, the web form — all of those plug in as ordinary HTTP routes in the same &lt;code&gt;redb.Route&lt;/code&gt;. When the human clicks &lt;strong&gt;Approve&lt;/strong&gt;, the webhook wakes the suspended Exchange and feeds the result back to the agent engine. On timeout, the agent receives a &lt;code&gt;tool_result&lt;/code&gt; with &lt;code&gt;status:"timeout"&lt;/code&gt; and decides what to do next.&lt;/p&gt;

&lt;p&gt;The point: &lt;strong&gt;this is not AI logic.&lt;/strong&gt; It's the EIP &lt;code&gt;Aggregator + Correlation Identifier + Wire-Tap + Reply-To&lt;/code&gt; pattern, which already applies to a refund engine without an LLM. They're both Exchanges in the same runtime, so the pattern is reused.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Idempotent tool retries via &lt;code&gt;ToolIdempotencyProps&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Webhook consumers retry. Network timeouts retry. Anthropic's batch sometimes delivers a payload twice. If your tool call "issue refund $50" runs twice, that's a bad day at standup.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ToolIdempotencyProps&lt;/code&gt; keeps &lt;code&gt;idempotency-key → tool-result&lt;/code&gt; with a TTL. In the DSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:tool-issue-invoice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLlmTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"issue_invoice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Caching&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolCachingPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Idempotent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// sha256(args + customer-id + day)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the agent engine sees &lt;code&gt;Caching = Idempotent&lt;/code&gt;, it consults &lt;code&gt;ToolIdempotencyProps&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the route runs. Hit → returns the saved &lt;code&gt;tool_result&lt;/code&gt;, route never executes. Miss → route runs, result is recorded. Framework-level, not "remember to wrap your handler".&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Multi-tenant via &lt;code&gt;?redb=&amp;lt;name&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;One worker, fifty customers. Each customer is &lt;strong&gt;their own REDB instance&lt;/strong&gt; — their conversations, their cost budgets, their approvals, their knowledge base. In 3.1.0 this would have meant fifty &lt;code&gt;IServiceScope&lt;/code&gt;s or fifty route registrations. In 3.1.1 it's a &lt;strong&gt;per-exchange hint&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/llm/ask?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LlmHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Redb&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"X-Tenant"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llm://claude?conversationFromHeader=true"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;?redb=acme&lt;/code&gt; or a header — the engine pulls the named REDB instance from the registry, &lt;strong&gt;on the current Exchange&lt;/strong&gt;, no factory, no route swap, no restart. Conversations, audit, approvals all land in the right tenant. Each tenant gets its own billing surface and governance lane, with no awareness of the others.&lt;/p&gt;

&lt;p&gt;This was the single feature most worth the late nights. It's the difference between "we have a multi-tenant agent platform" and "we have a multi-tenant deployment story, here's a wiki page".&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Audit trail without a separate integration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ToolAuditProps&lt;/code&gt; is a REDB object: tenant id, conversation id, exchange id, tool name, input args (or hash, if PII), output (or hash), duration, status. Every tool invocation is recorded automatically — because &lt;strong&gt;tools are routes&lt;/strong&gt;, and &lt;code&gt;redb.Route&lt;/code&gt; already has post-processors.&lt;/p&gt;

&lt;p&gt;The query "show me every Claude tool call in tenant &lt;code&gt;acme&lt;/code&gt; over the last 7 days, side-effect=mutating, cost=expensive, ordered by time" isn't a separate analytics pipeline. It's a &lt;strong&gt;&lt;code&gt;value_string&lt;/code&gt; index plus one SQL&lt;/strong&gt; — the same indexed-business-id pattern the rest of the REDB ecosystem uses.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Async batch + callback consumer
&lt;/h3&gt;

&lt;p&gt;Anthropic Message Batches and OpenAI Batch are &lt;strong&gt;up to 50% cheaper&lt;/strong&gt;, with a price tag of up to 24 hours of latency. For offline workloads — classifying a million tickets, extracting fields from a million PDFs — that's the right knob to turn.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LlmBatchProps&lt;/code&gt; stores the batch id, statuses, and the link back to the originating Exchange collection. &lt;code&gt;LlmCallbackProcessor&lt;/code&gt; is &lt;strong&gt;just an &lt;code&gt;HTTP&lt;/code&gt; route&lt;/strong&gt; that the provider calls when the batch finishes. The route reads the batch id, fetches the results, dispatches each one back to its originating Exchange via correlation id — and those Exchanges resume their journey as if the synchronous call had just finished, twenty-four hours later.&lt;/p&gt;

&lt;p&gt;On top of this: idempotency (same &lt;code&gt;ToolIdempotencyProps&lt;/code&gt;), retry (the standard &lt;code&gt;redb.Route&lt;/code&gt; circuit breaker), backpressure (Kafka or queue downstream). No batch dispatcher to write. No webhook handler to write. No "what if the callback arrives twice" to test separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Versioned prompt registry: &lt;code&gt;#&lt;/code&gt;-refs
&lt;/h3&gt;

&lt;p&gt;The single most depressing prod bug pattern: "the model is answering differently". You go check git blame, somebody touched the system prompt three weeks ago, the tests passed because the eval set is small. The prompt is &lt;strong&gt;code&lt;/strong&gt;, and it deserves a registry with versions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// where prompts get registered:&lt;/span&gt;
&lt;span class="n"&gt;promptRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"triage-system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"v3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"""
&lt;/span&gt;    &lt;span class="n"&gt;You&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="n"&gt;triage&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Classify&lt;/span&gt; &lt;span class="k"&gt;into&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tech&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abuse&lt;/span&gt;&lt;span class="p"&gt;]...&lt;/span&gt;
    &lt;span class="s"&gt;""");
&lt;/span&gt;
&lt;span class="c1"&gt;// in the route:&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#triage-system@v3"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PromptTemplateProps&lt;/code&gt; is a REDB object: name, version, body, metadata (author, date, experiment id). When the engine resolves &lt;code&gt;#name&lt;/code&gt;, it pins &lt;strong&gt;exactly that version&lt;/strong&gt; into &lt;code&gt;MessageProps&lt;/code&gt; for the call. Six months later you can say with certainty "this conversation ran on &lt;code&gt;triage-system&lt;/code&gt; v3", not "probably v3, that's what we were doing then".&lt;/p&gt;

&lt;p&gt;The juicy part: &lt;code&gt;EvalRunProps&lt;/code&gt; records eval runs &lt;strong&gt;bound to a prompt version&lt;/strong&gt;. "v4 gives +12% accuracy on the golden set" stops being a spreadsheet and becomes a query.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Tree-branching conversations for A/B and counterfactuals
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ConversationProps&lt;/code&gt; is stored as a REDB tree via the native &lt;code&gt;parent_id&lt;/code&gt;. That means a conversation isn't a flat list of messages — it's a &lt;strong&gt;tree&lt;/strong&gt;. So you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;branch from any message and run an alternative continuation with a different model or temperature;&lt;/li&gt;
&lt;li&gt;keep a user branch and an experiment branch in parallel;&lt;/li&gt;
&lt;li&gt;compute metrics across pairs of branches ("with-tools vs without-tools on the exact same context").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sliding-window memory &lt;strong&gt;truncates the past&lt;/strong&gt;. Tree-branching memory &lt;strong&gt;writes alternative pasts and compares them&lt;/strong&gt;. For production agents, the second one is an order of magnitude more useful, because improving prompts in production is exactly that loop: take a real conversation, replay with a new prompt, score it, ship the winner.&lt;/p&gt;

&lt;p&gt;And nothing here was bolted on for LLMs. Tree via &lt;code&gt;parent_id&lt;/code&gt; has been a first-class REDB feature from day one — for product hierarchies, org charts, file systems. The LLM stack just wrote its own entity on the same primitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. A jury of cheap models with a senior model as arbiter — Scatter-Gather + Aggregator
&lt;/h3&gt;

&lt;p&gt;One of the most underrated production tricks in agent-land: &lt;strong&gt;don't trust a single model&lt;/strong&gt;. Send the same task to several cheap models in parallel (Haiku, GPT-4o-mini, Mistral-Small, Gemini-Flash, Llama-3.1-70b on Groq), gather their answers, and hand the original prompt &lt;strong&gt;plus all five candidates&lt;/strong&gt; to a &lt;strong&gt;senior model&lt;/strong&gt; (Sonnet, Opus, GPT-4o) acting as arbiter — pick the best, synthesise a new one, or say "nobody nailed it, ask the human". The literature calls this &lt;strong&gt;mixture-of-agents&lt;/strong&gt; or &lt;strong&gt;ensemble voting&lt;/strong&gt;, and on hard tasks it routinely buys you +10 to +20% accuracy at a lower bill than running everything through Opus.&lt;/p&gt;

&lt;p&gt;In a vanilla LangChain shape, this becomes a hundred-line orchestrator with try/catch, per-model timeouts, retry knobs, and a hand-rolled aggregator. In an ESB shape, it's the &lt;strong&gt;bog-standard EIP &lt;code&gt;Scatter-Gather + Aggregator&lt;/code&gt;&lt;/strong&gt;, with twenty-five years of hardening in integration buses. The LLM is just one more endpoint type the Scatter-Gather fans out to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://contract-clauses-to-classify"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"contract-jury"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Multicast&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                                          &lt;span class="c1"&gt;// Scatter&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopOnException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                           &lt;span class="c1"&gt;// one provider down? carry on&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                &lt;span class="c1"&gt;// per branch&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#contract-classify@v3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4o-mini"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#contract-classify@v3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"groq-llama-70b"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#contract-classify@v3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mistral-small"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#contract-classify@v3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                                                &lt;span class="c1"&gt;// Gather: results land in Exchange.Properties["multicast.results"]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JuryAggregator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                            &lt;span class="c1"&gt;// splice candidates into one arbiter prompt&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sonnet"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                             &lt;span class="c1"&gt;// Arbiter&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#jury-arbiter@v2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://contract-clauses-classified"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JuryAggregator&lt;/code&gt; is a tiny processor: takes the four answers and the original prompt, builds one arbiter message of the shape "here is the task; here are candidates A/B/C/D; return the final classification or say &lt;code&gt;unclear&lt;/code&gt;". The arbiter answers with structured JSON. Headers &lt;code&gt;llm.tokens.in/out&lt;/code&gt; are recorded &lt;strong&gt;per branch&lt;/strong&gt; plus once for the arbiter — cost is transparent at the route level.&lt;/p&gt;

&lt;p&gt;Why ESB-shape makes this so cheap to express:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallelism for free.&lt;/strong&gt; Multicast EIP already knows how to fan out across N branches, await all-or-N-of-M, handle timeouts and partial failures. No &lt;code&gt;Task.WhenAll&lt;/code&gt; with hand-written failure handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency for free.&lt;/strong&gt; A retry on a failed branch hits the same &lt;code&gt;ToolIdempotencyProps&lt;/code&gt; (or provider-side prompt-hash cache) and dedups. No double billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget for free.&lt;/strong&gt; &lt;code&gt;.CostBudget(usd: 0.05)&lt;/code&gt; on the arbiter is its own circuit-breaker; per-branch &lt;code&gt;.CostBudget(usd: 0.01)&lt;/code&gt; caps the cheap models. A combined ceiling is a different &lt;code&gt;CostBudgetProps&lt;/code&gt; key. Breach it and the whole jury fails fast — failover to a rule-based classifier (which sits as another branch on the route).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit for free.&lt;/strong&gt; Each of the five LLM calls is captured in &lt;code&gt;ToolAuditProps&lt;/code&gt;, all stitched to one exchange id. A month later you can answer "model X agrees with the arbiter 73% of the time — let's drop it and save".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eval for free.&lt;/strong&gt; &lt;code&gt;EvalRunProps&lt;/code&gt; records the run with five branches plus the arbiter — replay against a golden set to find the &lt;strong&gt;optimal jury composition&lt;/strong&gt; (swap GPT-4o-mini for Gemini-Flash, see if accuracy holds).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production this pattern wins on two axes. &lt;strong&gt;Accuracy&lt;/strong&gt;: on tasks like "is this contract clause a risk", where a single model wobbles, four cheap models plus a Sonnet arbiter routinely outperforms a lone Opus, &lt;strong&gt;at 2.5× lower cost&lt;/strong&gt;. &lt;strong&gt;Robustness&lt;/strong&gt;: when Anthropic is having a bad afternoon, the Anthropic branch fails, the other four return, the arbiter receives four candidates instead of five and ships a verdict. Graceful degradation, not a hard outage.&lt;/p&gt;

&lt;p&gt;One subtle production gotcha: &lt;strong&gt;never tell the arbiter who wrote which answer&lt;/strong&gt;. If the arbiter's prompt says "candidate from Claude Haiku, candidate from GPT-4o-mini", the arbiter develops favourites (so do humans). So &lt;code&gt;JuryAggregator&lt;/code&gt; anonymises the candidates as &lt;code&gt;A/B/C/D&lt;/code&gt;, shuffles their order (Latin-squared on &lt;code&gt;exchange-id&lt;/code&gt; for reproducibility), and &lt;strong&gt;only after the arbiter responds&lt;/strong&gt; do we map back to "A was the Haiku answer". Clean signal for the post-hoc analysis "which model agrees with the arbiter most often".&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Sub-agents — an agent as another agent's tool
&lt;/h3&gt;

&lt;p&gt;A direct consequence of the "tool = route" architecture: if a route can contain &lt;code&gt;.To("llm://...")&lt;/code&gt;, then &lt;strong&gt;a tool can itself be an agent&lt;/strong&gt;. The parent agent doesn't "know" there's another LLM hiding behind that tool. To it, &lt;code&gt;research_topic&lt;/code&gt; is just another tool, like &lt;code&gt;web_search&lt;/code&gt; or &lt;code&gt;math_eval&lt;/code&gt;. Inside the tool, though, lives a fully-fledged second-tier agent with its own model, prompt, toolset, budget, iteration cap, retry policies, RAG sources.&lt;/p&gt;

&lt;p&gt;In code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sub-agent: a research specialist with its own toolset&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:research-subagent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLlmTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"research_topic"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Deep research on a topic. Takes {topic, depth}. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                     &lt;span class="s"&gt;"Returns a structured summary with sources."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""{"&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="s"&gt;":"&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="s"&gt;":{
&lt;/span&gt;                    &lt;span class="s"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="s"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s"&gt;"short"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"deep"&lt;/span&gt;&lt;span class="p"&gt;]}},&lt;/span&gt;
                  &lt;span class="s"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s"&gt;""")
&lt;/span&gt;        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SideEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolSideEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolCostClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expensive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;// parent sees the call is pricey&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Knowledge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"research-corpus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// sub-agent has its own RAG corpus&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sonnet"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;// mid-tier model — research specialist&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#research-specialist@v3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tavily_web_search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"http_fetch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"regex_extract"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxIterations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;// sub-agent has its own budget&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExtractResearchSummary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Parent agent uses the sub-agent as just another tool&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://complex-business-questions"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"opus"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="c1"&gt;// planner — top-tier model&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#senior-analyst@v1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"research_topic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// ← our sub-agent&lt;/span&gt;
               &lt;span class="s"&gt;"sql_query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;// ← plain data tool&lt;/span&gt;
               &lt;span class="s"&gt;"math_eval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;// ← computation&lt;/span&gt;
               &lt;span class="s"&gt;"draft_report"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;// ← another sub-agent (report drafter)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxIterations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.00&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;// top-level budget&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://business-answers"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What actually happens under the hood: when Opus decides to call &lt;code&gt;research_topic&lt;/code&gt;, the agent engine builds the JSON input and routes it through &lt;code&gt;RouteToolBridge&lt;/code&gt; to &lt;code&gt;direct:research-subagent&lt;/code&gt;. That route runs as a &lt;strong&gt;fresh Exchange&lt;/strong&gt;, inheriting transaction scope, principal, headers, and DI scope from the parent. Inside it, Sonnet runs its own tool-use loop (web search → fetch → extract), potentially with eight iterations and three tools. It returns a structured summary in &lt;code&gt;Out.Body&lt;/code&gt;, which the engine repackages as a &lt;code&gt;tool_result&lt;/code&gt; and hands back to Opus. Opus sees the result, keeps planning, calls more tools as needed.&lt;/p&gt;

&lt;p&gt;In other words: &lt;strong&gt;the architecture is recursive without a single line of dedicated code&lt;/strong&gt;. A sub-agent inside a sub-agent works the same way. At depth three, audit, budget, idempotency, prompt versioning, RAG, governance — all of it still works, because none of those are tied to "agent level"; they're tied to the Exchange, and the Exchange is the same primitive at any depth.&lt;/p&gt;

&lt;p&gt;This unlocks &lt;strong&gt;three derivative patterns&lt;/strong&gt; that are awkward to assemble in the flat "one agent, many tools" shape:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(a) Hierarchical agents — planner and workers.&lt;/strong&gt; A top-tier model (Opus, GPT-4o) plays planner: decomposes the task and dispatches chunks to specialists. Each specialist is a sub-agent with a narrow prompt and a narrow toolset. The planner may have &lt;strong&gt;no direct data access at all&lt;/strong&gt; — only through sub-agents. That gives you &lt;strong&gt;hard separation of authority&lt;/strong&gt;: the planner can't accidentally call &lt;code&gt;delete_records&lt;/code&gt; because that tool isn't in its set; only &lt;code&gt;data-cleanup-subagent&lt;/code&gt; has it, and the planner has to hand off explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(b) Specialist sub-agents with their own memory.&lt;/strong&gt; A sub-agent can have its own &lt;code&gt;conversationId&lt;/code&gt; (a different branch of the REDB tree), its own &lt;code&gt;Knowledge(...)&lt;/code&gt; sources, its own prompt registry. A &lt;code&gt;legal-review-subagent&lt;/code&gt; lives inside the corporate legal corpus, sees &lt;strong&gt;only&lt;/strong&gt; that, answers strictly in the legal register. The senior agent never gets direct access to those documents. ACL is enforced at the route level, not at the "we hope the model doesn't quote it" level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(c) Cost-shaped escalation.&lt;/strong&gt; A cheap agent (Haiku) takes the first swing. If it returns &lt;code&gt;unclear&lt;/code&gt; or confidence is below threshold, &lt;strong&gt;it calls&lt;/strong&gt; &lt;code&gt;escalate_to_senior&lt;/code&gt; itself as a tool — and behind that tool sits a route to Opus with full context. Most queries land on Haiku for pennies; only the hard ones reach Opus. On high-volume workloads the economics change by an order of magnitude.&lt;/p&gt;

&lt;p&gt;How this relates to the static jury in pattern #9: &lt;strong&gt;the jury is a statically-wired sub-agent pattern&lt;/strong&gt;. The route knows up front there are N candidates and one arbiter; the DAG is hard-coded in the DSL. &lt;strong&gt;Sub-agents-as-tools is the dynamic version&lt;/strong&gt;: the parent agent &lt;strong&gt;decides for itself&lt;/strong&gt; whom to call and how often. Jury wins on predictable classification/forecasting pipelines. Sub-agents win on open-ended research where the number of steps and the toolset can't be known in advance.&lt;/p&gt;

&lt;p&gt;The obvious failure mode is cycles. If sub-agent A can call B and B can call A, you've got an unbounded recursion in theory. In practice three guards keep it bounded: &lt;code&gt;MaxIterations&lt;/code&gt; at every level (no sub-agent loops forever), &lt;code&gt;CostBudgetProps&lt;/code&gt; (each nested call burns parent budget), and an optional depth limit in headers (&lt;code&gt;LlmHeaders.SubAgentDepth&lt;/code&gt; increments per level; the route rejects calls past a configured limit). Real tasks rarely benefit beyond &lt;code&gt;depth = 3-4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And one more piece of architectural elegance: &lt;strong&gt;a sub-agent is just a route&lt;/strong&gt;, which means it has a stock URI. Different parent agents can share one &lt;code&gt;research-subagent&lt;/code&gt;, which has its own &lt;code&gt;?redb=acme&lt;/code&gt; for tenant isolation, its own rate limiter (&lt;code&gt;?throttle=5/sec&lt;/code&gt;), its own circuit breaker. The sub-agent behaves like an &lt;strong&gt;internal LLM-tier microservice&lt;/strong&gt; that you reuse across routes without duplicating prompts or proliferating clients.&lt;/p&gt;




&lt;h2&gt;
  
  
  Streaming: what actually changes when a token leaves the provider
&lt;/h2&gt;

&lt;p&gt;In 3.1.0, providers streamed but the &lt;strong&gt;client never saw it&lt;/strong&gt; — we accumulated tokens into a string and returned the whole thing. In 3.1.1, the chain closes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the provider yields &lt;code&gt;IAsyncEnumerable&amp;lt;string&amp;gt;&lt;/code&gt; — frames as they arrive;&lt;/li&gt;
&lt;li&gt;the agent engine sticks that into &lt;code&gt;Out.Body&lt;/code&gt; as &lt;code&gt;IAsyncEnumerable&amp;lt;string&amp;gt;&lt;/code&gt;, &lt;strong&gt;without materialising&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;the HTTP consumer sees &lt;code&gt;IAsyncEnumerable&amp;lt;string&amp;gt;&lt;/code&gt; and switches to SSE — one &lt;code&gt;data: ...\n\n&lt;/code&gt; per frame;&lt;/li&gt;
&lt;li&gt;the WS consumer sends one WebSocket message per frame;&lt;/li&gt;
&lt;li&gt;non-streaming consumers (Kafka, RabbitMQ, ActiveMQ) materialise the way they used to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architectural payoff: streaming stops being a separate &lt;strong&gt;mode&lt;/strong&gt; and becomes a &lt;strong&gt;payload type&lt;/strong&gt;. Same Exchange can fan out to SSE &lt;em&gt;and&lt;/em&gt; Kafka simultaneously (multicast EIP) — Kafka waits for materialisation, SSE sees frames live. No "streaming endpoint" vs "regular endpoint" duplication.&lt;/p&gt;

&lt;p&gt;Underneath, this is the well-known pipe pattern of "iterator instead of collection". The only LLM-specific bit is making sure async iteration &lt;strong&gt;runs inside the end-to-end Exchange tracking&lt;/strong&gt; so traces and metrics see the whole journey, not "received an Exchange and lost the rest".&lt;/p&gt;




&lt;h2&gt;
  
  
  RAG: chunks as first-class REDB objects
&lt;/h2&gt;

&lt;p&gt;In 3.1.1, &lt;code&gt;KnowledgeChunkProps&lt;/code&gt; is a REDB object with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the text;&lt;/li&gt;
&lt;li&gt;the source (&lt;code&gt;source-uri&lt;/code&gt;, &lt;code&gt;tenant-id&lt;/code&gt;, &lt;code&gt;doc-id&lt;/code&gt;, &lt;code&gt;chunk-index&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;metadata (language, date, tags, ACL — who's allowed to see it);&lt;/li&gt;
&lt;li&gt;a placeholder for embeddings (vector store ships in a follow-up; the MVP is keyword search via &lt;code&gt;value_string&lt;/code&gt; indexes plus FTS).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this means: &lt;strong&gt;a RAG source is a route&lt;/strong&gt;, not a separate vector service. &lt;code&gt;From("file://docs?include=*.md").To("knowledge://acme")&lt;/code&gt; indexes documents. &lt;code&gt;From("kafka://support-tickets").Knowledge("acme", k: 5)&lt;/code&gt; injects the top-5 chunks into the system prompt before &lt;code&gt;.To("llm://claude")&lt;/code&gt;. ACL and tenant filtering happen as &lt;code&gt;value_*&lt;/code&gt;-indexed SQL &lt;strong&gt;before&lt;/strong&gt; chunks reach the prompt.&lt;/p&gt;

&lt;p&gt;When the vector store lands, it sits next to keyword search behind the same &lt;code&gt;IKnowledgeStore&lt;/code&gt; interface, no route changes. That's the &lt;strong&gt;architectural goal&lt;/strong&gt;: tomorrow's features don't break today's routes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three real-world enterprise scenarios — reports, forecasts, alerts
&lt;/h2&gt;

&lt;p&gt;The patterns above are atomic bricks. Now three end-to-end scenarios where those bricks combine into routes you'd actually run on Tuesday morning. No AI hype, no "cognitive automation", no pretending to "transform the industry". Just &lt;strong&gt;the dull stuff a human currently does by hand every day, taken over by an agent that lives in the same bus as the data&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 1 — A daily financial snapshot for the CFO, in their inbox by 7:00 AM
&lt;/h3&gt;

&lt;p&gt;Every business morning a finance analyst pulls together yesterday's revenue, expenses by category, plan-vs-actual variances, top-5 largest transactions, account balances, FX rates. Then they write a paragraph or two — "revenue +3.2% to plan, expenses +1.7%, the variance is X". An hour and a half, gone. Worth automating? Obvious yes. Worth standing up a separate AI service for it? Hard no.&lt;/p&gt;

&lt;p&gt;The whole route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every weekday at 07:00 local&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cron://daily-cfo-report?schedule=0 0 7 ? * MON-FRI"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"daily-cfo-report"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoadYesterdayMetrics&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                       &lt;span class="c1"&gt;// pulls from ERP/bank API/data warehouse&lt;/span&gt;
                                                            &lt;span class="c1"&gt;// → e.In.Body = {revenue, expenses, accounts, fx, top5}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoadQuarterContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                         &lt;span class="c1"&gt;// same ETL: plan, prior quarter, MTD/QTD&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FinancialDailySnapshot&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                 &lt;span class="c1"&gt;// strongly-typed payload&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Multicast&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                           &lt;span class="c1"&gt;// branch A: short-form for the CFO&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#cfo-daily-summary@v7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;800&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                           &lt;span class="c1"&gt;// branch B: bulletised for the board chat&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#cfo-daily-bullets@v7"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RenderHtmlReport&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                           &lt;span class="c1"&gt;// mustache template: numbers as a table,&lt;/span&gt;
                                                            &lt;span class="c1"&gt;// model summaries as the lede&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"smtp://mail.acme.com?to=cfo@acme.com,board@acme.com"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
        &lt;span class="s"&gt;"&amp;amp;subject=Daily%20FY%20snapshot%20${date:yyyy-MM-dd}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"teams://board-channel?card=adaptive"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// same HTML body → adaptive card in Teams&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wiretap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://reports.cfo-daily.archive"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// copy to archive for audit and training&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven steps, and those seven steps cover the whole job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schedule&lt;/strong&gt; — &lt;code&gt;cron&lt;/code&gt; lives in the URI; no separate scheduler service. &lt;code&gt;redb.Route&lt;/code&gt; already does this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data pickup&lt;/strong&gt; — &lt;code&gt;LoadYesterdayMetrics&lt;/code&gt; is &lt;strong&gt;just a processor&lt;/strong&gt; that hits your existing integrations through the same &lt;code&gt;redb.Route&lt;/code&gt; (Kafka, REST, JDBC). Whatever retries and circuit breakers you already wired around those integrations apply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-tone output&lt;/strong&gt; — multicast to two branches of cheap Haiku with different prompt templates. Pennies, hard-budgeted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned prompt&lt;/strong&gt; — &lt;code&gt;#cfo-daily-summary@v7&lt;/code&gt;. Six months from now the CFO complains "the report has gotten worse" — open &lt;code&gt;EvalRunProps&lt;/code&gt;, see v7 regressed against the golden set, roll back to v6 with no redeploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery&lt;/strong&gt; — two transports (SMTP + Teams), both stock &lt;code&gt;redb.Route&lt;/code&gt; connectors. Tomorrow they want it in Slack too: &lt;code&gt;.To("slack://...")&lt;/code&gt;, one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit&lt;/strong&gt; — &lt;code&gt;Wiretap&lt;/code&gt; copies the whole exchange to a Kafka archive. Twelve months later a regulator asks "what did we send on 2026-04-15?" — pulled from the archive with metadata that says "model X, prompt version Y, source data Z" (because &lt;code&gt;MessageProps&lt;/code&gt; records both input and output).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget&lt;/strong&gt; — &lt;code&gt;.CostBudget(usd: 0.02)&lt;/code&gt; per branch. 250 trading days × 2 branches × $0.02 ≈ $10/year on token spend. The analyst's hour a day at fully-loaded cost is $50. Pays itself back in week one of month one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What pushes this from "cute demo" to "actual enterprise asset": when the CFO asks about "that strange number in last Tuesday's report", the auditor pulls one trace id and in &lt;strong&gt;30 seconds&lt;/strong&gt; has: the snapshot at input, the prompt version that ran, the exact output the model produced, the file that hit SMTP, who opened it in Teams. Not "let me get back to you tomorrow". Right now — because it's all REDB objects pinned to one exchange id.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 2 — A weekly cash-flow forecast with a jury and an Opus arbiter
&lt;/h3&gt;

&lt;p&gt;Cash-flow forecasting is the textbook case where &lt;strong&gt;one model is worse than zero models&lt;/strong&gt;: a confident-sounding model error leads to a decision that costs more than the analyst's salary for the month. Pattern #9 (jury + arbiter) earns its keep here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cron://weekly-cashflow-forecast?schedule=0 0 9 ? * MON"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"weekly-cashflow-forecast"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BuildCashflowFeatures&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                      &lt;span class="c1"&gt;// bank balances, AR/AP, scheduled payments,&lt;/span&gt;
                                                            &lt;span class="c1"&gt;// seasonality, FX exposure&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Knowledge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"acme-finance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;// RAG: forecasting playbook, prior reports,&lt;/span&gt;
                                                            &lt;span class="c1"&gt;// methodology, known seasonality notes&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Multicast&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sonnet"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                          &lt;span class="c1"&gt;// four different models — cheap insurance&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#cashflow-forecast@v4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// against correlated errors&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4o"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#cashflow-forecast@v4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gemini-pro"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#cashflow-forecast@v4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mistral-large"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#cashflow-forecast@v4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JuryAggregator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                             &lt;span class="c1"&gt;// anonymise A/B/C/D + shuffle&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"opus"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                &lt;span class="c1"&gt;// arbiter — strongest model in the rotation&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#cashflow-arbiter@v2"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExtractStructuredForecast&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                  &lt;span class="c1"&gt;// parse JSON: 30 days × {low, mid, high}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Forecast&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;ConfidenceLow&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ApprovalGate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                       &lt;span class="c1"&gt;// low confidence → wait for CFO sign-off&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"smtp://...?to=cfo@acme.com&amp;amp;priority=high"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RenderForecastReport&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"smtp://...?to=treasury@acme.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wiretap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://reports.cashflow.archive"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This route runs about $2-3 per execution, once a week — call it $150/year. The analyst would spend a day producing the same artefact. The savings aren't really the point. The point is &lt;strong&gt;robustness&lt;/strong&gt;: four independent forecasts from four different vendors (Anthropic, OpenAI, Google, Mistral) give you &lt;strong&gt;consensus as a confidence signal&lt;/strong&gt;. Four models agree → the arbiter just codifies. Four models diverge → the arbiter writes "model A sees risk X, the rest don't, recommend human review", and the &lt;code&gt;Choice&lt;/code&gt; branch routes the report to an approval gate without anyone wiring anything custom.&lt;/p&gt;

&lt;p&gt;A year in, the treasury team queries &lt;code&gt;EvalRunProps&lt;/code&gt;: "Gemini-Pro disagrees with consensus 18% of the time, and in 70% of those cases Gemini was right". Excellent — it gets &lt;strong&gt;upweighted&lt;/strong&gt; in the jury, not dropped. That's not a guess; it's a SQL query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 3 — A predictive plant alert: XGBoost does the math, the LLM does the sentence
&lt;/h3&gt;

&lt;p&gt;In manufacturing — the real kind, with conveyor belts and SCADA tags, not the metaphorical kind — the bread-and-butter task is "from the metrics, predict that this line will throw a fault in 4-6 hours, and ping the on-shift engineer &lt;strong&gt;before&lt;/strong&gt; it does". The classical play here is a feature store and a gradient-boosted model. That play is still correct, and the LLM is &lt;strong&gt;not&lt;/strong&gt; there to replace it. What the LLM does well is the &lt;strong&gt;last-mile translation&lt;/strong&gt; from "anomaly score + top features + line context" into a sentence a human reads at 3 AM and understands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Tap the SCADA bus (MQTT/Kafka), 5-minute aggregation window&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://scada.metrics?groupId=plant-anomaly-watcher"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plant-anomaly-watcher"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"LineId"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
               &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;            &lt;span class="c1"&gt;// window per production line&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RunAnomalyModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;                            &lt;span class="c1"&gt;// classical ML — XGBoost, nothing fancy;&lt;/span&gt;
                                                            &lt;span class="c1"&gt;// emits {score, top-features, line-context}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AnomalyReport&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;Score&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Knowledge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plant-runbooks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// RAG: this line's runbooks,&lt;/span&gt;
                                                            &lt;span class="c1"&gt;// history of similar incidents&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SystemPromptRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#plant-incident-explainer@v9"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"metrics-history"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fetch-shift-log"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// agent fetches more if it needs to&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxIterations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EnrichWithOnDutyEngineer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;           &lt;span class="c1"&gt;// who's on shift on this line, right now&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Multicast&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"teams://engineering-shift-{LineId}?card=adaptive"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sms://twilio?to={engineer.phone}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://incidents.predicted.archive"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://scada.metrics.normal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// archive the boring days too&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's actually happening, and why this works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The ML model isn't getting kicked out.&lt;/strong&gt; Anomaly scoring is still XGBoost, which has been good at exactly this for a decade. The LLM stands &lt;strong&gt;after&lt;/strong&gt; it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LLM explains, not predicts.&lt;/strong&gt; The system prompt is "anomaly on Line 7, top-3 features look like X, runbook says Y, prior incidents Z. In one page: what likely happened, what the engineer should do in the next hour, and the three parameters to check first". This is synthesis — the place LLMs are objectively strong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent's tools are the plant's data.&lt;/strong&gt; &lt;code&gt;metrics-history&lt;/code&gt; is a &lt;code&gt;direct:metrics-history&lt;/code&gt; route that queries Influx/Prometheus. &lt;code&gt;fetch-shift-log&lt;/code&gt; reads the shift log from REDB. The agent decides whether to invoke them, how many times. &lt;code&gt;MaxIterations(4)&lt;/code&gt; caps it from looping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-channel delivery.&lt;/strong&gt; Teams adaptive card with an "I've got it" button (which, by the way, fires an &lt;code&gt;ApprovalGate&lt;/code&gt;-style webhook), SMS to the on-shift number via Twilio (already a &lt;code&gt;redb.Route&lt;/code&gt; transport), copy to a Kafka archive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Aggregate by LineId, window 5min&lt;/code&gt;&lt;/strong&gt; is the EIP &lt;code&gt;Aggregator&lt;/code&gt; — bucket metrics by group. Nothing &lt;code&gt;redb.Route.Llm&lt;/code&gt;-specific; this is plain integration code that's been working in your bus for years.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The business outcome: time from "anomaly appears" to "the engineer on the line knows what to look for" drops from ~40 minutes (operator notices, calls, describes, engineer arrives, engineer starts hunting) to ~4-5 minutes. On expensive lines, an hour of unplanned downtime is tens of thousands of dollars; this route pays for itself the &lt;strong&gt;first&lt;/strong&gt; time a near-miss is caught.&lt;/p&gt;

&lt;p&gt;Notice the LLM's role is &lt;strong&gt;narrow&lt;/strong&gt; in all three cases. Not "AI runs the factory". Not "AI runs the books". The LLM does &lt;strong&gt;one specific job&lt;/strong&gt; in each route — summarise, synthesise, pick from candidates, explain. Around it sits all the boring enterprise plumbing: schedules, ETL, RAG, multi-channel delivery, audit, governance, budgets. That plumbing &lt;strong&gt;is&lt;/strong&gt; &lt;code&gt;redb.Route&lt;/code&gt;. The LLM is the last brick that previously demanded its own service.&lt;/p&gt;




&lt;h2&gt;
  
  
  Storytelling: how a chat demo becomes a platform
&lt;/h2&gt;

&lt;p&gt;Here's the timeline I've now watched several times in different teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 1.&lt;/strong&gt; Somebody shows the team that Claude can answer a support ticket. &lt;code&gt;LlmHttpRoutes.cs&lt;/code&gt; — six lines, &lt;code&gt;From("http://...").To("llm://haiku")&lt;/code&gt;, demo works. This is a &lt;strong&gt;project&lt;/strong&gt;, not a platform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/llm/ask?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxIterations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Day 7.&lt;/strong&gt; Coworkers say "it doesn't remember what I just told it". A &lt;code&gt;Process(...)&lt;/code&gt; step adds an &lt;code&gt;X-Chat-Id&lt;/code&gt; header, &lt;code&gt;ConversationFromHeader()&lt;/code&gt; is flipped on. That's it — &lt;code&gt;AddRedbLlmStorage()&lt;/code&gt; is already in &lt;code&gt;Program.cs&lt;/code&gt;, &lt;code&gt;MessageProps&lt;/code&gt; are already being written.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LlmHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConversationId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"X-Chat-Id"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Day 14.&lt;/strong&gt; "Can it run a command on the server?" A &lt;code&gt;tool-shell&lt;/code&gt; route appears — &lt;strong&gt;a separate route with &lt;code&gt;.AsLlmTool("shell")&lt;/code&gt;&lt;/strong&gt;, backed by &lt;code&gt;redb.Route.Exec&lt;/code&gt;, allowlist &lt;code&gt;[cmd, pwsh]&lt;/code&gt;, working dir in &lt;code&gt;temp&lt;/code&gt;, timeout 5 sec, 8 KB stdout cap. Safety lives &lt;strong&gt;in the DSL&lt;/strong&gt;, not in advice in a system prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:tool-shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLlmTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;SideEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolSideEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolCostClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cheap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExecDsl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;AllowedCommands&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pwsh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;TimeoutMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxStdoutBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Day 21.&lt;/strong&gt; "Finance says the token bill is 4x plan." Add &lt;code&gt;.CostBudget(usd: 0.50)&lt;/code&gt; per conversation. No new services, no new dashboards — &lt;code&gt;CostBudgetProps&lt;/code&gt; is an existing REDB object, the tsak.web dashboard already knows how to render it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 30.&lt;/strong&gt; "We need every agent action to be auditable." Already done: &lt;code&gt;ToolAuditProps&lt;/code&gt; started writing the day tools became routes. The auditor opens a SQL query — every call this quarter, filtered by tenant and side effect. No "let's set up an integration".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 45.&lt;/strong&gt; "Legal wants human-in-loop on the refund tool." Add &lt;code&gt;.Process&amp;lt;ApprovalGate&amp;gt;()&lt;/code&gt; before exec. &lt;code&gt;ApprovalProps&lt;/code&gt; writes, the Slack bot is just another HTTP route. Done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 60.&lt;/strong&gt; "We're onboarding a second customer — separate database, separate billing." Add &lt;code&gt;Process(e =&amp;gt; e.In.Headers[LlmHeaders.Redb] = e.In.Headers["X-Tenant"])&lt;/code&gt;. One route, two tenants, zero changes to logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 90.&lt;/strong&gt; "Run a million tickets through the classifier overnight." The route is already there. Change one knob: &lt;code&gt;.Mode(LlmMode.Batch)&lt;/code&gt; — a synchronous call becomes an Anthropic Batch, &lt;code&gt;LlmBatchProps&lt;/code&gt; records the id, &lt;code&gt;LlmCallbackProcessor&lt;/code&gt; waits for the webhook, results land in the same Kafka the live route uses during the day. -50% on the bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 120.&lt;/strong&gt; "Which prompt versions regressed?" &lt;code&gt;EvalRunProps&lt;/code&gt; already stores runs. SQL across &lt;code&gt;PromptTemplateRef&lt;/code&gt;, accuracy diff, v4 is bad, roll back to v3. No redeploy.&lt;/p&gt;

&lt;p&gt;There's no "and then we rewrote it on a Kubernetes operator" beat in this story. There's no "and then we added an AI platform" beat. The &lt;strong&gt;six lines&lt;/strong&gt; that worked as a chat demo on Day 1 are &lt;strong&gt;literally the same file&lt;/strong&gt; that on Day 120 is running a multi-tenant, jury-arbitrated, batch-classifying, audit-grade platform. No migration, no rewrite, no architecture v2 — just lines accreting into the route as the requirements showed up. The chat demo doesn't &lt;em&gt;grow into&lt;/em&gt; a platform — &lt;strong&gt;it was standing on a platform from day one&lt;/strong&gt;, just opting into more of its features over time.&lt;/p&gt;

&lt;p&gt;This is the pitch I couldn't quite articulate at announcement time. Now I can. The deliverable here isn't an agent framework. It's &lt;strong&gt;a runway&lt;/strong&gt;. You walk along the runway and you're already platform-shaped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo routes — see it live
&lt;/h2&gt;

&lt;p&gt;The patterns above aren't concept art. The repo &lt;code&gt;redbase-app/redb-route&lt;/code&gt; ships two demo files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LlmDemoRoutes.cs&lt;/code&gt;&lt;/strong&gt; — three shapes of the LLM call: inline step (&lt;code&gt;.Llm("demo-stub")&lt;/code&gt;), endpoint (&lt;code&gt;.To("llm://demo-stub")&lt;/code&gt;), tool (&lt;code&gt;.AsLlmTool("echo_tool")&lt;/code&gt;). All on the stub provider, no API keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LlmHttpRoutes.cs&lt;/code&gt;&lt;/strong&gt; — two HTTP endpoints (&lt;code&gt;/api/llm/ask&lt;/code&gt; no tools, &lt;code&gt;/api/llm/shell&lt;/code&gt; with the shell tool through &lt;code&gt;redb.Route.Exec&lt;/code&gt;), both passing &lt;code&gt;X-Chat-Id&lt;/code&gt; for conversation memory, both running real Claude Haiku.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;dotnet run&lt;/code&gt; from &lt;code&gt;redb.Route.Demo&lt;/code&gt;, port 5088 opens, and curl works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"what time is it on this host?"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Chat-Id: test1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     http://localhost:5088/api/llm/shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route&lt;/a&gt; (Apache 2.0).&lt;/p&gt;




&lt;h2&gt;
  
  
  What's not done, and why I keep saying so
&lt;/h2&gt;

&lt;p&gt;Sliding-window memory isn't done. A real vector store isn't done. There's no AI-graph editor in tsak.web — runtime conversation inspection shows up as plain REDB objects in the existing UI, no special pane. There's no built-in eval-service integration — &lt;code&gt;EvalRunStore&lt;/code&gt; records the runs, but "click compare against prod" is manual.&lt;/p&gt;

&lt;p&gt;That's fine. The skip-list is a &lt;strong&gt;technique&lt;/strong&gt;, not an apology. An open-source project that lies in its README about scope is one you don't come back to. I'd rather say "not yet" and ship it next minor than say "shipped" and field issues for two months.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zooming out: why the ESB shape matters
&lt;/h2&gt;

&lt;p&gt;The phrase "AI-native architecture" is fashionable. What it usually means is "we built everything around the LLM" — and what usually backs that up is &lt;strong&gt;a new dev stack running parallel to the old one&lt;/strong&gt;. That's not an architecture decision. That's a &lt;strong&gt;policy duplication problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you have retry in two stacks, audit in two stacks, governance in two stacks, tenant isolation in two stacks — those two implementations &lt;strong&gt;drift&lt;/strong&gt;. A year later, the AI stack adds bounded-context auditing, the integration stack doesn't. Legal asks for one report — nobody can produce it. The duplication wasn't visible on day one; it's load-bearing on day three hundred.&lt;/p&gt;

&lt;p&gt;The ESB shape is a deliberate choice of &lt;strong&gt;one control point for I/O of all kinds&lt;/strong&gt;. The LLM is a kind of I/O — asynchronous, with tools, with context, but I/O. Putting it inside the ESB isn't a philosophical pose; it's engineering economy: &lt;strong&gt;one governance policy covers everything&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is not new. Garland and Ripley wrote it up in the SOA literature of the 2000s. What's new is that in 2026 the argument finally &lt;strong&gt;applies to LLMs&lt;/strong&gt;, because LLMs grew up enough to expose &lt;strong&gt;standard interfaces&lt;/strong&gt;: tool use, streaming, batch APIs, embeddings. Before that, "LLM in the ESB" meant "wrap a REST call and pray". Now it means "use existing EIP patterns with minor adaptations".&lt;/p&gt;

&lt;p&gt;That economy is the entire pitch of &lt;code&gt;redb.Route.Llm&lt;/code&gt;. Not "we have the best agent engine." Microsoft and LangChain have better agent engines. We have &lt;strong&gt;a perfectly average agent engine in the right place in the architecture&lt;/strong&gt;. I'll take that trade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;p&gt;3.1.2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sliding-window memory as a built-in policy;&lt;/li&gt;
&lt;li&gt;a vector-store interface behind &lt;code&gt;IKnowledgeStore&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;pgvector and Qdrant as the first back-ends;&lt;/li&gt;
&lt;li&gt;an &lt;code&gt;EvalCompare&lt;/code&gt; DSL for side-by-side prompt-version runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3.2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;ConversationProps&lt;/code&gt; tree UI in tsak.web;&lt;/li&gt;
&lt;li&gt;streaming-aware aggregator EIP (buffer partial frames to semantic boundaries);&lt;/li&gt;
&lt;li&gt;distributed batch — multiple workers behind one &lt;code&gt;LlmCallbackProcessor&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-modal (image input/output as a payload type);&lt;/li&gt;
&lt;li&gt;voice agents as another transport (&lt;code&gt;voice://...&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;routing by cost / latency / accuracy SLA per message.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Part 1 (3.1.0 announcement): &lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/"&gt;redb.Route 3.1.0 — LLM as just another connector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redbroute-apache-camel-for-net-22-transports-30-eip-patterns-compiled-dsl-11m0"&gt;redb.Route — Apache Camel for .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/enterprise-integration-patterns-in-net-the-deep-dive-series-part-1-the-four-in-memory-channels-3e24"&gt;EIP series part 1 — channels and Exchange&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redbroute-301-flat-dsl-navigation-crtp-refactor-and-a-silent-null-fix-3m7n"&gt;redb.Route 3.0.1 patch notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route&lt;/a&gt; (Apache 2.0)&lt;/li&gt;
&lt;li&gt;Demo routes: &lt;code&gt;redb.Route/demos/redb.Route.Demo/Routes/LlmDemoRoutes.cs&lt;/code&gt;, &lt;code&gt;LlmHttpRoutes.cs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Habr companion (Russian, not a translation): link added after publication&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Three weeks ago I shipped 3.1.0 with an honest skip-list. Three weeks later, most of it is shipped: streaming end-to-end, tool cache, RAG knowledge store, async batch + callback consumer, eval-run store, versioned prompt registry, multi-tenant &lt;code&gt;?redb=&amp;lt;name&amp;gt;&lt;/code&gt;, idempotent tool retries, human-in-loop approval gates, full audit. On top, two patterns that ship the architecture's real punchline: &lt;strong&gt;a jury of cheap models with a senior model as arbiter&lt;/strong&gt;, dropping straight onto stock Scatter-Gather + Aggregator EIP — no custom orchestrator; and &lt;strong&gt;sub-agents as tools&lt;/strong&gt; — recursive agent composition where one agent's tool is itself an agent with its own model, prompt, budget, RAG corpus, all working out of the box because &lt;code&gt;RouteToolBridge → direct: → llm://&lt;/code&gt; is a fixed point. Plus three honest enterprise scenarios with code: a daily CFO snapshot to inbox, a weekly cash-flow forecast with jury arbitration, and a predictive plant alert that hands XGBoost the prediction and the LLM the sentence. The point isn't the feature checklist. The point is that &lt;strong&gt;enterprise-grade properties land on the day you actually need them, with no rewrite&lt;/strong&gt;, because the LLM lives inside the ESB along with every other I/O. Those six lines you typed for the Day-1 chat demo are &lt;strong&gt;literally the same file&lt;/strong&gt; that's running the Day-90 multi-tenant audit-grade platform. There's no "now we rewrite" moment in this story.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>redb.Route 3.1.0 — LLM(AI) as just another connector: `.To("llm://claude")` and tools-as-routes</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Tue, 09 Jun 2026 14:19:01 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redbroute-310-llmai-as-just-another-connector-tollmclaude-and-tools-as-routes-4fcg</link>
      <guid>https://dev.to/rinat_kozin/redbroute-310-llmai-as-just-another-connector-tollmclaude-and-tools-as-routes-4fcg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fznyh7pojjnuxambgkei6.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fznyh7pojjnuxambgkei6.webp" alt="redb connect llm" width="800" height="1200"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Series:&lt;/strong&gt; redb ecosystem (announcement; deep-dive separate)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;redb.Route&lt;/code&gt; 3.1.0 just shipped &lt;strong&gt;two new transports&lt;/strong&gt;: &lt;code&gt;redb.Route.Llm&lt;/code&gt; (the 24th) and &lt;code&gt;redb.Route.Exec&lt;/code&gt; (the 25th). The LLM is now an addressable endpoint the same way Kafka, RabbitMQ and HTTP are: calling a model is &lt;code&gt;.To("llm://claude")&lt;/code&gt;, an agent tool is a route with &lt;code&gt;.AsLlmTool("shell")&lt;/code&gt; on it, a scheduled agent is &lt;code&gt;From("llm://factory?schedule=5m")&lt;/code&gt;. Exec is a process spawner with allowlist, working-directory and timeout — both the backend that powers shell tools for agents and a first-class scheduled consumer in its own right (cron-less health probes, backups, deploy glue). No parallel "AI framework" sitting next to the integration framework — same DSL, same retry/throttle/circuit-breaker/audit, same OpenTelemetry traces, same dashboard endpoint counters.&lt;/p&gt;

&lt;p&gt;This post is the announcement. A deep-dive on the agent loop, the governance hooks and the REDB storage internals will follow as a separate piece. Here: what landed, what it looks like in code, and what's honestly not done yet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;First time reading about &lt;code&gt;redb.Route&lt;/code&gt;?&lt;/strong&gt; Short context from earlier posts in the series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redbroute-apache-camel-for-net-22-transports-30-eip-patterns-compiled-dsl-11m0"&gt;redb.Route — Apache Camel for .NET: 22 transports, 30+ EIP patterns, compiled DSL&lt;/a&gt; — why it exists, and what "Apache Camel for .NET" actually means&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redbase-redbroute-redbtsak-300-shipped-27pf"&gt;RedBase / redb.Route / redb.Tsak 3.0.0 shipped&lt;/a&gt; — the 3.0.0 release that this LLM connector slots into&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/enterprise-integration-patterns-in-net-the-deep-dive-series-part-1-the-four-in-memory-channels-3e24"&gt;Enterprise Integration Patterns in .NET — Part 1: the four in-memory channels (and the Exchange they carry)&lt;/a&gt; — how the runtime is built, if you want internals&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redbroute-301-flat-dsl-navigation-crtp-refactor-and-a-silent-null-fix-3m7n"&gt;redb.Route 3.0.1 — flat DSL navigation, CRTP refactor, and a silent null fix&lt;/a&gt; — the previous patch right before 3.1.0&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  The shortest possible explanation
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://orders"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxTokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://orders.translated"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One line, one full LLM call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the inbound message body becomes the user prompt;&lt;/li&gt;
&lt;li&gt;the agent runs (provider call → optional &lt;code&gt;tool_use&lt;/code&gt; → next provider call → …) until &lt;code&gt;EndTurn&lt;/code&gt; or &lt;code&gt;MaxIterations&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the assistant text lands in &lt;code&gt;exchange.Out.Body&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;token usage, model id, stop reason, iteration count are pushed into headers;&lt;/li&gt;
&lt;li&gt;OpenTelemetry traces and metrics light up automatically;&lt;/li&gt;
&lt;li&gt;the endpoint shows up in the tsak.web dashboard with msg/sec, average duration, error rate and last-error info — like every other connector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole reason "connector, not library" matters. If you already have an Apache Camel-class ESB with retry, breaker, idempotent consumer and audit, the only honest move is to turn the LLM into one more endpoint inside it. No re-implementing retries. No separate dashboard. No "AI infra" running parallel to the integration infra.&lt;/p&gt;


&lt;h2&gt;
  
  
  One adapter, 14 OpenAI-compatible APIs
&lt;/h2&gt;

&lt;p&gt;Two production providers ship today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OpenAiProvider&lt;/code&gt;&lt;/strong&gt; — one universal transport for &lt;strong&gt;14 OpenAI-compatible APIs&lt;/strong&gt;: &lt;code&gt;openai&lt;/code&gt;, &lt;code&gt;anthropic&lt;/code&gt;/&lt;code&gt;claude&lt;/code&gt; (via Anthropic's official OpenAI-compat endpoint), &lt;code&gt;groq&lt;/code&gt;, &lt;code&gt;cerebras&lt;/code&gt;, &lt;code&gt;openrouter&lt;/code&gt;, &lt;code&gt;gemini&lt;/code&gt; (OpenAI-compat surface), &lt;code&gt;github-models&lt;/code&gt;, &lt;code&gt;mistral&lt;/code&gt;, &lt;code&gt;together&lt;/code&gt;, &lt;code&gt;huggingface&lt;/code&gt;, &lt;code&gt;deepseek&lt;/code&gt;, &lt;code&gt;ollama&lt;/code&gt;, &lt;code&gt;lmstudio&lt;/code&gt;, plus a generic &lt;code&gt;custom&lt;/code&gt; for any self-hosted gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;StubProvider&lt;/code&gt;&lt;/strong&gt; — deterministic echo for unit tests and CI runs with no keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A native &lt;code&gt;AnthropicProvider&lt;/code&gt; (Messages API) is on deck for features the OpenAI-compat surface doesn't expose.&lt;/p&gt;

&lt;p&gt;Switching provider is one DI registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLlmConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"groq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"groq"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelId&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"llama-3.3-70b-versatile"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiKey&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"REDB_LLM_GROQ_KEY"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change &lt;code&gt;provider&lt;/code&gt; to &lt;code&gt;"anthropic"&lt;/code&gt; and &lt;code&gt;modelId&lt;/code&gt; to &lt;code&gt;"claude-haiku-4-5"&lt;/code&gt; and the same &lt;code&gt;.To("llm://...")&lt;/code&gt; is calling Claude. The DSL surface doesn't move a character.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools are routes
&lt;/h2&gt;

&lt;p&gt;The central architectural call: &lt;strong&gt;a tool is an ordinary &lt;code&gt;RouteBuilder&lt;/code&gt; route with one extra DSL aspect, &lt;code&gt;.AsLlmTool("name")&lt;/code&gt;&lt;/strong&gt;. Four properties fall out of that and would otherwise have to be paid for separately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:tool-shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLlmTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Run a small shell command on the host. Input: {\"command\":\"&amp;lt;name&amp;gt;\",\"args\":[\"...\"]}."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;
                &lt;span class="s"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="s"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="s"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="s"&gt;""")
&lt;/span&gt;        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SideEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolSideEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolCostClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cheap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExecDsl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AllowedCommands&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pwsh"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scratchDir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimeoutMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxStdoutBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8_192&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxStderrBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8_192&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you get for free:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tools-as-routes.&lt;/strong&gt; The 30+ EIP processors are usable &lt;em&gt;inside&lt;/em&gt; a tool — &lt;code&gt;Splitter&lt;/code&gt;, &lt;code&gt;Aggregator&lt;/code&gt;, &lt;code&gt;CircuitBreaker&lt;/code&gt;, &lt;code&gt;Throttle&lt;/code&gt;, &lt;code&gt;Filter&lt;/code&gt;, &lt;code&gt;TryCatch&lt;/code&gt;. A tool that talks to a fragile API gets wrapped in a breaker and a throttle without a line of runtime glue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools from existing routes.&lt;/strong&gt; Any &lt;code&gt;From("http://...")&lt;/code&gt; or &lt;code&gt;From("sql://...")&lt;/code&gt; you already have becomes a tool by adding &lt;code&gt;.AsLlmTool("name")&lt;/code&gt;. Its auth, its audit trail, its metrics are already in place — because it's already a route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventory-as-data.&lt;/strong&gt; &lt;code&gt;IToolDescriptorRegistry&lt;/code&gt; knows every tool by name with its JSON schema. Filter with &lt;code&gt;?tools=*&lt;/code&gt; or &lt;code&gt;?tools=lookup,shell&lt;/code&gt; on the URI; build "an agent that has every read-only tool" in one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero bumps on sibling connectors.&lt;/strong&gt; The &lt;code&gt;AsLlmTool()&lt;/code&gt; aspect lives in &lt;code&gt;redb.Route.Llm.Abstractions&lt;/code&gt;. The other 22 connectors can be used as tools today without a NuGet bump on any of them.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  A real example — a standalone "curl → Claude → shell → reply" demo
&lt;/h2&gt;

&lt;p&gt;This is a complete program. Two files — &lt;code&gt;Llm.HttpShell.csproj&lt;/code&gt; and &lt;code&gt;Program.cs&lt;/code&gt;, top-level statements, no host abstractions on top. Drop your Anthropic key into one place and &lt;code&gt;dotnet run&lt;/code&gt; brings up an HTTP endpoint on port 5088 where Claude Haiku can call a shell on the host and answer in the context of the previous turns.&lt;/p&gt;

&lt;h3&gt;
  
  
  csproj — five NuGet packages
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&lt;/span&gt; &lt;span class="na"&gt;Sdk=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.NET.Sdk"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Exe&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net9.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"redb.Route"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"3.1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"redb.Route.Http"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"3.1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"redb.Route.Llm"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"3.1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"redb.Route.Llm.Abstractions"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"3.1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"redb.Route.Exec"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"3.1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.Extensions.Logging.Console"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"9.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;redb.Route&lt;/code&gt; is the ESB core, &lt;code&gt;redb.Route.Http&lt;/code&gt; is the HTTP transport, &lt;code&gt;redb.Route.Llm[.Abstractions]&lt;/code&gt; is the LLM connector and the &lt;code&gt;.AsLlmTool()&lt;/code&gt; DSL aspect, &lt;code&gt;redb.Route.Exec&lt;/code&gt; is the process spawner with allowlist and timeout. No &lt;code&gt;redb.Core&lt;/code&gt;/&lt;code&gt;redb.Postgres&lt;/code&gt; — the demo is self-contained and keeps conversation memory in process.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ApiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;your-key&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one you get from &lt;code&gt;https://console.anthropic.com/&lt;/code&gt;. Switching to Groq is &lt;code&gt;Provider = "groq"&lt;/code&gt;, &lt;code&gt;ModelId = "llama-3.3-70b-versatile"&lt;/code&gt; and a key from &lt;code&gt;console.groq.com&lt;/code&gt;; the DSL below doesn't move.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. DI and &lt;code&gt;RouteContext&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;RouteContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ServiceCollection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLogging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSimpleConsole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SingleLine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimestampFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"HH:mm:ss "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetMinimumLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IRouteContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILoggerFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;CreateLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redb.Route"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildServiceProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RouteContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contextId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"llm-http-shell"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILoggerFactory&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILoggerFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RouteContext&lt;/code&gt; is the runtime container — routes, components, services. The &lt;code&gt;_ =&amp;gt; ctx&lt;/code&gt; closure registers &lt;code&gt;IRouteContext&lt;/code&gt; as a DI service before the context itself is constructed (the context in turn needs the &lt;code&gt;IServiceProvider&lt;/code&gt;). The last line hands &lt;code&gt;ILoggerFactory&lt;/code&gt; to the context explicitly; without it &lt;code&gt;.Log(...)&lt;/code&gt; steps in routes silently turn into no-ops (internals belong to the deep-dive piece).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F11ifyanthop79fx2a0k6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F11ifyanthop79fx2a0k6.png" alt="redb llm connector example" width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Three components
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ServerManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SharedHttpServerManager&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LlmComponent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ExecComponent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A component in redb.Route is a transport plugin owning a URI scheme: &lt;code&gt;http://&lt;/code&gt;, &lt;code&gt;llm://&lt;/code&gt;, &lt;code&gt;exec://&lt;/code&gt;. Pure registration, no business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Connection factory for Claude Haiku
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LlmConnectionFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Provider&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"anthropic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ModelId&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"claude-haiku-4-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ApiKey&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Temperature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;MaxTokens&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"haiku"&lt;/code&gt; is the label the route will reference as &lt;code&gt;Llm.Factory("haiku")&lt;/code&gt;. Same trick redb.Route uses framework-wide for named connection factories.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Agent engine + tool registry
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;toolRegistry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ToolDescriptorRegistry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IToolDescriptorRegistry&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;toolRegistry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;producerTemplate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ProducerTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IProducerTemplate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;producerTemplate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AgentEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;producerTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;producerTemplate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NoopAgentObserver&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NoopBudgetEnforcer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;approval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AutoApproveGate&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;redaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NoopRedactionFilter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NoopShadowRunner&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InMemoryConversationStore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;idempotency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;approvalStore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IAgentEngine&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every agent state surface — observability, budget, approval, redaction, shadow, conversation, idempotency — lives behind an interface. The demo wires Noop/InMemory implementations everywhere: the loop runs but nothing is persisted. Production swap is &lt;code&gt;AddRedbLlmStorage()&lt;/code&gt; (covered below): replaces &lt;code&gt;InMemoryConversationStore&lt;/code&gt; with &lt;code&gt;RedbConversationStore&lt;/code&gt;, attaches &lt;code&gt;RedbAuditObserver&lt;/code&gt;, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  6a. The HTTP route
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;isWindows&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OperatingSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsWindows&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isWindows&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pwsh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"powershell"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bash"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scratchDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTempPath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"redb-llm-shell"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scratchDir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;systemPrompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;"You can run small shell commands through the 'shell' tool. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;$"The host is &lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;isWindows&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"Windows (use cmd /c)"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Linux (use sh -c)"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"Use the tool when the user asks about the system, files, or commands; "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"then summarise what you learned in one short sentence."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http:0.0.0.0:5088/api/llm/shell?inOut=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llm-http-shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConvertBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LlmHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;LlmHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConversationId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Chat-Id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
                    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[LLM-SHELL] ▶ chat=${header.llm.conversation.id} prompt=${body}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LlmDsl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"haiku"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConversationFromHeader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxIterations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsUri&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[LLM-SHELL] ◀ iters=${header.llm.tool.iterations} stop=${header.llm.stop_reason} "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
             &lt;span class="s"&gt;"tokensIn=${header.llm.tokens.in} tokensOut=${header.llm.tokens.out}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[LLM-SHELL] ◀ reply=${body}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The POST body is converted to a string and becomes the user prompt. &lt;code&gt;Process&lt;/code&gt; puts the system prompt into the standard &lt;code&gt;LlmHeaders.SystemPrompt&lt;/code&gt; header and resolves the conversation id from the client's &lt;code&gt;X-Chat-Id&lt;/code&gt; (without it — &lt;code&gt;default&lt;/code&gt;). &lt;code&gt;.Tools("shell")&lt;/code&gt; is the only LLM-specific knob: "give the agent the tool registered under this name." &lt;code&gt;MaxIterations(10)&lt;/code&gt; caps the tool loop (otherwise it's an unbounded ping between model and tool).&lt;/p&gt;

&lt;h3&gt;
  
  
  6b. The tool itself is a route
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:tool-shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLlmTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Run a small shell command on the host. Input: "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                &lt;span class="s"&gt;"{\"command\":\"&amp;lt;name&amp;gt;\",\"args\":[\"...\"]}. Output: "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                &lt;span class="s"&gt;"{\"stdout\":\"...\",\"stderr\":\"...\",\"exitCode\":N}. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                &lt;span class="s"&gt;$"Allowed commands: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                &lt;span class="s"&gt;$"Working directory is pinned to '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scratchDir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;                &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="s"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="s"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="s"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"string"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="p"&gt;},&lt;/span&gt;
                  &lt;span class="s"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="s"&gt;""")
&lt;/span&gt;            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SideEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolSideEffect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolCostClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cheap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[SHELL-TOOL] ▶ in=${body}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExecDsl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AllowedCommands&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scratchDir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimeoutMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxStdoutBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8_192&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxStderrBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;8_192&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[SHELL-TOOL] ◀ exit=${header.redbExec.ExitCode} body=${body}"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Description&lt;/code&gt; and the JSON schema are what Claude sees as "this function is available, here's its signature." &lt;code&gt;SideEffect.ReadOnly&lt;/code&gt; and &lt;code&gt;Cost.Cheap&lt;/code&gt; are guidance for the governance hooks (budgets, approval gating). After &lt;code&gt;.Then()&lt;/code&gt; it's an ordinary route: &lt;code&gt;Log&lt;/code&gt; → &lt;code&gt;ExecDsl.Run()&lt;/code&gt; with allowlist, working dir, 5-second timeout and a stdout/stderr cap → &lt;code&gt;Log&lt;/code&gt;. No LLM-specific code below &lt;code&gt;.Then()&lt;/code&gt;. It's still just a route — which means you can wrap it in a &lt;code&gt;CircuitBreaker&lt;/code&gt;, a &lt;code&gt;Throttle&lt;/code&gt;, a &lt;code&gt;Transaction&lt;/code&gt;, a &lt;code&gt;WireTap&lt;/code&gt; shadow, anything from the 30+ EIP processors redb.Route already ships.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3l05zy0dqfcphekj9tqz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3l05zy0dqfcphekj9tqz.png" alt="redb llm connector example" width="800" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1nvjvv47pa2bb4wlktc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1nvjvv47pa2bb4wlktc.png" alt="redb llm connector example" width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Start and block
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;producerTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ManualResetEventSlim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancelKeyPress&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run

curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"how much free disk space?"&lt;/span&gt; http://localhost:5088/api/llm/shell
curl &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"what did you just say?"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Chat-Id: my-chat"&lt;/span&gt; http://localhost:5088/api/llm/shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First request: Claude sees the system prompt ("you have a &lt;code&gt;shell&lt;/code&gt; tool"), decides to check free space, calls the tool with something like &lt;code&gt;{"command":"cmd","args":["/c","fsutil","volume","diskfree","C:"]}&lt;/code&gt;, gets the stdout back, summarises it for the human. Second request, with &lt;code&gt;X-Chat-Id: my-chat&lt;/code&gt;, finds the previous turn in &lt;code&gt;InMemoryConversationStore&lt;/code&gt; and answers in context.&lt;/p&gt;

&lt;p&gt;The logs at that moment show the full path: &lt;code&gt;[LLM-SHELL] ▶ prompt=...&lt;/code&gt; → &lt;code&gt;[SHELL-TOOL] ▶ in={"command":"cmd",...}&lt;/code&gt; → &lt;code&gt;[SHELL-TOOL] ◀ exit=0 body={"stdout":"...","exitCode":0}&lt;/code&gt; → &lt;code&gt;[LLM-SHELL] ◀ iters=2 stop=end_turn tokensIn=... tokensOut=... reply=...&lt;/code&gt;. All of it is just the redb.Route &lt;code&gt;.Log()&lt;/code&gt; step, not a separate LLM-tracing pipeline.&lt;/p&gt;

&lt;p&gt;The command allowlist (&lt;code&gt;cmd&lt;/code&gt;, &lt;code&gt;pwsh&lt;/code&gt;, &lt;code&gt;sh&lt;/code&gt;, &lt;code&gt;bash&lt;/code&gt;) is the security envelope: anything outside it is rejected by &lt;code&gt;redb.Route.Exec&lt;/code&gt; before a process starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;redb.Route.Exec&lt;/code&gt; — process spawning as the 25th transport
&lt;/h2&gt;

&lt;p&gt;In the demo above &lt;code&gt;ExecDsl.Run()&lt;/code&gt; showed up as "the backend of the &lt;code&gt;shell&lt;/code&gt; tool", but it's a connector in its own right, shipped in 3.1.0 alongside &lt;code&gt;redb.Route.Llm&lt;/code&gt;. It closes a boring but ubiquitous gap: the framework already spoke 22+ transports (HTTP, Kafka, SQL, …), but reaching out to the operating system itself was missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer — &lt;code&gt;.To(ExecDsl.Run(...))&lt;/code&gt;&lt;/strong&gt; — synchronous spawn. The command is resolved in this order: JSON body → headers &lt;code&gt;redbExec.Command&lt;/code&gt;/&lt;code&gt;redbExec.Args&lt;/code&gt; → URI options &lt;code&gt;?command=...&lt;/code&gt;. The Out body is JSON shaped for the LLM tool ABI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stdout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stderr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"exitCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timedOut"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is exactly why the shell tool in the demo is &lt;code&gt;.To(ExecDsl.Run().AllowedCommands(...))&lt;/code&gt; with zero glue code in between. The model produces &lt;code&gt;{"command":"cmd","args":[...]}&lt;/code&gt;, the producer parses it, runs it, returns structured JSON. Route-level features (&lt;code&gt;CircuitBreaker&lt;/code&gt;, &lt;code&gt;Throttle&lt;/code&gt;, &lt;code&gt;OnException&lt;/code&gt;, audit) apply automatically — it's an endpoint like every other one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer — &lt;code&gt;From(ExecDsl.Run(...).Schedule("5m"))&lt;/code&gt;&lt;/strong&gt; — same spawner, but as a first-class source endpoint with a built-in scheduler. No cron, no separate worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExecDsl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./scripts/health-check.sh"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"5m"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                   &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimeoutMs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30_000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"redbExec.ExitCode"&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://alerts.internal/oncall"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://metrics.healthy"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 5 minutes the script runs, the route branches on exit code — one way to an HTTP webhook for the on-call, the other to a Kafka topic. &lt;code&gt;?schedule=&lt;/code&gt; accepts simple intervals (&lt;code&gt;500ms&lt;/code&gt;, &lt;code&gt;30s&lt;/code&gt;, &lt;code&gt;5m&lt;/code&gt;, &lt;code&gt;1h&lt;/code&gt;). For cron expressions: &lt;code&gt;From("quartz://&amp;lt;cron&amp;gt;").To(ExecDsl.Run(...))&lt;/code&gt; — Quartz is already a scheduler, no point re-implementing it inside Exec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The security envelope&lt;/strong&gt; — what gives shell-as-a-tool the right to exist in the first place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AllowedCommands&lt;/code&gt; — case-insensitive on the file name, so &lt;code&gt;/usr/bin/git&lt;/code&gt; and &lt;code&gt;git.exe&lt;/code&gt; both match &lt;code&gt;git&lt;/code&gt;. Anything off the list is rejected with &lt;code&gt;UnauthorizedAccessException&lt;/code&gt; before the process starts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WorkingDirectory&lt;/code&gt; — pins the cwd; the spawned process can't escape it on its own.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EnvironmentOverrides&lt;/code&gt; + &lt;code&gt;ScrubEnvironment&lt;/code&gt; — start from an empty environment and apply only the &lt;code&gt;KEY=VALUE&lt;/code&gt; pairs you set.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TimeoutMs&lt;/code&gt; — wall-clock kill-switch that takes the whole process tree.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MaxStdoutBytes&lt;/code&gt; / &lt;code&gt;MaxStderrBytes&lt;/code&gt; — caps that protect the host from a runaway process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why a separate package, not part of &lt;code&gt;redb.Route.Llm&lt;/code&gt;&lt;/strong&gt; — three reasons: (1) Exec is useful with no LLM in sight (scheduled health probes, log rotation, deploy glue, backups); (2) &lt;code&gt;redb.Route.Llm&lt;/code&gt; shouldn't drag a dependency on process spawning into projects whose agents only call HTTP or SQL tools; (3) the same allowlist/timeout/cap mechanism will be reused by future connectors that share the same security profile.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;From("llm://...")&lt;/code&gt; — the scheduled agent consumer
&lt;/h2&gt;

&lt;p&gt;This is the bit that has no equivalent in Camel's &lt;code&gt;langchain4j-*&lt;/code&gt; family (where LLM is producer-only): &lt;strong&gt;the LLM endpoint is a first-class source&lt;/strong&gt;, with the scheduler baked in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llm://groq?schedule=5m"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;"&amp;amp;systemPromptRef=#watchdog-system"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;"&amp;amp;initialBodyRef=#daily-brief"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
     &lt;span class="s"&gt;"&amp;amp;tools=*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rabbitmq://alerts"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 5 minutes: a fresh agent run with the system prompt resolved from registry key &lt;code&gt;#watchdog-system&lt;/code&gt; and the user prompt from &lt;code&gt;#daily-brief&lt;/code&gt;. The reply goes to RabbitMQ. &lt;code&gt;?schedule=&lt;/code&gt; takes simple intervals (&lt;code&gt;500ms&lt;/code&gt;, &lt;code&gt;30s&lt;/code&gt;, &lt;code&gt;5m&lt;/code&gt;, &lt;code&gt;1h&lt;/code&gt;); for cron use &lt;code&gt;From("quartz://...").To("llm://...")&lt;/code&gt; — Quartz is already a scheduler, no point duplicating it inside the LLM consumer.&lt;/p&gt;

&lt;p&gt;Use cases that fit this shape: watchdog agents, scheduled report generation, self-improving agents with conversation memory (same history across runs).&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;#&lt;/code&gt;-prompts — the dynamic registry
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;#&lt;/code&gt; prefix on a URI parameter turns the value into a registry lookup. Some other route can rewrite the prompt by name; the next agent run picks it up without a redeploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"style.terse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Reply in fewer than 5 words."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct:chat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llm://scripted?systemPromptRef=#style.terse"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mock:done"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ... later, from another route ...&lt;/span&gt;
&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToRegistry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"style.terse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Reply in French only."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// the next call sees the new value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resolution: first &lt;code&gt;IPromptTemplateRegistry&lt;/code&gt; (versioned, what eval replay needs), then the generic context registry (a plain string). Without &lt;code&gt;#&lt;/code&gt; the value is literal and arrives at the provider unchanged.&lt;/p&gt;

&lt;p&gt;This is the same &lt;code&gt;#name&lt;/code&gt; registry convention redb.Route uses framework-wide for connection factories. Same convention, not a separate prompt-ref DSL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agent memory — &lt;code&gt;AddRedbLlmStorage()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;By default every state surface is in-memory (conversation transcripts, tool idempotency, approvals, cost ledgers, audit). That keeps &lt;code&gt;AddRedbRouteLlm()&lt;/code&gt; zero-dependency — fine for tests and stateless agents, lost on restart.&lt;/p&gt;

&lt;p&gt;One line swaps the defaults for REDB-backed stores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRedbRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRedbRouteLlm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRedbIdempotentRepository&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// required for idempotency&lt;/span&gt;
    &lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRedbLlmStorage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;             &lt;span class="c1"&gt;// ← REDB stores on every surface&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What &lt;code&gt;AddRedbLlmStorage()&lt;/code&gt; replaces:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;REDB store&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IConversationStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InMemoryConversationStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RedbConversationStore&lt;/code&gt; — tree-backed, parent linkage via native &lt;code&gt;parent_id&lt;/code&gt;, message ids on indexed &lt;code&gt;value_string&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IToolIdempotencyStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InMemoryToolIdempotencyStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RedbToolIdempotencyStore&lt;/code&gt; (on top of &lt;code&gt;IIdempotentRepository&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IApprovalStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InMemoryApprovalStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RedbApprovalStore&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ICostBudgetStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InMemoryCostBudgetStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RedbCostBudgetStore&lt;/code&gt; — running totals per tenant/window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IAgentObserver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NoopAgentObserver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RedbAuditObserver&lt;/code&gt; — one row per tool call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two non-obvious design choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-row business identifiers live on &lt;code&gt;_objects.value_string&lt;/code&gt; (an indexed column), not inside the props JSON. Lookups are O(log n) on one column, not a full scan with JSON decode.&lt;/li&gt;
&lt;li&gt;Transcript integrity is the tree's native &lt;code&gt;parent_id&lt;/code&gt; (&lt;code&gt;IRedbService.CreateChildAsync&lt;/code&gt;), not a soft FK in props. The tree primitive enforces it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stores lazy-sync their scheme on first use. No migration step.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the engine carries for free
&lt;/h2&gt;

&lt;p&gt;This is the central argument for "connector, not library." Everything redb.Route already does applies to LLM calls without extra code:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;DSL primitive&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Retry / backoff&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RedeliveryPolicy&lt;/code&gt;, &lt;code&gt;OnException&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Throttle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resilience&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CircuitBreaker&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IdempotentConsumer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compensation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Saga&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit / shadow&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WireTap&lt;/code&gt;, &lt;code&gt;Multicast&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tracing &amp;amp; metrics&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RouteActivitySource&lt;/code&gt;, &lt;code&gt;RouteMetrics&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;redb&lt;/code&gt; schemes (typed object engine)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A tool that calls an expensive API gets wrapped in a &lt;code&gt;CircuitBreaker&lt;/code&gt; and a &lt;code&gt;Throttle&lt;/code&gt;. Want a shadow run of a new system prompt alongside the old one? &lt;code&gt;Multicast&lt;/code&gt; + &lt;code&gt;WireTap&lt;/code&gt;. These aren't "features of the LLM connector" — they're the engine, and the LLM is attached to it as an endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's live-tested, and what isn't
&lt;/h2&gt;

&lt;p&gt;The honest status of the provider matrix (this is in the README, repeated here):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groq + Llama 3.3 70B&lt;/strong&gt; — the most reliable free tier in scope. Carries the strict assertion in &lt;code&gt;BasicChatTests&lt;/code&gt; and &lt;code&gt;ToolRouteTests&lt;/code&gt; (the model has to produce a literal token).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral small-latest&lt;/strong&gt; — stable for short-form replies; tool-use on the free tier is flaky, so tool tests assert philosophically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 2.0 Flash&lt;/strong&gt; — 15 RPM on free; works on a quiet repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic Claude&lt;/strong&gt; (Haiku 4.5 / Sonnet 4.6) — via the OpenAI-compat endpoint, live-tested in &lt;code&gt;ClaudeChatTests&lt;/code&gt; and the &lt;code&gt;redb.Route.Demo&lt;/code&gt; host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenRouter / Cerebras&lt;/strong&gt; — scaffolding works; individual free models rate-limit; tests gated on &lt;code&gt;[EnvFact]&lt;/code&gt; and skipped without a key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honest skip-list, things not in this release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings and vector stores&lt;/strong&gt; — Phase 2 (&lt;code&gt;embed://&lt;/code&gt;, &lt;code&gt;vector://&lt;/code&gt; schemes are planned).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG primitives, document loaders, web search&lt;/strong&gt; — not first-class yet (a Tavily search tool is already shipped in &lt;code&gt;redb.Route.Llm.Tools&lt;/code&gt; and plugs in as any other &lt;code&gt;.AsLlmTool("web_search")&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sliding-window memory shapes&lt;/strong&gt; (window-by-N-messages, window-by-K-tokens) — not first-class. Persistent transcripts via &lt;code&gt;AddRedbLlmStorage()&lt;/code&gt; are shipped; a windowed shape on top is realizable today via &lt;code&gt;Process&lt;/code&gt; + the conversation store, just not as a one-line option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native AnthropicProvider&lt;/strong&gt; — OpenAI-compat surface covers most scenarios; the native Messages API is for the features the compat surface doesn't expose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffh4rbr3jf860xsd8r2vz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffh4rbr3jf860xsd8r2vz.png" alt="redb llm connector example run on tsak side" width="800" height="721"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;GitHub leads NuGet.&lt;/strong&gt; Fresh bug fixes (e.g. the &lt;code&gt;tool_use&lt;/code&gt;/&lt;code&gt;tool_result&lt;/code&gt;&lt;br&gt;
pairing recovery on conversation reload, or the Windows OEM-codepage&lt;br&gt;
decoding fix in &lt;code&gt;redb.Route.Exec&lt;/code&gt;) are already cut on &lt;code&gt;main&lt;/code&gt; under version&lt;br&gt;
&lt;strong&gt;3.1.1&lt;/strong&gt; — see &lt;a href="https://github.com/redbase-app/redb-route/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;&lt;code&gt;CHANGELOG.md&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
in the public repo. NuGet packages are batched into the next release, so&lt;br&gt;
if you hit a production bug — check &lt;code&gt;main&lt;/code&gt; and the latest pre-release tag&lt;br&gt;
first, then wait for the NuGet bump. This is normal practice, not a&lt;br&gt;
process bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;redb.Route on GitHub&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redb.Route.Llm on NuGet&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.nuget.org/packages/redb.Route.Llm" rel="noopener noreferrer"&gt;nuget.org/packages/redb.Route.Llm&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redb.Route.Exec on NuGet&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.nuget.org/packages/redb.Route.Exec" rel="noopener noreferrer"&gt;nuget.org/packages/redb.Route.Exec&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full package README&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route/blob/main/redb.Route.Llm/README.md" rel="noopener noreferrer"&gt;redb.Route.Llm/README.md&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USER-GUIDE (full walkthrough — DSL, governance, conversation)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route/blob/main/redb.Route.Llm/doc/USER-GUIDE.md" rel="noopener noreferrer"&gt;redb.Route.Llm/doc/USER-GUIDE.md&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STORAGE — REDB schemas for conversation / approval / audit / idempotency&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route/blob/main/redb.Route.Llm/doc/STORAGE.md" rel="noopener noreferrer"&gt;redb.Route.Llm/doc/STORAGE.md&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exec connector README&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route/blob/main/redb.Route.Exec/README.md" rel="noopener noreferrer"&gt;redb.Route.Exec/README.md&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone &lt;code&gt;Llm.HttpShell&lt;/code&gt; demo (curl → Claude → shell → reply)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route/blob/main/demo/Llm.HttpShell/Program.cs" rel="noopener noreferrer"&gt;demo/Llm.HttpShell/Program.cs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full demo project (22+ routes)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route/blob/main/demo/redb.Route.Demo/Routes/LlmHttpRoutes.cs" rel="noopener noreferrer"&gt;demo/redb.Route.Demo/Routes/LlmHttpRoutes.cs&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discussions&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/redbase-app/redb-route/discussions" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route/discussions&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All Apache 2.0. A separate deep-dive will cover the agent tool-loop, the governance hook interfaces, and a tour of the REDB storage schemas. Questions in the comments are exactly what writes the next post — especially anything along the lines of "does the connector do X if I do Y?"&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>REDB inside, part 1.1 — why the same 13 tables stay fast no matter how many classes you throw at them</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Mon, 08 Jun 2026 21:57:41 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redb-inside-part-11-why-the-same-13-tables-stay-fast-no-matter-how-many-classes-you-throw-at-1gg5</link>
      <guid>https://dev.to/rinat_kozin/redb-inside-part-11-why-the-same-13-tables-stay-fast-no-matter-how-many-classes-you-throw-at-1gg5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8eg570it3m54lls8m4v.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8eg570it3m54lls8m4v.webp" alt="REDB Index" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A couple of weeks ago I published &lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redb-inside-part-1-the-13-tables-the-whole-engine-runs-on-with-the-actual-sql-and-why-its-not-18mf"&gt;REDB inside, part 1 — the 13 tables the whole engine runs on&lt;/a&gt;. It walked through &lt;code&gt;_objects&lt;/code&gt;, &lt;code&gt;_values&lt;/code&gt;, &lt;code&gt;_structures&lt;/code&gt;, why this isn't classical EAV, and what the &lt;code&gt;_scheme_metadata_cache&lt;/code&gt; does. If you haven't read it, start there — the rest of this post assumes you know the layout.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;part 1.1&lt;/strong&gt;, not part 2. Same physical-storage conversation, different angle. Part 2 is going to be about code-first schemes (&lt;code&gt;SyncSchemeAsync&amp;lt;T&amp;gt;&lt;/code&gt;), and &lt;strong&gt;the deep C# dive — LINQ translator, CRUD internals, trees — that's parts 3 through 5 of the series&lt;/strong&gt;. This post stays in the database layer.&lt;/p&gt;

&lt;p&gt;The reason for a 1.1 instead of jumping to 2 is simple: every time I publish part 1, the same question comes back in the comments and in DMs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Fine, typed columns beat string EAV. But you've still got 9.7 million rows in &lt;code&gt;_values&lt;/code&gt;. Any real query — &lt;code&gt;WHERE Salary &amp;gt; 80000&lt;/code&gt;, &lt;code&gt;WHERE OrderDate &amp;gt;= '2026-06-01'&lt;/code&gt; — has to scan that table. Without one index per field per schema you're living in Seq Scan."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair question. Let's actually answer it, with the DDL, the prod numbers, and a side note about how to &lt;em&gt;remove&lt;/em&gt; indexes once your app stabilizes.&lt;/p&gt;

&lt;p&gt;Numbers in this post come from TSUM — a logistics system handling truck movements and orders through distribution centers. Real production, no benchmark setup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; In a classical EF schema, indexes multiply with tables. In REDB, the index set is designed once in DDL and serves any business schema you put on top — adding the 33rd class to your domain doesn't add a single line to &lt;code&gt;redbPostgre.sql&lt;/code&gt;. The active surface for any business class is just &lt;strong&gt;2 tables&lt;/strong&gt; (&lt;code&gt;_objects&lt;/code&gt; + &lt;code&gt;_values&lt;/code&gt;), plus 2 more for lookups and 3 for RTTI metadata; the rest is infrastructure that doesn't grow with your class count. On TSUM prod this gives &lt;strong&gt;999 orders / 991 ms&lt;/strong&gt; on 2 cores with default Postgres settings and zero framework-level cache: one SELECT pulls all 999 existing routes (139 ms), bulk-save writes 32 changed objects through the COPY protocol (154 ms), and 967 unchanged rows never reach the database thanks to an app-level &lt;code&gt;ComputeHash()&lt;/code&gt; short-circuit.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The "9.7 million rows" objection, defused in two sentences
&lt;/h2&gt;

&lt;p&gt;Before we open any DDL, here's the structural answer to the objection above.&lt;/p&gt;

&lt;p&gt;Classical EAV has rows like &lt;code&gt;(object_id, attribute_name TEXT, value TEXT)&lt;/code&gt;. An index on &lt;code&gt;value&lt;/code&gt; is useless there because values are heterogeneous, casts happen at runtime, and selectivity on &lt;code&gt;attribute_name&lt;/code&gt; is low. &lt;strong&gt;REDB is not that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two things change the picture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Values are already split into typed columns.&lt;/strong&gt; &lt;code&gt;_Long&lt;/code&gt;, &lt;code&gt;_String&lt;/code&gt;, &lt;code&gt;_Numeric&lt;/code&gt;, &lt;code&gt;_DateTimeOffset&lt;/code&gt;, &lt;code&gt;_Boolean&lt;/code&gt;, &lt;code&gt;_Guid&lt;/code&gt;, &lt;code&gt;_ListItem&lt;/code&gt;, &lt;code&gt;_Object&lt;/code&gt;. No casts at query time. An index on &lt;code&gt;_Long&lt;/code&gt; is a normal bigint btree, not a polymorphic guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every row in &lt;code&gt;_values&lt;/code&gt; carries &lt;code&gt;_id_structure&lt;/code&gt;&lt;/strong&gt; — a bigint pointing at the schema field it belongs to. With 401 fields across the TSUM domain, &lt;code&gt;WHERE _id_structure = X&lt;/code&gt; discards roughly 99.75% of the table before the optimizer even looks at the value column.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So a facet query never scans 9.7M rows. It scans &lt;code&gt;~_values_count / structure_count&lt;/code&gt; rows, then does an Index Only Scan on a typed column. We'll see this in the EXPLAIN-equivalent reasoning below.&lt;/p&gt;

&lt;p&gt;That's the architecture. Now the question that actually matters: &lt;strong&gt;how exactly are the indexes laid out so that this runs in milliseconds?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The architectural payoff: indexes don't grow with your domain
&lt;/h2&gt;

&lt;p&gt;In a classical EF layout, the table count grows linearly with business entities. Each table averages 3–5 indexes — primary key, FK indexes, business filters. So &lt;strong&gt;the total number of indexes in the database grows multiplicatively&lt;/strong&gt; with project size.&lt;/p&gt;

&lt;p&gt;Take TSUM. Here's what's in &lt;code&gt;tsum.Domain/Entities&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TransportationRoute&lt;/code&gt; (the actual route)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TransportationPoint&lt;/code&gt; (route stops)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Driver&lt;/code&gt;, &lt;code&gt;Vehicle&lt;/code&gt;, &lt;code&gt;ShippingPoint&lt;/code&gt;, &lt;code&gt;YardPlace&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SliceSettings&lt;/code&gt;, &lt;code&gt;SliceSnapshot&lt;/code&gt;, &lt;code&gt;TransportSnapshot&lt;/code&gt;, &lt;code&gt;TransportNorm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SpecialRcSettings&lt;/code&gt;, &lt;code&gt;TonnageGroupSettings&lt;/code&gt;, &lt;code&gt;GarageState&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TsumAdUserRef&lt;/code&gt;, &lt;code&gt;UserFilterPreference&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fifteen "root" entities. But that's just the start. Inside &lt;code&gt;TransportationRoute&lt;/code&gt; alone you'll find &lt;code&gt;RedbListItem&lt;/code&gt; references to: &lt;code&gt;Drivers&lt;/code&gt;, &lt;code&gt;Vehicles&lt;/code&gt;, &lt;code&gt;CarMarks&lt;/code&gt;, &lt;code&gt;ShippingPoints&lt;/code&gt;, &lt;code&gt;BusinessTypes&lt;/code&gt;, &lt;code&gt;YardPlaces&lt;/code&gt; (×2 — &lt;code&gt;PlaceTo&lt;/code&gt; / &lt;code&gt;PlaceFrom&lt;/code&gt;), &lt;code&gt;LoadingZones&lt;/code&gt;, &lt;code&gt;TransportStatuses&lt;/code&gt;, &lt;code&gt;DeliveryStatuses&lt;/code&gt;, &lt;code&gt;TripRisks&lt;/code&gt;. Every other entity has its own set: chassis types, readiness statuses, delay reasons, route classifications.&lt;/p&gt;

&lt;p&gt;In a classical schema, every one of those lookups is its own table. Plus audit/history tables (&lt;code&gt;TransportSnapshot&lt;/code&gt;, &lt;code&gt;SliceSnapshot&lt;/code&gt; are clearly snapshot entities). Plus M2M tables for collection links. &lt;strong&gt;A realistic estimate for TSUM in a classical layout is 60–80 tables.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At 60–80 tables and 3–4 indexes per table, you're looking at &lt;strong&gt;200–320 indexes&lt;/strong&gt; to design, maintain, reindex, and drop when stale. Every release in an EF project ships an &lt;code&gt;AddColumn&lt;/code&gt; + &lt;code&gt;CreateIndex&lt;/code&gt; migration. By year two, somebody walks the schema with a flashlight removing duplicate indexes that piled up because three different developers added similar-but-not-identical indexes for slightly different queries.&lt;/p&gt;

&lt;p&gt;In REDB the picture is different. The same 32 schemes / 401 properties in TSUM physically have &lt;strong&gt;2 primary keys&lt;/strong&gt;: &lt;code&gt;pk__objects&lt;/code&gt; and &lt;code&gt;pk__values&lt;/code&gt;. The full index list is hardcoded in &lt;code&gt;redbPostgre.sql&lt;/code&gt; and runs around 50 indexes — &lt;strong&gt;for the entire system&lt;/strong&gt;. Not per class. For the system.&lt;/p&gt;

&lt;p&gt;Now here's the clarification I owe readers from part 1. I called REDB "13 tables" in that post, and a fair number of people walked away with the impression that all 13 are involved every time you query a class. They aren't. The actual breakdown is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data storage — 2 tables:&lt;/strong&gt; &lt;code&gt;_objects&lt;/code&gt; (the header) and &lt;code&gt;_values&lt;/code&gt; (the field values). Every &lt;code&gt;Query&amp;lt;T&amp;gt;()&lt;/code&gt;, every &lt;code&gt;SaveAsync&lt;/code&gt;, every LINQ filter lands here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lookups — 2 tables:&lt;/strong&gt; &lt;code&gt;_lists&lt;/code&gt; and &lt;code&gt;_list_items&lt;/code&gt;. These power &lt;code&gt;RedbListItem&lt;/code&gt; fields (Driver, Vehicle, ShippingPoint, etc.). Lookups are shared across classes and don't grow when you add a new business entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTTI / schema metadata — 3 tables:&lt;/strong&gt; &lt;code&gt;_types&lt;/code&gt; (primitives), &lt;code&gt;_schemes&lt;/code&gt; (classes), &lt;code&gt;_structures&lt;/code&gt; (class properties). The engine reads these through &lt;code&gt;_scheme_metadata_cache&lt;/code&gt;; business queries never touch them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything else — infrastructure:&lt;/strong&gt; &lt;code&gt;_users&lt;/code&gt;, &lt;code&gt;_roles&lt;/code&gt;, &lt;code&gt;_permissions&lt;/code&gt;, &lt;code&gt;_links&lt;/code&gt;, &lt;code&gt;_dependencies&lt;/code&gt;, &lt;code&gt;_functions&lt;/code&gt;, &lt;code&gt;_scheme_metadata_cache&lt;/code&gt;, soft-delete via &lt;code&gt;@@__deleted&lt;/code&gt;. These don't grow with class count and aren't on the hot path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So whatever class a developer writes in &lt;code&gt;tsum.Domain&lt;/code&gt;, its data physically lives in &lt;strong&gt;2 tables&lt;/strong&gt;; reference fields hit &lt;strong&gt;2&lt;/strong&gt; more for lookups; schema metadata sits in &lt;strong&gt;3&lt;/strong&gt; RTTI tables. The six infrastructure tables look identical for a 5-class project and a 500-class one. &lt;em&gt;That&lt;/em&gt; is what "indexes invariant of class count" really means — not "all 13 tables on every query," but "the active footprint for business data is two tables, and it doesn't change."&lt;/p&gt;

&lt;p&gt;When a developer adds the 33rd class to &lt;code&gt;tsum.Domain&lt;/code&gt; — say, &lt;code&gt;WarehouseSlot&lt;/code&gt; — the database gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one row in &lt;code&gt;_schemes&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;N rows in &lt;code&gt;_structures&lt;/code&gt; (one per property),&lt;/li&gt;
&lt;li&gt;zero index migrations. None.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the entire query surface for the new class is served by the same indexes on &lt;code&gt;_objects&lt;/code&gt; and &lt;code&gt;_values&lt;/code&gt; that served the previous 32. &lt;code&gt;WHERE _id_scheme = ID(WarehouseSlot)&lt;/code&gt; uses &lt;code&gt;IX__objects__schemes&lt;/code&gt;. Sort by creation date — &lt;code&gt;IX__objects__scheme_date_create&lt;/code&gt;. Find an object by some property value — &lt;code&gt;IX__values__structure_object_lookup&lt;/code&gt;. Parent–child tree — &lt;code&gt;IX__objects__scheme_parent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the architectural payoff the post is built around. &lt;strong&gt;The index plan is fixed at engine design time&lt;/strong&gt;, not smeared across business-app releases. The skeptic will counter: "so generic indexes on &lt;code&gt;_values&lt;/code&gt; must be slower than per-field indexes in an EF schema." They aren't — and we'll see why below.&lt;/p&gt;

&lt;p&gt;The same applies to MSSQL. Worth saying out loud: this post isn't Postgres-specific. &lt;code&gt;redb.MSSql/sql/redbMSSQL.sql&lt;/code&gt; carries a mirror set of indexes with minor dialect differences (covered in a later section). The architectural trick — indexes on the physical schema, not on business tables — ports between engines without giving anything up.&lt;/p&gt;




&lt;h2&gt;
  
  
  A tour of &lt;code&gt;redbPostgre.sql&lt;/code&gt;, grouped by what it's for
&lt;/h2&gt;

&lt;p&gt;If you open the DDL and read top to bottom you'll see ~50 indexes interleaved. They're easier to understand grouped &lt;strong&gt;by use case&lt;/strong&gt; rather than by table. Let's do it that way.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Storage uniqueness in &lt;code&gt;_values&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Three partial-unique indexes, one per shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Scalar fields: one (object, field) → one value&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;UIX__values__structure_object&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- The marker row for a nested class/array inside a parent&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;UIX__values__structure_object_parent&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Collection elements indexed by position/key&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;UIX__values__structure_object_array_index&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why three instead of one. Each shape has a different uniqueness key. Scalars are unique by &lt;code&gt;(structure, object)&lt;/code&gt;. Nested-structure markers add &lt;code&gt;parent&lt;/code&gt;. Array elements add &lt;code&gt;index&lt;/code&gt;. A single covering unique index can't express this (NULLs would break the semantics), but three partials map cleanly onto the physical model.&lt;/p&gt;

&lt;p&gt;Bonus: each partial only stores its slice of rows, so the btree is smaller than one giant unique index would be. On 9.7M rows that's tens of percent saved in page cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Facet search — the hottest path
&lt;/h3&gt;

&lt;p&gt;A facet query in REDB is "find objects whose field X compares to Y." In SQL terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;structure_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Long&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pattern is served by a single composite index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__values__structure_object_lookup&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ListItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every typed column except &lt;code&gt;_String&lt;/code&gt; is in the key. Index Only Scan: Postgres finds the matching rows and grabs the value &lt;strong&gt;directly from the index&lt;/strong&gt; — no heap fetch.&lt;/p&gt;

&lt;p&gt;In parallel, two covering indexes with &lt;code&gt;INCLUDE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__values__object_structure_lookup&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ListItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__values__object_array_null&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ListItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_Object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These cover the opposite direction: "load all values of an object in one query," for &lt;code&gt;LoadAsync&amp;lt;T&amp;gt;&lt;/code&gt;. Same trick — Index Only Scan, zero heap reads.&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;_String&lt;/code&gt; is excluded. Postgres btree caps at ~2700 bytes per index row. If a JWT payload ends up in &lt;code&gt;_String&lt;/code&gt; at 3 KB, the insert blows up. So strings get their own partial index with a length guard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__values__String_not_null&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_String&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Long strings (CMS bodies, base64 payloads) aren't covered — and they aren't filtered with &lt;code&gt;=&lt;/code&gt; either. For substring search there's a separate GIN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_trgm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__values__String_pattern&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_String&lt;/span&gt; &lt;span class="n"&gt;gin_trgm_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_String&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This serves &lt;code&gt;LIKE&lt;/code&gt;, &lt;code&gt;ILIKE&lt;/code&gt;, &lt;code&gt;$contains&lt;/code&gt;, &lt;code&gt;$startsWith&lt;/code&gt;, &lt;code&gt;$endsWith&lt;/code&gt;, regex — all through &lt;code&gt;pg_trgm&lt;/code&gt;. The MSSQL counterpart is full-text search or a persisted computed column with a filtered index; the choice is in the dialect's DDL.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Collections (arrays and dictionaries)
&lt;/h3&gt;

&lt;p&gt;Part 1 covered relational storage of collections — a marker row plus child rows linked by &lt;code&gt;_array_parent_id&lt;/code&gt; and &lt;code&gt;_array_index&lt;/code&gt;. The supporting indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- traverse collection elements by index&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__values__array_parent_index&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- look up by dictionary key (string keys)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__values__array_key&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- partial: only rows that belong to collections&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__values__parent_structure&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third one feeds the pivot CTE that reads nested Class/Dictionary fields like &lt;code&gt;AddressBook["home"].City&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Object trees (&lt;code&gt;_objects._id_parent&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Trees are the backbone of REDB — sections, categories, org charts, all linked through &lt;code&gt;_id_parent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- direct children: scheme + parent → id&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__scheme_parent&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- covering index for subtree traversal (metadata in INCLUDE)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__parent_id_descendant_lookup&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_date_create&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_date_modify&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_id_parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- partial: roots (objects without a parent)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__root_objects&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_id_parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- reverse path: child → parent → scheme&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__id_parent_scheme&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_id_parent&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WhereHasAncestor&lt;/code&gt;, &lt;code&gt;WhereHasDescendant&lt;/code&gt;, &lt;code&gt;LoadTreeAsync&lt;/code&gt; all run through this quartet.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Sorting and metadata
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- main "newest first" feed index&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__scheme_date_create&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_date_create&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- sort by name within a scheme&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__scheme_name&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- global indexes for cross-scheme search&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__name&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__hash&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the indexes for &lt;code&gt;RedbPrimitive&amp;lt;T&amp;gt;&lt;/code&gt; (when an object &lt;em&gt;is&lt;/em&gt; a primitive — counter, token, single value):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__value_long&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_value_long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_value_long&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__value_string&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_value_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_value_string&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__value_guid&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_value_guid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_value_guid&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__value_datetime&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_value_datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_value_datetime&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__objects__value_numeric&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_value_numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_value_numeric&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All partial. Each one only indexes rows where its value column is set.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Schemes and structures (&lt;code&gt;_structures&lt;/code&gt;, &lt;code&gt;_schemes&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;These are the cold tables — orders of magnitude smaller than &lt;code&gt;_values&lt;/code&gt;, mostly read while building the metadata cache. They still need to be sub-millisecond:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- covering index for ORDER BY field name&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__structures__name&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_structures&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_collection_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- covering lookup by structure ID&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__structures__id_lookup&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_structures&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_collection_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- partial: split collection vs non-collection&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__structures__not_collection&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_structures&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_collection_type&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX__structures__collection&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_structures&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_scheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_collection_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_collection_type&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DDL comments record the reason: these indexes kill Seq Scan on InitPlan and EXISTS subqueries inside &lt;code&gt;build_hierarchical_properties_optimized&lt;/code&gt;, dropping query cost from 6.10 to 4.29 (-30%).&lt;/p&gt;

&lt;h3&gt;
  
  
  What got removed
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;redbPostgre.sql&lt;/code&gt; has a commented-out block worth quoting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ============================================&lt;/span&gt;
&lt;span class="c1"&gt;-- REMOVED REDUNDANT INDEXES (migration_drop_redundant_indexes.sql)&lt;/span&gt;
&lt;span class="c1"&gt;-- Reason: Covered by composite index IX__values__structure_object_lookup&lt;/span&gt;
&lt;span class="c1"&gt;-- Facet search ALWAYS filters by (_id_structure, _id_object) BEFORE value&lt;/span&gt;
&lt;span class="c1"&gt;-- ============================================&lt;/span&gt;
&lt;span class="c1"&gt;-- CREATE INDEX "IX__values__String" ON _values (_String) ...;&lt;/span&gt;
&lt;span class="c1"&gt;-- CREATE INDEX "IX__values__Long" ON _values (_Long) ...;&lt;/span&gt;
&lt;span class="c1"&gt;-- CREATE INDEX "IX__values__Guid" ON _values (_Guid) ...;&lt;/span&gt;
&lt;span class="c1"&gt;-- ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a cleanup history committed straight into the schema file. Early REDB shipped one index per typed column. Real workloads showed they never lit up: facet queries &lt;strong&gt;always&lt;/strong&gt; start from &lt;code&gt;_id_structure&lt;/code&gt;, never from a raw value column. The composite &lt;code&gt;(_id_structure, _id_object, _Long, ...)&lt;/code&gt; swallowed them whole. The single-column ones got pulled.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is fast on prod
&lt;/h2&gt;

&lt;p&gt;Architecture is half the story. The other half is the actual numbers. Here's a typical TSUM order-processing tick from production logs:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[TSUM] orders=999 routes(+2 ~30 =967) drivers(+0 ~0) vehicles(+0 ~0)
       sync=482 query=139 save=154 total=991ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[TSUM] orders=999 routes(+2 ~30 =967) drivers(+0 ~0) vehicles(+0 ~0) sync=482 query=139 save=154 total=991ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;999 orders&lt;/strong&gt; arrived in one XML batch from SAP S/4 (the stored procedure &lt;code&gt;usp_TsUM_MonitoringReport_xml&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;routes(+2 ~30 =967)&lt;/strong&gt;: of the 999 incoming, 2 were new routes, 30 changed, &lt;strong&gt;967 weren't touched at all&lt;/strong&gt; (content unchanged).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;drivers / vehicles (+0 ~0)&lt;/strong&gt;: the driver and vehicle dictionaries had no changes in this batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sync = 482 ms&lt;/strong&gt; — this is &lt;strong&gt;lookup-dictionary sync&lt;/strong&gt; (&lt;code&gt;DictionarySyncService.SyncFromOrdersAsync&lt;/code&gt;): walk all 999 orders, reconcile drivers / vehicles / shipping points / business types / delivery statuses against REDB lists, upsert any new &lt;code&gt;list_items&lt;/code&gt; if needed. &lt;strong&gt;This is not schema sync.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;query = 139 ms&lt;/strong&gt; — &lt;strong&gt;one SELECT&lt;/strong&gt; to REDB: &lt;code&gt;WhereRedb(o =&amp;gt; codes.Contains(o.ValueString))&lt;/code&gt; — pulls &lt;strong&gt;all 999&lt;/strong&gt; existing routes by their codes in a single query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;save = 154 ms&lt;/strong&gt; — &lt;code&gt;SaveAsync&lt;/code&gt; for &lt;strong&gt;32 objects&lt;/strong&gt; (2 created + 30 updated). The 967 unchanged ones never get here.&lt;/li&gt;
&lt;li&gt;Environment: &lt;strong&gt;2 cores&lt;/strong&gt;, default &lt;code&gt;shared_buffers&lt;/code&gt;, no &lt;code&gt;pg_prewarm&lt;/code&gt;, no framework-level cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Important caveat: schema sync (&lt;code&gt;SyncSchemeAsync&amp;lt;T&amp;gt;&lt;/code&gt; — the bit that compares &lt;code&gt;_structure_hash&lt;/code&gt; and refreshes &lt;code&gt;_scheme_metadata_cache&lt;/code&gt;) is a &lt;strong&gt;separate&lt;/strong&gt; step that runs once at process startup, before any orders flow. It's not in this log line at all. The "sync" number here is application-level dictionary reconciliation, not schema reconciliation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's break down what's physically happening behind each number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query: 139 ms for 999 objects in one call
&lt;/h3&gt;

&lt;p&gt;The most striking number in the log. This isn't 999 separate &lt;code&gt;LoadAsync&lt;/code&gt; calls — it's &lt;strong&gt;one&lt;/strong&gt; facet query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TransportationRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhereRedb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValueString&lt;/span&gt;&lt;span class="p"&gt;!))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It translates roughly to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- find all routes whose _value_string is in the list of 999 codes&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;routeSchemeId&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_value_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, one batched query for &lt;code&gt;_values&lt;/code&gt; of all matched objects via &lt;code&gt;IX__values__object_array_null&lt;/code&gt; (covering INCLUDE) — Index Only Scan, no heap access. Output: 999 fully hydrated &lt;code&gt;RedbObject&amp;lt;TransportationRoute&amp;gt;&lt;/code&gt; instances with all ~50 properties each, including ListItem references to dictionaries.&lt;/p&gt;

&lt;p&gt;139 ms / 999 objects = &lt;strong&gt;0.14 ms per object&lt;/strong&gt; on paper, but that math is misleading — the real cost is two SQL round trips (one for &lt;code&gt;_objects&lt;/code&gt;, one for &lt;code&gt;_values&lt;/code&gt;) plus C# hydration.&lt;/p&gt;

&lt;p&gt;No N+1. No &lt;code&gt;Include&lt;/code&gt;s. ListItem references resolve through &lt;code&gt;RefDataCache&lt;/code&gt; in memory, so &lt;code&gt;Vehicle&lt;/code&gt;, &lt;code&gt;Driver&lt;/code&gt;, &lt;code&gt;ShippingPoint&lt;/code&gt; get filled without extra SQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save: 154 ms for 32 changed objects
&lt;/h3&gt;

&lt;p&gt;Two things matter here, and both are critical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First — application-level change tracking.&lt;/strong&gt; Before &lt;code&gt;SaveAsync&lt;/code&gt;, every existing route runs through &lt;code&gt;ComputeHash()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hashBefore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ComputeHash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;EnrichRouteFromOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routeProps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ComputeHash&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;hashBefore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;toSave&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// data really changed — queue for save&lt;/span&gt;
    &lt;span class="n"&gt;updatedCount&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;skippedCount&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;   &lt;span class="c1"&gt;// byte-identical — don't touch the database&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of the 999 incoming orders, &lt;strong&gt;967 are skipped right here&lt;/strong&gt;. They never hit &lt;code&gt;_objects&lt;/code&gt;/&lt;code&gt;_values&lt;/code&gt;, never generate a single DML statement. This isn't a REDB-engine optimization — it's an application-code pattern, but REDB supports it cheaply via &lt;code&gt;ComputeHash()&lt;/code&gt; over Props.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second — bulk save in a single transaction.&lt;/strong&gt; The remaining 32 objects go in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toSave&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// toSave: List&amp;lt;IRedbObject&amp;gt;, 32 items&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffthq4shh662mytnsknty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffthq4shh662mytnsknty.png" alt="logs select and materialize from 9m properties" width="799" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's what happens inside. &lt;code&gt;_objects&lt;/code&gt; gets normalized in a batch first — for all 32 objects, one INSERT/UPDATE pass (new rows go via COPY/&lt;code&gt;UNNEST&lt;/code&gt;-INSERT, modified rows via batched UPDATE). Then &lt;code&gt;_values&lt;/code&gt;: for each modified object the value rows are recreated, and again not row-by-row but in one bulk call through Postgres COPY protocol. &lt;strong&gt;All in a single transaction&lt;/strong&gt; — &lt;code&gt;_objects&lt;/code&gt; and &lt;code&gt;_values&lt;/code&gt; commit atomically.&lt;/p&gt;

&lt;p&gt;COPY matters. It's a streaming binary protocol that ingests N rows without a per-row round trip — the driver writes a continuous byte stream into the socket. For 32 objects × ~50 properties = ~1600 value rows, that's one COPY operation instead of 1600 INSERTs. Btree indexes get their batch and update amortized.&lt;/p&gt;

&lt;p&gt;So 154 ms for 32 objects (~4.8 ms per object on full overwrite) isn't "the engine is very fast" — it's "the engine doesn't do unnecessary work." Bulk where it can bulk; early skip where data didn't change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sync: 482 ms — what it really is
&lt;/h3&gt;

&lt;p&gt;482 ms is the heaviest line in the log, and it's also the most commonly misread one. It's &lt;strong&gt;not&lt;/strong&gt; schema sync — schema sync already ran at Worker startup, before any orders showed up. The 482 ms is &lt;code&gt;DictionarySyncService.SyncFromOrdersAsync&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Five parallel dictionary loads from REDB: &lt;code&gt;Drivers&lt;/code&gt;, &lt;code&gt;Vehicles&lt;/code&gt;, &lt;code&gt;ShippingPoints&lt;/code&gt;, &lt;code&gt;BusinessTypes&lt;/code&gt;, &lt;code&gt;DeliveryStatuses&lt;/code&gt; — via &lt;code&gt;redb.ListProvider.GetListByNameWithItemsAsync(...)&lt;/code&gt;. Pulls &lt;code&gt;_lists&lt;/code&gt; plus all their &lt;code&gt;_list_items&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Walk over 999 orders: each &lt;code&gt;DriverId&lt;/code&gt;, &lt;code&gt;CarId&lt;/code&gt;, &lt;code&gt;ShippingPoint&lt;/code&gt;, &lt;code&gt;BusinessTypes&lt;/code&gt; is checked against existing list items. If new, an upsert is queued.&lt;/li&gt;
&lt;li&gt;If dictionaries changed — refresh &lt;code&gt;RefDataCache&lt;/code&gt; (a static in-memory cache so the Order → RouteProps mapping doesn't re-query the DB).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This batch had zero deltas on drivers and vehicles (&lt;code&gt;+0 ~0&lt;/code&gt;), but 482 ms still went into &lt;strong&gt;proving&lt;/strong&gt; they didn't change — that's 999 rows being checked against thousands of existing list items across multiple dictionaries.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;application-level&lt;/strong&gt; sync. It belongs to TSUM's logistics process, not REDB's engine. But it makes a useful point: even when "discovering nothing needs to change" costs more than the actual save (482 ms sync vs 154 ms save), the whole tick still fits in under a second for a thousand orders.&lt;/p&gt;

&lt;p&gt;Schema sync is a separate story. Cold start of the Worker — tens to hundreds of milliseconds, one-off (32 schemes, hash comparison). Hot restart with no model changes — single-digit milliseconds. It happens &lt;strong&gt;before&lt;/strong&gt; the first &lt;code&gt;[TSUM]&lt;/code&gt; log line and isn't counted in the per-tick budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coming back to the 9.7M-row objection
&lt;/h3&gt;

&lt;p&gt;The original objection was "how can this be fast at that table size." The answer: &lt;strong&gt;the engine never scans &lt;code&gt;_values&lt;/code&gt; whole&lt;/strong&gt;. Any query starts from one of these four anchors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PK (&lt;code&gt;LoadAsync&lt;/code&gt; by ID)&lt;/strong&gt; — O(log n) on the btree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_id_scheme&lt;/code&gt; + &lt;code&gt;_value_string&lt;/code&gt;&lt;/strong&gt; for facets through a &lt;code&gt;RedbPrimitive&lt;/code&gt; column, like the TSUM case above — Index Only Scan on &lt;code&gt;IX__objects__value_string&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_id_structure&lt;/code&gt;&lt;/strong&gt; (regular facet search) — discards ~99.75% of rows immediately. With 401 properties, the average &lt;code&gt;_id_structure&lt;/code&gt; covers ~24K of the 9.7M total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_id_object&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;LoadAsync&lt;/code&gt; — all values for an object) — discards even more aggressively, since an object has tens of values, not thousands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Table size in rows only affects the index size in pages. Postgres btree fan-out is in the hundreds, so 9.7M rows is a 4–5 level deep tree. Search is logarithmic, not linear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvz9jq71v5yv3vbv3qlo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvz9jq71v5yv3vbv3qlo2.png" alt="logs select and materialize from 9m properties" width="799" height="465"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  "You don't always need everything you've got": pruning indexes after stabilization
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;redbPostgre.sql&lt;/code&gt; carries the &lt;strong&gt;full spectrum&lt;/strong&gt; of indexes for every REDB scenario: facet search, trees, collections, GIN full-text, partial indexes for NOT NULL, indexes for recursive permission CTEs. That's right for a library: you don't know up front which workload profile a given app will have.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;in your specific app, some of those indexes may never light up.&lt;/strong&gt; That's fine. TSUM, for instance, never calls &lt;code&gt;WhereHasDescendant&lt;/code&gt; because its domain hierarchies are flat — so &lt;code&gt;IX__objects__parent_id_descendant_lookup&lt;/code&gt; sits in pages but no scan ever goes through it.&lt;/p&gt;

&lt;p&gt;After the app's been in production for two to four weeks under stable load, &lt;strong&gt;pull the index stats&lt;/strong&gt; and trim aggressively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexrelname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;idx_scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sz&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_indexes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;idx_scan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;  &lt;span class="c1"&gt;-- only indexes &amp;gt; 10 MB&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjzz7w7ayks13wwkjofqz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjzz7w7ayks13wwkjofqz.png" alt="idx_scan" width="800" height="712"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On TSUM, this query surfaces about &lt;strong&gt;4423 MB&lt;/strong&gt; of indexes with &lt;code&gt;idx_scan = 0&lt;/code&gt;. That &lt;strong&gt;doesn't&lt;/strong&gt; mean those indexes are bad. It means nobody's hitting those query paths in this workload profile.&lt;/p&gt;

&lt;p&gt;Working with the result:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't drop everything immediately.&lt;/strong&gt; First, walk the list: which indexes serve scenarios that "haven't fired yet" (quarterly reports, year-end reconciliations, big imports)? Leave those alone — re-creation later costs more than the disk you'd save now.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The rest are candidates for &lt;code&gt;DROP INDEX CONCURRENTLY&lt;/code&gt;.&lt;/strong&gt; It's safe — no table lock — and you can always restore the index with the same &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt;, since the DDL knows what shape it should have.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;After dropping, watch for two to four more weeks.&lt;/strong&gt; If no unexpectedly slow queries show up in logs, the call was right. If something regresses, restore.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a &lt;strong&gt;standard PostgreSQL practice&lt;/strong&gt;, not a REDB-specific trick. But it works cleaner in REDB because the index set is designed once and frozen in DDL. Pruning is one targeted decision in one place — not a hunt through three years of accumulated migrations across business tables.&lt;/p&gt;

&lt;p&gt;The other direction works too. If you spot a single facet query that's hot enough to warrant an extra push, nothing stops you from adding a &lt;strong&gt;local&lt;/strong&gt; index just for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- e.g., "find routes for a specific driver in the last 7 days"&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX_my_app__route_by_driver&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_ListItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_id_structure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;driver_field_id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That kind of index is application-side optimization. It lives in your app's migration, not in the REDB core. And the same &lt;code&gt;pg_stat_user_indexes&lt;/code&gt; audit applies to it after a few weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's deliberately &lt;em&gt;not&lt;/em&gt; in the DDL, and why
&lt;/h2&gt;

&lt;p&gt;A second-level skeptic asks the next question: "Why no hash index on &lt;code&gt;_String&lt;/code&gt;? Why no BRIN on &lt;code&gt;_date_create&lt;/code&gt;? Why no expression index on &lt;code&gt;LOWER(_String)&lt;/code&gt;?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hash indexes.&lt;/strong&gt; Historically weaker than btree on most metrics in Postgres, and they don't support partial conditions. Almost every REDB index is partial — so hash is structurally a non-fit, not a coin-flip choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BRIN on &lt;code&gt;_date_create&lt;/code&gt;.&lt;/strong&gt; A reasonable candidate on big archival tables, but redundant for the current REDB profile. &lt;code&gt;IX__objects__scheme_date_create (DESC, _id)&lt;/code&gt; already serves the exact scenario BRIN would compete with. BRIN would only win in a cold-storage case (&amp;gt;10M rows on a single scheme) — that's a separate "archival classes" phase, and the current core DDL doesn't address it yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expression indexes (&lt;code&gt;LOWER(_String)&lt;/code&gt;, &lt;code&gt;_DateTimeOffset::date&lt;/code&gt;, etc.).&lt;/strong&gt; Deliberately omitted. This is an &lt;strong&gt;application choice&lt;/strong&gt;: case-insensitive search on a specific text field, aggregation by day/month — these are options that belong in the app's migration, not the engine. GIN with &lt;code&gt;pg_trgm&lt;/code&gt; already provides case-insensitive substring search via ILIKE, and that covers most needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bloom, BRIN-multi, GIN on jsonb.&lt;/strong&gt; All candidates for narrow scenarios. If you have one very specific query that's provably hot and provably not covered by the standard set — you add a local index for it in a separate migration. Without touching &lt;code&gt;redbPostgre.sql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The principle is general: &lt;strong&gt;the core DDL covers 95% of scenarios, the remaining 5% lives in the application layer.&lt;/strong&gt; That's the opposite of the EF approach, where the DDL belongs to the application from day one and every index is part of its migration history.&lt;/p&gt;




&lt;h2&gt;
  
  
  MSSQL: nearly the same picture
&lt;/h2&gt;

&lt;p&gt;While we're in DDL territory, worth saying clearly: this post isn't Postgres-specific. &lt;code&gt;redb.MSSql/sql/redbMSSQL.sql&lt;/code&gt; carries a mirror set of indexes, and the architectural trick — "indexes don't grow with class count" — works identically on both engines.&lt;/p&gt;

&lt;p&gt;Ports cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All three partial-unique indexes on &lt;code&gt;_values&lt;/code&gt; (in MSSQL — filtered indexes with &lt;code&gt;WHERE&lt;/code&gt;, different syntax, same semantics).&lt;/li&gt;
&lt;li&gt;Composite indexes for facet search (&lt;code&gt;IX__values__structure_object_lookup&lt;/code&gt; exists in both DDLs with an identical key shape).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INCLUDE&lt;/code&gt; covering indexes — natively supported in MSSQL since 2005, port one-to-one.&lt;/li&gt;
&lt;li&gt;Object-tree indexes (&lt;code&gt;IX__objects__scheme_parent&lt;/code&gt;, &lt;code&gt;IX__objects__parent_id_descendant_lookup&lt;/code&gt;) — unchanged.&lt;/li&gt;
&lt;li&gt;Partial / filtered indexes for &lt;code&gt;RedbPrimitive&amp;lt;T&amp;gt;&lt;/code&gt; — unchanged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Differs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GIN + &lt;code&gt;pg_trgm&lt;/code&gt;&lt;/strong&gt; for substring search on &lt;code&gt;_String&lt;/code&gt;. The MSSQL counterpart is full-text search or a filtered index over a computed column. &lt;code&gt;redbMSSQL.sql&lt;/code&gt; takes a different path (&lt;code&gt;LIKE&lt;/code&gt; over a regular btree with a length-bound partial).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NULLS NOT DISTINCT&lt;/code&gt;&lt;/strong&gt; in Postgres 15+ — MSSQL spells it differently (&lt;code&gt;UNIQUE&lt;/code&gt; + filtered &lt;code&gt;WHERE NOT NULL&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ON DELETE CASCADE&lt;/code&gt; from &lt;code&gt;_structures&lt;/code&gt; to &lt;code&gt;_values&lt;/code&gt;&lt;/strong&gt; works directly in Postgres but not in MSSQL because of multiple cascade paths — replaced with an &lt;code&gt;INSTEAD OF DELETE&lt;/code&gt; trigger. The DELETE plan is slightly different in MSSQL but the indexes are the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-type filtered indexes&lt;/strong&gt; are spelled out more explicitly in MSSQL (&lt;code&gt;IX__values__Long_filter&lt;/code&gt;, &lt;code&gt;IX__values__Guid_filter&lt;/code&gt;, ...) — in the Postgres DDL they're commented out as "covered by composite." That's a strategy difference: the MSSQL optimizer is a bit more partial to narrow filtered indexes per type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the main point — &lt;strong&gt;architectural invariance is preserved&lt;/strong&gt;. Same 2+2+3+infrastructure layout, same facet schema, same fact that the index set doesn't grow when you add a class. To a developer writing C# models, the difference between Postgres and MSSQL is invisible.&lt;/p&gt;




&lt;h2&gt;
  
  
  TSUM as the proof case
&lt;/h2&gt;

&lt;p&gt;Bringing the numbers from the top of the post into one place.&lt;/p&gt;

&lt;p&gt;What REDB serves in TSUM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;32 classes in &lt;code&gt;tsum.Domain&lt;/code&gt; (from &lt;code&gt;TransportationRoute&lt;/code&gt; to &lt;code&gt;UserFilterPreference&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;401 properties across those classes,&lt;/li&gt;
&lt;li&gt;227,896 objects at the time these metrics were captured,&lt;/li&gt;
&lt;li&gt;9,773,174 rows in &lt;code&gt;_values&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This whole schema lands not in 13 tables per entity, but in &lt;strong&gt;2 data tables&lt;/strong&gt; (&lt;code&gt;_objects&lt;/code&gt; + &lt;code&gt;_values&lt;/code&gt;) + &lt;strong&gt;2 lookup tables&lt;/strong&gt; (&lt;code&gt;_lists&lt;/code&gt; + &lt;code&gt;_list_items&lt;/code&gt;) + &lt;strong&gt;3 RTTI tables&lt;/strong&gt; (&lt;code&gt;_types&lt;/code&gt; + &lt;code&gt;_schemes&lt;/code&gt; + &lt;code&gt;_structures&lt;/code&gt;). The remaining six are infrastructure — permissions, users, roles, links, dependencies, functions, soft-delete, metadata cache — invariant of business class count. The full index set is the same &lt;code&gt;redbPostgre.sql&lt;/code&gt; you'd ship for a five-class project. In a classical EF layout, those 32 classes with their lookups and snapshots would expand to &lt;strong&gt;roughly 60–80 tables&lt;/strong&gt; and &lt;strong&gt;200+ indexes&lt;/strong&gt;, with a migration history accumulating across the project's lifetime.&lt;/p&gt;

&lt;p&gt;Production metrics for a typical order-processing tick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;999 orders in one XML batch from SAP&lt;/strong&gt;, processed in &lt;strong&gt;991 ms&lt;/strong&gt; total:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sync = 482 ms&lt;/code&gt; — reconciling lookup dictionaries (Drivers, Vehicles, ShippingPoints, BusinessTypes, DeliveryStatuses) against REDB lists,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;query = 139 ms&lt;/code&gt; — &lt;strong&gt;one&lt;/strong&gt; SELECT loading all 999 existing routes by their codes,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;save = 154 ms&lt;/code&gt; — &lt;code&gt;SaveAsync&lt;/code&gt; for &lt;strong&gt;32 changed&lt;/strong&gt; objects (2 created + 30 updated). The other 967 are short-circuited by &lt;code&gt;ComputeHash()&lt;/code&gt; before they reach the database.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Infrastructure — &lt;strong&gt;2 cores&lt;/strong&gt;, &lt;strong&gt;stock PostgreSQL settings&lt;/strong&gt; (no &lt;code&gt;shared_buffers&lt;/code&gt; tuning, no &lt;code&gt;pg_prewarm&lt;/code&gt;, no bumped &lt;code&gt;work_mem&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;No REDB-framework-level cache — every &lt;code&gt;Query&amp;lt;T&amp;gt;()&lt;/code&gt; round-trips to the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a hand-picked benchmark — it's a real production tick where a developer writes ordinary C# models with a &lt;code&gt;[RedbScheme]&lt;/code&gt; attribute and the logistics process just runs. Bulk on the read (one SELECT for 999 objects), bulk on the write (&lt;code&gt;_objects&lt;/code&gt; and &lt;code&gt;_values&lt;/code&gt; together in one transaction over the COPY protocol), early skip where data didn't change.&lt;/p&gt;

&lt;p&gt;The technical takeaway: &lt;strong&gt;REDB hits these numbers not through magic, but because the engine doesn't do unnecessary work&lt;/strong&gt;. The index plan is baked into the storage schema; bulk runs where bulk applies; application-level change tracking saves the most expensive thing — DML for changes that aren't there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrap-up and what's coming next
&lt;/h2&gt;

&lt;p&gt;Short summary. REDB ships &lt;strong&gt;one&lt;/strong&gt; index set in DDL — about 50 indexes for the entire system. That set serves any business schema: 32 classes, 100 classes, 500 classes — same indexes. That's the inverse of the EF approach, where indexes multiply with tables and slowly become technical debt that needs periodic pruning.&lt;/p&gt;

&lt;p&gt;Operational pattern — once your application stabilizes, run &lt;code&gt;pg_stat_user_indexes&lt;/code&gt;, find the &lt;code&gt;idx_scan = 0&lt;/code&gt; candidates, and prune carefully (with &lt;code&gt;DROP INDEX CONCURRENTLY&lt;/code&gt; for safe rollback). Add your own local indexes for hot application-specific queries — in a separate migration, leaving the engine DDL alone.&lt;/p&gt;

&lt;p&gt;MSSQL is nearly the same picture. Minor differences in text indexing and per-type filtered indexes, but architectural invariance carries over.&lt;/p&gt;

&lt;p&gt;Coming up in the series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt; — Code-first schemes: how &lt;code&gt;SyncSchemeAsync&amp;lt;T&amp;gt;&lt;/code&gt; turns a C# class into rows in &lt;code&gt;_schemes&lt;/code&gt; + &lt;code&gt;_structures&lt;/code&gt;, what &lt;code&gt;_structure_hash&lt;/code&gt; is, how automatic onboarding works when you add a new property.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt; — CRUD internals: &lt;code&gt;SaveAsync&lt;/code&gt; and &lt;code&gt;LoadAsync&lt;/code&gt; from inside, change tracking via TreeDiff, COPY-protocol bulk insert, lazy loading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4&lt;/strong&gt; — LINQ to SQL: how &lt;code&gt;Where(x =&amp;gt; x.Salary &amp;gt; 80000)&lt;/code&gt; becomes &lt;code&gt;CASE WHEN _id_structure = X THEN _Long END &amp;gt; 80000&lt;/code&gt;, how &lt;code&gt;OrderBy&lt;/code&gt; and window functions work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 5&lt;/strong&gt; — Object trees: &lt;code&gt;LoadTreeAsync&lt;/code&gt;, &lt;code&gt;GetDescendantsAsync&lt;/code&gt;, &lt;code&gt;WhereHasAncestor&lt;/code&gt;, recursive CTEs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 6&lt;/strong&gt; — Window functions: &lt;code&gt;Win.RowNumber()&lt;/code&gt;, &lt;code&gt;Win.Rank()&lt;/code&gt;, &lt;code&gt;PartitionBy/OrderBy&lt;/code&gt; over REDB objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deep C# dive is parts 3–5. In the storage layer, one topic remains — schema migrations — and that's where the next post picks up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/redbase-app" rel="noopener noreferrer"&gt;GitHub: redbase-app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/redbase-app/redb" rel="noopener noreferrer"&gt;redb.Core repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/redbase-app/redb/blob/main/redb.Postgres/sql/redbPostgre.sql" rel="noopener noreferrer"&gt;Postgres DDL (redbPostgre.sql)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/redbase-app/redb/blob/main/redb.MSSql/sql/redbMSSQL.sql" rel="noopener noreferrer"&gt;MSSQL DDL (redbMSSQL.sql)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://redbase.app/" rel="noopener noreferrer"&gt;Docs and samples (EN)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Earlier in the series (on dev.to)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/two-tables-zero-migrations-full-linq-a-net-data-engine-thats-been-running-our-production-for-2482"&gt;An EF Core alternative for .NET apps with complex object graphs — full LINQ, no migrations, no DbContext&lt;/a&gt; — the wide-angle intro to RedBase: LINQ surface, production story, generated SQL.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redb-inside-part-1-the-13-tables-the-whole-engine-runs-on-with-the-actual-sql-and-why-its-not-18mf"&gt;REDB inside, part 1 — the 13 tables the whole engine runs on (with the actual SQL, and why it's not EAV)&lt;/a&gt; — the schema walkthrough this post builds on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/redbroute-301-flat-dsl-navigation-crtp-refactor-and-a-silent-null-fix-3m7n"&gt;redb.Route 3.0.1 — flat DSL navigation, CRTP refactor, and a silent null fix&lt;/a&gt; — separate cycle on the ESB framework that sits on top of redb.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>postgres</category>
      <category>sql</category>
      <category>opensource</category>
    </item>
    <item>
      <title>REDB inside, part 1 — the 13 tables the whole engine runs on (with the actual SQL, and why it's not EAV)</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Thu, 04 Jun 2026 18:49:10 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redb-inside-part-1-the-13-tables-the-whole-engine-runs-on-with-the-actual-sql-and-why-its-not-18mf</link>
      <guid>https://dev.to/rinat_kozin/redb-inside-part-1-the-13-tables-the-whole-engine-runs-on-with-the-actual-sql-and-why-its-not-18mf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4axqh7txqeiopwarxhkd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4axqh7txqeiopwarxhkd.png" alt="REDB SQL"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A couple of weeks ago I published &lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/two-tables-zero-migrations-full-linq-a-net-data-engine-thats-been-running-our-production-for-2482"&gt;the redb.Core intro post&lt;/a&gt; — what RedBase is at the API level, why I wrote it, what production looks like, the LINQ surface, what generated SQL looks like for nested dictionary lookups. If you haven't read it, start there — it's the wide-angle shot.&lt;/p&gt;

&lt;p&gt;This post starts a new series — &lt;strong&gt;"REDB inside"&lt;/strong&gt; — that drills down into the engine. One article per layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1 (this post) — the database schema. 13 tables, what each one does, why the design is what it is, and the SQL you'd run to dump any object flat.&lt;/li&gt;
&lt;li&gt;Part 2 — code-first schemes. How &lt;code&gt;SyncSchemeAsync&amp;lt;T&amp;gt;&lt;/code&gt; walks a C# class and turns it into rows in &lt;code&gt;_schemes&lt;/code&gt; + &lt;code&gt;_structures&lt;/code&gt;, the &lt;code&gt;_structure_hash&lt;/code&gt; mechanism, automatic onboarding via &lt;code&gt;InitializeAsync&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Part 3 — CRUD internals. &lt;code&gt;SaveAsync&lt;/code&gt;, &lt;code&gt;LoadAsync&lt;/code&gt;, the TreeDiff change-tracking algorithm, COPY-protocol bulk insert, lazy loading.&lt;/li&gt;
&lt;li&gt;Part 4 — LINQ-to-SQL. How &lt;code&gt;Where(x =&amp;gt; x.Salary &amp;gt; 80000)&lt;/code&gt; becomes &lt;code&gt;WHERE _id_structure = X AND _Long &amp;gt; 80000&lt;/code&gt;, the pivot CTE patterns, dialect differences (Postgres &lt;code&gt;array_agg FILTER&lt;/code&gt; vs MSSQL &lt;code&gt;MAX CASE WHEN&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Part 5 — trees. &lt;code&gt;LoadTreeAsync&lt;/code&gt;, &lt;code&gt;GetDescendantsAsync&lt;/code&gt;, &lt;code&gt;WhereHasAncestor&lt;/code&gt;, closure-table vs recursive CTE.&lt;/li&gt;
&lt;li&gt;Part 6 — window functions. &lt;code&gt;Win.RowNumber()&lt;/code&gt;, &lt;code&gt;Win.Rank()&lt;/code&gt;, &lt;code&gt;PartitionBy&lt;/code&gt;/&lt;code&gt;OrderBy&lt;/code&gt; over REDB objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one stands alone. You don't need to read them in order — but if you want to understand why anything in parts 2-6 works the way it does, &lt;strong&gt;you need this post&lt;/strong&gt;. Everything else is built on the 13 tables.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The whole RedBase engine runs on 13 tables. No JSON blob hiding the schema, no &lt;code&gt;NVARCHAR(MAX)&lt;/code&gt; catch-all column — every C# type lands in its own typed column. Let me show you how that works, and why this isn't classical EAV even though it kind of looks like it at first glance.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  "Wait, isn't this just EAV?"
&lt;/h2&gt;

&lt;p&gt;It's the most common reaction I get, and it deserves a real answer before we look at any DDL.&lt;/p&gt;

&lt;p&gt;Classical EAV (Entity–Attribute–Value) looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;object_id | attribute_name | value
----------|----------------|---------
42        | "FirstName"    | "Alice"
42        | "Age"          | "28"
42        | "Salary"       | "85000"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything in one table. Types erased. Attribute names are strings. Any non-trivial query becomes a self-join or a giant &lt;code&gt;PIVOT&lt;/code&gt;. The filter "employees earning over $80k" turns into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;object_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;attributes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;attribute_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Salary'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- runtime cast, no usable index&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add three more conditions and you're looking at three self-joins on the same table. Explain plans get embarrassing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REDB doesn't do that.&lt;/strong&gt; Here's what &lt;code&gt;_values&lt;/code&gt; actually looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_id&lt;/span&gt;              &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_structure&lt;/span&gt;    &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- FK → _structures (which field this is)&lt;/span&gt;
    &lt;span class="n"&gt;_id_object&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- FK → _objects&lt;/span&gt;
    &lt;span class="c1"&gt;-- typed value columns, exactly one is non-NULL per row:&lt;/span&gt;
    &lt;span class="n"&gt;_String&lt;/span&gt;          &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Long&lt;/span&gt;            &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Guid&lt;/span&gt;            &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Double&lt;/span&gt;          &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Boolean&lt;/span&gt;         &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_ByteArray&lt;/span&gt;       &lt;span class="n"&gt;bytea&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_Numeric&lt;/span&gt;         &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_ListItem&lt;/span&gt;        &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- FK → _list_items&lt;/span&gt;
    &lt;span class="n"&gt;_Object&lt;/span&gt;          &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- FK → _objects (cross-object reference)&lt;/span&gt;
    &lt;span class="c1"&gt;-- relational collection storage:&lt;/span&gt;
    &lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- FK → _values (parent element)&lt;/span&gt;
    &lt;span class="n"&gt;_array_index&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;          &lt;span class="c1"&gt;-- '0','1','2' for arrays, key for dictionaries&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field identity is a &lt;strong&gt;foreign key&lt;/strong&gt; (&lt;code&gt;_id_structure&lt;/code&gt;), not a string. The value lives in a &lt;strong&gt;typed column&lt;/strong&gt; chosen at write time based on the field's declared C# type. Think of it as &lt;strong&gt;runtime type information (RTTI)&lt;/strong&gt; persisted into the schema: the engine always knows what type each field is, because that's recorded once in &lt;code&gt;_structures._id_type&lt;/code&gt; and reused for every value of that field.&lt;/p&gt;

&lt;p&gt;Reading values back is one CASE expression dispatched on type, not a self-join:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_type&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'String'&lt;/span&gt;  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_String&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Long'&lt;/span&gt;    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Long&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Guid'&lt;/span&gt;    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Guid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical differences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Classical EAV&lt;/th&gt;
&lt;th&gt;REDB &lt;code&gt;_values&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Field identity&lt;/td&gt;
&lt;td&gt;string in &lt;code&gt;attribute_name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;FK → &lt;code&gt;_structures&lt;/code&gt; → &lt;code&gt;_types&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where the value lives&lt;/td&gt;
&lt;td&gt;one &lt;code&gt;text&lt;/code&gt;/&lt;code&gt;varchar(max)&lt;/code&gt; column&lt;/td&gt;
&lt;td&gt;typed column per C# type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter &lt;code&gt;Salary &amp;gt; 80000&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WHERE attribute_name='Salary' AND value::numeric &amp;gt; 80000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WHERE _id_structure = $1 AND _Long &amp;gt; 80000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index on the value&lt;/td&gt;
&lt;td&gt;string index + runtime cast&lt;/td&gt;
&lt;td&gt;partial B-tree index on the typed column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arrays/dictionaries&lt;/td&gt;
&lt;td&gt;separate table or JSON blob&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_array_parent_id&lt;/code&gt; + &lt;code&gt;_array_index&lt;/code&gt; in the same row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema metadata&lt;/td&gt;
&lt;td&gt;implicit in attribute names&lt;/td&gt;
&lt;td&gt;first-class rows in &lt;code&gt;_schemes&lt;/code&gt;/&lt;code&gt;_structures&lt;/code&gt;/&lt;code&gt;_types&lt;/code&gt;, denormalized into a metadata cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So yes, the row shape rhymes with EAV — but the type system and the indexing story are completely different. That's why I've been pushing back on the EAV label.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 13 tables, at a glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_types          — type catalog (~37 system rows)
_schemes        — schemes (C# classes mapped to DB rows)
_structures     — fields of schemes (with nesting and collection metadata)
_objects        — objects (data rows, tree-shaped via self-FK)
_values         — field values (typed columns + relational collections)
_lists          — pick-list/dictionary catalog
_list_items     — pick-list entries
_users          — users (system IDs −1, 0, 1)
_roles          — roles
_users_roles    — M2M user ↔ role
_permissions    — permissions on objects (inherited along the tree)
_links          — M2M relations between objects
_functions      — stored expressions attached to schemes
_dependencies   — cross-scheme dependencies
─────────────────────────────────────────────
_scheme_metadata_cache   — denormalized cache of structures × types
_migrations              — history of props-schema migrations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last two live in their own SQL files but matter just as much in practice. Let's walk through them layer by layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1 — the type catalog: &lt;code&gt;_types&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_types&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_id&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_name&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_db_type&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- which _values column to use: 'Long', 'String', 'Guid', ...&lt;/span&gt;
    &lt;span class="n"&gt;_type&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;    &lt;span class="c1"&gt;-- C# type name: 'long', 'string', 'Guid', ...&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;System type IDs are &lt;strong&gt;negative constants&lt;/strong&gt; near &lt;code&gt;long.MinValue&lt;/code&gt;. A small sample:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;_id&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;_name&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;_db_type&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;C# type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-9223372036854775709&lt;/td&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;Boolean&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-9223372036854775708&lt;/td&gt;
&lt;td&gt;DateTime&lt;/td&gt;
&lt;td&gt;DateTimeOffset&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DateTime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-9223372036854775704&lt;/td&gt;
&lt;td&gt;Long&lt;/td&gt;
&lt;td&gt;Long&lt;/td&gt;
&lt;td&gt;&lt;code&gt;long&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-9223372036854775700&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-9223372036854775695&lt;/td&gt;
&lt;td&gt;Decimal&lt;/td&gt;
&lt;td&gt;Numeric&lt;/td&gt;
&lt;td&gt;&lt;code&gt;decimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-9223372036854775675&lt;/td&gt;
&lt;td&gt;Class&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;nested class (marker only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-9223372036854775668&lt;/td&gt;
&lt;td&gt;Array&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;T[]&lt;/code&gt; / &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; (marker only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-9223372036854775667&lt;/td&gt;
&lt;td&gt;Dictionary&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dictionary&amp;lt;K,V&amp;gt;&lt;/code&gt; (marker only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scary-looking numbers are just constants picked far from anything a user-generated key could ever hit (user IDs start at &lt;code&gt;1_000_000&lt;/code&gt; via a &lt;code&gt;global_identity&lt;/code&gt; sequence). &lt;code&gt;Class&lt;/code&gt;, &lt;code&gt;Array&lt;/code&gt;, and &lt;code&gt;Dictionary&lt;/code&gt; have &lt;strong&gt;no column of their own&lt;/strong&gt; in &lt;code&gt;_values&lt;/code&gt; — they're marker types; the actual leaves live in child rows.&lt;/p&gt;

&lt;p&gt;There are ~37 built-in types total. Numeric ones (&lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Short&lt;/code&gt;, &lt;code&gt;Byte&lt;/code&gt;, &lt;code&gt;Float&lt;/code&gt;) physically store as &lt;code&gt;Long&lt;/code&gt;/&lt;code&gt;Double&lt;/code&gt;. Strings include semantic variants (&lt;code&gt;Email&lt;/code&gt;, &lt;code&gt;Url&lt;/code&gt;, &lt;code&gt;Phone&lt;/code&gt;) that all use &lt;code&gt;_String&lt;/code&gt;. Then &lt;code&gt;DateOnly&lt;/code&gt;/&lt;code&gt;TimeOnly&lt;/code&gt;/&lt;code&gt;TimeSpan&lt;/code&gt;, geo (&lt;code&gt;Latitude&lt;/code&gt;/&lt;code&gt;Longitude&lt;/code&gt;), file metadata (&lt;code&gt;FilePath&lt;/code&gt;/&lt;code&gt;MimeType&lt;/code&gt;), &lt;code&gt;Enum&lt;/code&gt;/&lt;code&gt;EnumInt&lt;/code&gt;, and collection markers (&lt;code&gt;Array&lt;/code&gt;/&lt;code&gt;Dictionary&lt;/code&gt;/&lt;code&gt;JsonDocument&lt;/code&gt;/&lt;code&gt;XDocument&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2 — schemes and fields: &lt;code&gt;_schemes&lt;/code&gt; + &lt;code&gt;_structures&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_schemes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_id&lt;/span&gt;             &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_parent&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- nesting (namespaces)&lt;/span&gt;
    &lt;span class="n"&gt;_name&lt;/span&gt;           &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- e.g. 'MyApp.Models.EmployeeProps'&lt;/span&gt;
    &lt;span class="n"&gt;_alias&lt;/span&gt;          &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_structure_hash&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;-- field hash for fast change detection&lt;/span&gt;
    &lt;span class="n"&gt;_type&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;       &lt;span class="c1"&gt;-- FK → _types (Class by default)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_structures&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_id&lt;/span&gt;              &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_parent&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- nested props class&lt;/span&gt;
    &lt;span class="n"&gt;_id_scheme&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- FK → _schemes&lt;/span&gt;
    &lt;span class="n"&gt;_id_type&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- FK → _types&lt;/span&gt;
    &lt;span class="n"&gt;_id_list&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- FK → _lists (for ListItem fields)&lt;/span&gt;
    &lt;span class="n"&gt;_name&lt;/span&gt;            &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- C# property name&lt;/span&gt;
    &lt;span class="n"&gt;_alias&lt;/span&gt;           &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_order&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_collection_type&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- NULL=scalar, Array_ID or Dictionary_ID&lt;/span&gt;
    &lt;span class="n"&gt;_key_type&lt;/span&gt;        &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;-- key type for Dictionary&amp;lt;K,V&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;_readonly&lt;/span&gt;        &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_allow_not_null&lt;/span&gt;  &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_is_compress&lt;/span&gt;     &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_store_null&lt;/span&gt;      &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_default_value&lt;/span&gt;   &lt;span class="n"&gt;bytea&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RedbScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Employee"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmployeeProps&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt;            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="n"&gt;Age&lt;/span&gt;                  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Salary&lt;/span&gt;              &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]?&lt;/span&gt; &lt;span class="n"&gt;Skills&lt;/span&gt;            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;HomeAddress&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// nested class&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and &lt;code&gt;await redb.SyncSchemeAsync&amp;lt;EmployeeProps&amp;gt;()&lt;/code&gt; fires for the first time, the engine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inserts a row into &lt;code&gt;_schemes&lt;/code&gt; with &lt;code&gt;_name = "MyApp.EmployeeProps"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inserts one &lt;code&gt;_structures&lt;/code&gt; row per public property.&lt;/li&gt;
&lt;li&gt;For &lt;code&gt;Skills&lt;/code&gt;: sets &lt;code&gt;_collection_type = Array_ID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For &lt;code&gt;HomeAddress&lt;/code&gt;: sets &lt;code&gt;_id_type = Class_ID&lt;/code&gt; and recursively creates child &lt;code&gt;_structures&lt;/code&gt; rows whose &lt;code&gt;_id_parent&lt;/code&gt; points back at the parent structure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then it computes a hash over all those structures and writes it to &lt;code&gt;_schemes._structure_hash&lt;/code&gt;. Next time you call sync, comparing one UUID tells the engine whether anything actually changed — no row-by-row diff needed.&lt;/p&gt;

&lt;p&gt;There's a &lt;strong&gt;DB-level trigger&lt;/strong&gt; that validates field names: no system-reserved (&lt;code&gt;_id&lt;/code&gt;, &lt;code&gt;_name&lt;/code&gt;, &lt;code&gt;_date_create&lt;/code&gt;), no C# keywords (&lt;code&gt;class&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;), no leading digits. If you accidentally try to name a property &lt;code&gt;int&lt;/code&gt;, the INSERT throws before the bad row ever lands.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3 — objects: &lt;code&gt;_objects&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_id&lt;/span&gt;             &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_parent&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- tree parent (self-FK)&lt;/span&gt;
    &lt;span class="n"&gt;_id_scheme&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- FK → _schemes&lt;/span&gt;
    &lt;span class="n"&gt;_id_owner&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- FK → _users&lt;/span&gt;
    &lt;span class="n"&gt;_id_who_change&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- FK → _users&lt;/span&gt;
    &lt;span class="n"&gt;_date_create&lt;/span&gt;    &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_date_modify&lt;/span&gt;    &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_date_begin&lt;/span&gt;     &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_date_complete&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_key&lt;/span&gt;            &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_name&lt;/span&gt;           &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_note&lt;/span&gt;           &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_hash&lt;/span&gt;           &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;-- value columns for RedbPrimitive&amp;lt;T&amp;gt;:&lt;/span&gt;
    &lt;span class="n"&gt;_value_long&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_string&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_guid&lt;/span&gt;     &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_bool&lt;/span&gt;     &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_double&lt;/span&gt;   &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_numeric&lt;/span&gt;  &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_datetime&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_value_bytes&lt;/span&gt;    &lt;span class="n"&gt;bytea&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth pointing out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tree via &lt;code&gt;_id_parent&lt;/code&gt;&lt;/strong&gt; is &lt;code&gt;ON DELETE CASCADE&lt;/code&gt;. Drop a root, the whole subtree goes with it. Depth is unbounded. This is the &lt;strong&gt;primary organizational structure&lt;/strong&gt; in REDB: sections, categories, folders, org charts, project trees — they're all just &lt;code&gt;_objects&lt;/code&gt; rows pointing at a parent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;_value_*&lt;/code&gt; columns&lt;/strong&gt; are for &lt;code&gt;RedbPrimitive&amp;lt;T&amp;gt;&lt;/code&gt;. When an object is conceptually a single primitive (e.g. &lt;code&gt;RedbObject&amp;lt;long&amp;gt;&lt;/code&gt; for a counter, &lt;code&gt;RedbObject&amp;lt;string&amp;gt;&lt;/code&gt; for a token), there's no need to spin up &lt;code&gt;_values&lt;/code&gt; rows — the value rides in the object row itself. Eight columns, one per &lt;code&gt;_db_type&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Soft delete&lt;/strong&gt; is a scheme called &lt;code&gt;@@__deleted&lt;/code&gt; (&lt;code&gt;_id = -10&lt;/code&gt;). &lt;code&gt;mark_for_deletion()&lt;/code&gt; walks the subtree via recursive CTE and atomically reparents everything under a trash container with &lt;code&gt;_id_scheme = -10&lt;/code&gt;. Actual physical deletion is a separate, batched &lt;code&gt;purge_trash()&lt;/code&gt;. This means you can offer "undelete" cheaply, and your data lake/CDC tools never see a destructive delete on the hot path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 4 — the values: &lt;code&gt;_values&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the table that earns its keep. Everything else exists to make this one fast and consistent.&lt;/p&gt;

&lt;p&gt;The DDL was up top. The interesting part is &lt;strong&gt;how collections fit into a flat row layout&lt;/strong&gt; without a side table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalar field
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Age = 28&lt;/code&gt; produces exactly one row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_id_structure=struct_Age   _id_object=42   _Long=28   _array_parent_id=NULL   _array_index=NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Array of primitives
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Skills = ["C#", "SQL", "React"]&lt;/code&gt; produces a &lt;strong&gt;marker row&lt;/strong&gt; plus one row per element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- marker: "the array property exists" (without it, the property is NULL, not [])&lt;/span&gt;
&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;   &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;struct_Skills&lt;/span&gt;   &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;   &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;   &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;

&lt;span class="c1"&gt;-- elements; _array_parent_id points at the marker; _array_index is the position&lt;/span&gt;
&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;   &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;struct_Skills&lt;/span&gt;   &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;   &lt;span class="n"&gt;_String&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"C#"&lt;/span&gt;     &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'0'&lt;/span&gt;   &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt;   &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;struct_Skills&lt;/span&gt;   &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;   &lt;span class="n"&gt;_String&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"SQL"&lt;/span&gt;    &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'1'&lt;/span&gt;   &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;103&lt;/span&gt;   &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;struct_Skills&lt;/span&gt;   &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;   &lt;span class="n"&gt;_String&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"React"&lt;/span&gt;  &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2'&lt;/span&gt;   &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That marker row matters: it's how the engine tells &lt;code&gt;null&lt;/code&gt; (no marker, no elements) apart from &lt;code&gt;[]&lt;/code&gt; (marker present, zero children). The same shape works for empty dictionaries too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dictionary
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PhoneDir = { "home": "+7 999…", "work": "+7 495…" }&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- marker&lt;/span&gt;
&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;   &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;struct_PhoneDir&lt;/span&gt;   &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;   &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;   &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;

&lt;span class="c1"&gt;-- entries; _array_index holds the dictionary key (as text)&lt;/span&gt;
&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;   &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;struct_PhoneDir&lt;/span&gt;   &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;   &lt;span class="n"&gt;_String&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"+7 999..."&lt;/span&gt;   &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'home'&lt;/span&gt;   &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="n"&gt;_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;   &lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;struct_PhoneDir&lt;/span&gt;   &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;   &lt;span class="n"&gt;_String&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;"+7 495..."&lt;/span&gt;   &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'work'&lt;/span&gt;   &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_array_index&lt;/code&gt; is &lt;code&gt;text&lt;/code&gt; precisely so dictionaries with string keys work without a separate table. Numeric dictionaries store keys as their string representation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nested class
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;HomeAddress.City = "Moscow"&lt;/code&gt; works the same way. The &lt;code&gt;_structures&lt;/code&gt; rows for &lt;code&gt;Address.City&lt;/code&gt;, &lt;code&gt;Address.Street&lt;/code&gt;, etc. carry an &lt;code&gt;_id_parent&lt;/code&gt; pointing at the parent structure (&lt;code&gt;HomeAddress&lt;/code&gt;). The &lt;code&gt;_values&lt;/code&gt; rows for those leaves carry an &lt;code&gt;_array_parent_id&lt;/code&gt; pointing at the marker row for &lt;code&gt;HomeAddress&lt;/code&gt; on this particular object.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three unique indexes keep all of this consistent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;UIX__values__structure_object&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;UIX__values__structure_object_parent&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;UIX__values__structure_object_array_index&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These three together guarantee: (a) at most one scalar/marker row per &lt;code&gt;(structure, object)&lt;/code&gt;, (b) at most one nested-class marker per &lt;code&gt;(structure, object, parent)&lt;/code&gt;, and (c) at most one element per &lt;code&gt;(structure, object, parent, index)&lt;/code&gt;. Try to insert a duplicate array element and the DB rejects it before any logic bug can corrupt the shape.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 5 — permissions: &lt;code&gt;_permissions&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_permissions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_id&lt;/span&gt;      &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_role&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- XOR with _id_user (CHECK constraint)&lt;/span&gt;
    &lt;span class="n"&gt;_id_user&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_id_ref&lt;/span&gt;  &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- 0 = global, otherwise FK → _objects&lt;/span&gt;
    &lt;span class="n"&gt;_select&lt;/span&gt;  &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_insert&lt;/span&gt;  &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_update&lt;/span&gt;  &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_delete&lt;/span&gt;  &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permissions inherit along the object tree. A recursive CTE walks &lt;strong&gt;upwards from the target object&lt;/strong&gt; up to 50 levels looking for the nearest ancestor that has an explicit permission row. &lt;code&gt;_id_ref = 0&lt;/code&gt; is the global fallback ("can this principal touch anything at all?"). Resolution priority is: user &amp;gt; role, specific object &amp;gt; global.&lt;/p&gt;

&lt;p&gt;There's an automatic trigger that, when a child object is created without its own permission row, &lt;strong&gt;copies down&lt;/strong&gt; the resolved permission from the nearest ancestor. The point isn't to materialize every permission — it's to keep the recursive CTE short. After a few months of activity the depth the resolver has to climb stays roughly constant.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;_scheme_metadata_cache&lt;/code&gt; — why a cache table
&lt;/h2&gt;

&lt;p&gt;Every query needs to know: "for an object with &lt;code&gt;_id_scheme = X&lt;/code&gt;, which &lt;code&gt;_structures&lt;/code&gt; rows exist, and what's the type of each?" That's a JOIN through &lt;code&gt;_structures → _types&lt;/code&gt; that would otherwise fire &lt;strong&gt;on every single read&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So that JOIN is denormalized into a separate table that gets refreshed when the scheme changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;_scheme_metadata_cache&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_scheme_id&lt;/span&gt;           &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_structure_id&lt;/span&gt;        &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_parent_structure_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_name&lt;/span&gt;                &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;type_name&lt;/span&gt;            &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- 'Long', 'String', 'Guid', ...&lt;/span&gt;
    &lt;span class="n"&gt;db_type&lt;/span&gt;              &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- 'Long', 'String', 'Guid', ...&lt;/span&gt;
    &lt;span class="n"&gt;type_semantic&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- 'Object', '_RObject', 'Array', ...&lt;/span&gt;
    &lt;span class="n"&gt;_collection_type&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;collection_type_name&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_key_type&lt;/span&gt;            &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;key_type_name&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;
    &lt;span class="c1"&gt;-- ... all the other _structures attributes inlined&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A trigger on &lt;code&gt;_schemes._structure_hash&lt;/code&gt; invalidates the cache for that scheme; on the next read, &lt;code&gt;sync_metadata_cache_for_scheme(scheme_id)&lt;/code&gt; rebuilds it lazily. The big &lt;code&gt;build_hierarchical_properties_optimized()&lt;/code&gt; function — the one that materializes an object's full property tree into JSON — never JOINs &lt;code&gt;_structures&lt;/code&gt; or &lt;code&gt;_types&lt;/code&gt; directly. It reads from this cache, and only from this cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two SQL queries: dump any object flat
&lt;/h2&gt;

&lt;p&gt;Here are two queries that show exactly what's in the box for a given object. The first uses raw JOINs (use this for ad-hoc debugging in DataGrip/SSMS). The second uses &lt;code&gt;_scheme_metadata_cache&lt;/code&gt; — what the engine actually runs at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1 — flat pivot, no cache
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;                                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;scheme_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;                                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;type_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_db_type&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'String'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_String&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Long'&lt;/span&gt;           &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Long&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Guid'&lt;/span&gt;           &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Guid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Double'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Double&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Boolean'&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Boolean&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'DateTimeOffset'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Numeric'&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Numeric&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'ListItem'&lt;/span&gt;       &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ListItem&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Object'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Object&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'ByteArray'&lt;/span&gt;      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ByteArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'base64'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt;                       &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;                                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;     &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Array'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Dictionary'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Class'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'collection_marker'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
         &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;     &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;                 &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'scalar'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;             &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'element['&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;']'&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt;                                                 &lt;span class="s1"&gt;'scalar'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;                                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt;      &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;_structures&lt;/span&gt;  &lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;_schemes&lt;/span&gt;     &lt;span class="n"&gt;s&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;_types&lt;/span&gt;       &lt;span class="n"&gt;t&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_type&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;object_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_order&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- MS SQL Server&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;                                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;scheme_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;                                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;type_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_db_type&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'String'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_String&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Long'&lt;/span&gt;           &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Long&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Guid'&lt;/span&gt;           &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Guid&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Double'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Double&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Boolean'&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Boolean&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'false'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'DateTimeOffset'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Numeric'&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Numeric&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'ListItem'&lt;/span&gt;       &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ListItem&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Object'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Object&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'ByteArray'&lt;/span&gt;      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;binary, base64 in app code&amp;gt;'&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt;                       &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;                                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'scalar'&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;                        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'element['&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;ISNULL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;']'&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt;                                                            &lt;span class="s1"&gt;'scalar'&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;                                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;_structures&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;_schemes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="n"&gt;s&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;_types&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;       &lt;span class="n"&gt;t&lt;/span&gt;  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_type&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;object_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a &lt;strong&gt;diagnostic&lt;/strong&gt; query — paste it into psql/DataGrip/SSMS, plug in any object ID, and you see exactly what's stored: which fields, which slot (scalar / collection marker / array element), which type. The marker rows for arrays show up too, which is exactly what you want when you're hunting down a &lt;code&gt;null&lt;/code&gt; vs &lt;code&gt;[]&lt;/code&gt; regression.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2 — same result via &lt;code&gt;_scheme_metadata_cache&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL (via _scheme_metadata_cache)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_scheme_id&lt;/span&gt;                                      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;scheme_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;                                           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type_name&lt;/span&gt;                                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;type_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_type&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;db_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collection_type_name&lt;/span&gt;                            &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;collection_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_type&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'String'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_String&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Long'&lt;/span&gt;           &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Long&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Guid'&lt;/span&gt;           &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Guid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Double'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Double&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Boolean'&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Boolean&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'DateTimeOffset'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Numeric'&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Numeric&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'ListItem'&lt;/span&gt;       &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ListItem&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Object'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Object&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'ByteArray'&lt;/span&gt;      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ByteArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'base64'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt;                       &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;                                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_order&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_values&lt;/span&gt;                &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;_scheme_metadata_cache&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_structure_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;object_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_order&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt; &lt;span class="k"&gt;LAST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- MS SQL Server (via _scheme_metadata_cache)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;_scheme_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;scheme_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                                         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;type_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                                     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;type_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;db_type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;db_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;collection_type_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;collection_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;db_type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'String'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_String&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Long'&lt;/span&gt;           &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Long&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Guid'&lt;/span&gt;           &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Guid&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Double'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Double&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Boolean'&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Boolean&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'false'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'DateTimeOffset'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_DateTimeOffset&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Numeric'&lt;/span&gt;        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Numeric&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'ListItem'&lt;/span&gt;       &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ListItem&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'Object'&lt;/span&gt;         &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Object&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;nvarchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'ByteArray'&lt;/span&gt;      &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;binary&amp;gt;'&lt;/span&gt;
        &lt;span class="k"&gt;ELSE&lt;/span&gt;                       &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;                                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_array_parent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;_order&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;_values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;_scheme_metadata_cache&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;_structure_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;_id_structure&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;object_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;_order&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;_array_index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why the second one is the production query:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Two heavy JOINs (&lt;code&gt;_structures&lt;/code&gt;, &lt;code&gt;_types&lt;/code&gt;) gone. The cache row carries everything those joins would have produced.&lt;/li&gt;
&lt;li&gt;The cache already has a B-tree on &lt;code&gt;_structure_id&lt;/code&gt; and a stable &lt;code&gt;_order&lt;/code&gt; for ordering — no extra sorts.&lt;/li&gt;
&lt;li&gt;The cache is refreshed only when the scheme changes (&lt;code&gt;_structure_hash&lt;/code&gt; flips), not on every read. Steady-state queries pay zero cost for it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build_hierarchical_properties_optimized()&lt;/code&gt; goes one step further: it slurps &lt;strong&gt;all &lt;code&gt;_values&lt;/code&gt; rows for one object into a &lt;code&gt;_values[]&lt;/code&gt; array in a single SELECT&lt;/strong&gt;, then walks the property tree purely in memory using &lt;code&gt;unnest()&lt;/code&gt;. Recursion into nested classes and array elements never touches the table again. For deeply nested object graphs this is a big deal — you can materialize a 40-row, 8-level-deep object with two SELECTs total.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  How this connects to the C# API
&lt;/h2&gt;

&lt;p&gt;For context — what those tables look like from a &lt;code&gt;SaveAsync&lt;/code&gt;/&lt;code&gt;LoadAsync&lt;/code&gt; perspective. The full API tour is in &lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/two-tables-zero-migrations-full-linq-a-net-data-engine-thats-been-running-our-production-for-2482"&gt;the intro post&lt;/a&gt;; here's just the mapping back to the tables we just looked at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RedbScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Employee"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmployeeProps&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;  &lt;span class="n"&gt;LastName&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;     &lt;span class="n"&gt;Age&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Salary&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]?&lt;/span&gt; &lt;span class="n"&gt;Skills&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// InitializeAsync scans the assembly →&lt;/span&gt;
&lt;span class="c1"&gt;//   - inserts/updates rows in _schemes + _structures for each [RedbScheme]&lt;/span&gt;
&lt;span class="c1"&gt;//   - refreshes _scheme_metadata_cache on changed schemes&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmployeeProps&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// SaveAsync →&lt;/span&gt;
&lt;span class="c1"&gt;//   - one row into _objects&lt;/span&gt;
&lt;span class="c1"&gt;//   - one row per scalar into _values (using the typed column for the C# type)&lt;/span&gt;
&lt;span class="c1"&gt;//   - for Skills: one marker row + one row per element, linked via _array_parent_id&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RedbObject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmployeeProps&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Alice Johnson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;EmployeeProps&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LastName&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Johnson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Age&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Salary&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;120_000m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Skills&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"C#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"PostgreSQL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Redis"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;// → 1 marker + 3 element rows&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// LoadAsync →&lt;/span&gt;
&lt;span class="c1"&gt;//   - SELECT _objects WHERE _id = id&lt;/span&gt;
&lt;span class="c1"&gt;//   - SELECT * FROM _values WHERE _id_object = id  (one shot, into a _values[])&lt;/span&gt;
&lt;span class="c1"&gt;//   - build_hierarchical_properties_optimized() materializes the C# graph&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmployeeProps&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SaveAsync&lt;/code&gt; reads &lt;code&gt;_scheme_metadata_cache&lt;/code&gt; to know which column to write each value to, mints IDs from the &lt;code&gt;global_identity&lt;/code&gt; sequence, and writes &lt;code&gt;_objects&lt;/code&gt; + &lt;code&gt;_values&lt;/code&gt;. &lt;code&gt;LoadAsync&lt;/code&gt; reads &lt;code&gt;_objects&lt;/code&gt; first, then &lt;strong&gt;one&lt;/strong&gt; SELECT pulls every &lt;code&gt;_values&lt;/code&gt; row for that object into a memory array, and the recursive materializer never goes back to the database for that load.&lt;/p&gt;




&lt;h2&gt;
  
  
  A few design choices that aren't obvious
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Negative constants for system IDs.&lt;/strong&gt; User-generated keys come from a sequence starting at &lt;code&gt;1_000_000&lt;/code&gt;. System types, schemes, users live near &lt;code&gt;long.MinValue&lt;/code&gt;. The two ranges can never collide. This means &lt;code&gt;_types._id = -10&lt;/code&gt; for the &lt;code&gt;@@__deleted&lt;/code&gt; scheme isn't a special-case in any query — it's just a perfectly normal FK that happens to be negative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;_structure_hash&lt;/code&gt; on &lt;code&gt;_schemes&lt;/code&gt;.&lt;/strong&gt; Without it, every &lt;code&gt;SyncSchemeAsync&amp;lt;T&amp;gt;&lt;/code&gt; call would have to re-read the schema structure and compare row-by-row. With it, comparing one UUID tells you whether anything changed. The cache-invalidation trigger fires only on real changes, so steady-state operation pays nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marker rows.&lt;/strong&gt; The trickiest design choice in &lt;code&gt;_values&lt;/code&gt;. There's no separate "collections" table — instead, a row with &lt;code&gt;_array_index = NULL AND _array_parent_id = NULL&lt;/code&gt; and a collection-typed &lt;code&gt;_id_structure&lt;/code&gt; is the marker, and child rows fan out from it via &lt;code&gt;_array_parent_id&lt;/code&gt;. This is what makes &lt;code&gt;null&lt;/code&gt; vs &lt;code&gt;[]&lt;/code&gt; distinguishable, lets dictionaries with string keys work without a side table, and keeps nested-class hierarchies in the same physical structure as flat fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;_Numeric NUMERIC(38, 18)&lt;/code&gt;.&lt;/strong&gt; Deliberate. &lt;code&gt;double&lt;/code&gt; for money quietly loses pennies; nobody wants a &lt;code&gt;0.0000000001&lt;/code&gt;-off invoice total in production. The 38/18 precision/scale is more than enough for currency, percentage, weight, even small molar quantities. The cost is storage size (16 bytes vs 8), which on the kind of property volume REDB sees is noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres vs MSSQL — the cascade-delete story.&lt;/strong&gt; On Postgres, &lt;code&gt;_values&lt;/code&gt; has &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; on its FK to &lt;code&gt;_structures&lt;/code&gt;. On MSSQL the same constraint would create multiple cascade paths and SQL Server refuses to compile that. The workaround: an &lt;code&gt;INSTEAD OF DELETE&lt;/code&gt; trigger on &lt;code&gt;_structures&lt;/code&gt; that manually cascades into &lt;code&gt;_values&lt;/code&gt;. Same observable behavior, different machinery. There are a handful of places in the codebase where the dialect abstraction exists specifically to paper over this kind of thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intro post&lt;/strong&gt; — &lt;a href="https://dev.to/rinat_kozin_d0a2ef43e7824/two-tables-zero-migrations-full-linq-a-net-data-engine-thats-been-running-our-production-for-2482"&gt;An EF Core alternative for .NET apps with complex object graphs&lt;/a&gt; &lt;em&gt;(published)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 (this post)&lt;/strong&gt; — the 13 tables, RTTI vs EAV, &lt;code&gt;_values&lt;/code&gt;, collection storage, &lt;code&gt;_scheme_metadata_cache&lt;/code&gt;, two diagnostic queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2&lt;/strong&gt; — code-first schemes: &lt;code&gt;SyncSchemeAsync&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;_structure_hash&lt;/code&gt;, automatic onboarding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3&lt;/strong&gt; — CRUD internals: &lt;code&gt;SaveAsync&lt;/code&gt;/&lt;code&gt;LoadAsync&lt;/code&gt;, TreeDiff change tracking, COPY-protocol bulk insert&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4&lt;/strong&gt; — LINQ → SQL: pivot CTEs, dialect splits, the &lt;code&gt;OfficeLocations["HQ"].City&lt;/code&gt; walkthrough&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 5&lt;/strong&gt; — trees: &lt;code&gt;LoadTreeAsync&lt;/code&gt;, &lt;code&gt;WhereHasAncestor&lt;/code&gt;, the closure-table vs recursive-CTE story&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 6&lt;/strong&gt; — window functions: &lt;code&gt;Win.RowNumber()&lt;/code&gt;, &lt;code&gt;Win.Rank()&lt;/code&gt;, &lt;code&gt;PartitionBy&lt;/code&gt;/&lt;code&gt;OrderBy&lt;/code&gt; over REDB objects&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where to look
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/redbase-app" rel="noopener noreferrer"&gt;github.com/redbase-app&lt;/a&gt; — all repos in the ecosystem&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/redbase-app/redb" rel="noopener noreferrer"&gt;github.com/redbase-app/redb&lt;/a&gt; — the redb.Core repo&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/redbase-app/redb/blob/main/redb.Postgres/sql/redbPostgre.sql" rel="noopener noreferrer"&gt;Postgres schema (redbPostgre.sql)&lt;/a&gt; — all 13 tables + 40+ indexes + triggers + stored functions in one file&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/redbase-app/redb/blob/main/redb.MSSql/sql/redbMSSQL.sql" rel="noopener noreferrer"&gt;MSSQL schema (redbMSSQL.sql)&lt;/a&gt; — same shape, dialect-adjusted&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://redbase.app/" rel="noopener noreferrer"&gt;redbase.app&lt;/a&gt; — docs and worked examples (EN)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions/critique very welcome in the comments — especially if you've built something similar and have war stories about indexing strategies on the values table, that's exactly the discussion I want to have.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>postgres</category>
      <category>sql</category>
      <category>opensource</category>
    </item>
    <item>
      <title>redb.Route 3.0.1 — flat DSL navigation, CRTP refactor, and a silent null fix</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Wed, 03 Jun 2026 16:53:16 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redbroute-301-flat-dsl-navigation-crtp-refactor-and-a-silent-null-fix-3m7n</link>
      <guid>https://dev.to/rinat_kozin/redbroute-301-flat-dsl-navigation-crtp-refactor-and-a-silent-null-fix-3m7n</guid>
      <description>&lt;p&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;code&gt;redb ecosystem&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;Before 3.0.1, deep nested scopes required closing in exact reverse order — tedious and easy to get wrong. Three things changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flat &lt;code&gt;End*()&lt;/code&gt; navigation
&lt;/h2&gt;

&lt;p&gt;A typed closer walks the &lt;code&gt;Parent&lt;/code&gt; chain and exits to the right level in one call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct://demo-cascade-endchoice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* per-item work */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item=${body}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// walks past Split → When → lands at route root&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"post-cascade"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cascade done"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Universal &lt;code&gt;.End()&lt;/code&gt; exits to the nearest scope without naming it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"b"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"inside"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// closes RichLog → returns Split body&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;// closes Split   → returns When body&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;// closes Choice  → returns route root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sibling branches open naturally after a closed sub-scope — &lt;code&gt;.When()&lt;/code&gt; and &lt;code&gt;.Otherwise()&lt;/code&gt; walk up to the enclosing &lt;code&gt;ChoiceDefinition&lt;/code&gt; via the &lt;code&gt;Parent&lt;/code&gt; chain, so this compiles as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndSplit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"list branch done [${routeId}]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// still on the When body, not on Split&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// ← sibling, works after EndSplit&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Otherwise&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fallback */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndChoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three logging styles, same pipeline step
&lt;/h2&gt;

&lt;p&gt;The updated demo shows all three forms side by side — useful to see the tradeoffs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// (A) Lambda — arbitrary C# at runtime&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"[lambda] item=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; branch=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// (B) String template — compiled by the expression engine, zero alloc when level is off&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[tmpl] item=${body} branch=${header.branch} [${routeId}]"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// (C) Rich-log scope — structured, multi-message, headers/properties as separate fields&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[rich-tmpl]   item=${body}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"[rich-lambda] upper=&lt;/span&gt;&lt;span class="p"&gt;{((&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;!).&lt;/span&gt;&lt;span class="nf"&gt;ToUpperInvariant&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"item-index"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShowRouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndLog&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;${body}&lt;/code&gt;, &lt;code&gt;${header.x}&lt;/code&gt;, &lt;code&gt;${property.y}&lt;/code&gt;, &lt;code&gt;${routeId}&lt;/code&gt;, &lt;code&gt;${exception.type}&lt;/code&gt;, &lt;code&gt;${exception.message}&lt;/code&gt; — all resolved by the compiled expression engine (Tokenizer → Parser → AST → IL). The template compiles once, runs as a cached delegate. &lt;code&gt;.Message()&lt;/code&gt; in a rich-log scope accepts both forms simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  CRTP base — 27 duplicate class bodies removed
&lt;/h2&gt;

&lt;p&gt;Every leaf method (&lt;code&gt;To&lt;/code&gt;, &lt;code&gt;Process&lt;/code&gt;, &lt;code&gt;SetBody&lt;/code&gt;, &lt;code&gt;Filter&lt;/code&gt;, &lt;code&gt;Split&lt;/code&gt;, &lt;code&gt;Transaction&lt;/code&gt;, ~40 more) now lives once in &lt;code&gt;RouteDefinitionBase&amp;lt;TSelf&amp;gt;&lt;/code&gt;. Each returns &lt;code&gt;TSelf&lt;/code&gt; — chaining stays on the concrete scope type throughout. Public API and route AST identical to 3.0.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix: &lt;code&gt;GetContext()&lt;/code&gt; silently returned null inside nested scopes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;IRouteDefinition.GetContext()&lt;/code&gt; was casting to &lt;code&gt;RouteDefinition&lt;/code&gt;, which only matched the route root. Inside &lt;code&gt;When&lt;/code&gt;, &lt;code&gt;Loop&lt;/code&gt;, &lt;code&gt;Traced&lt;/code&gt;, &lt;code&gt;Catch&lt;/code&gt; — it returned &lt;code&gt;null&lt;/code&gt; without throwing. Now walks &lt;code&gt;Parent&lt;/code&gt; to the owning route. Matters if you have extension methods that read context at DSL build time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Full demo:&lt;/strong&gt; &lt;code&gt;DeepDslShowcaseRoutes.cs&lt;/code&gt; · &lt;strong&gt;Changelog:&lt;/strong&gt; &lt;a href="https://github.com/redbase-app/redb-route/blob/main/CHANGELOG.md#301--2026-06-03" rel="noopener noreferrer"&gt;3.0.1&lt;/a&gt; · Apache 2.0&lt;/p&gt;




</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
      <category>new</category>
    </item>
  </channel>
</rss>
