<?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>Basic OP: 35 modules, 0 failures. Config OP: 0 failures.

Found and fixed: PII leak in id_token, missing Cache-Control: no-store.

report: https://github.com/redbase-app/redb-identity/blob/main/OPENID_CERTIFICATION.md

#dotnet #oidc #oauth</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Wed, 22 Jul 2026 20:05:11 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/basic-op-35-modules-0-failures-config-op-0-failures-found-and-fixed-pii-leak-in-idtoken-46j6</link>
      <guid>https://dev.to/rinat_kozin/basic-op-35-modules-0-failures-config-op-0-failures-found-and-fixed-pii-leak-in-idtoken-46j6</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/redbase-app/redb-identity/blob/main/OPENID_CERTIFICATION.md" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fopengraph.githubassets.com%2F4e168653422f711ccfc0e0f567d51fea23d6f15a27ca6b23b901038a818768e3%2Fredbase-app%2Fredb-identity" height="600" class="m-0" width="1200"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/redbase-app/redb-identity/blob/main/OPENID_CERTIFICATION.md" rel="noopener noreferrer" class="c-link"&gt;
            redb-identity/OPENID_CERTIFICATION.md at main · redbase-app/redb-identity · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Transport-agnostic OAuth 2.1 / OpenID Connect server for the redb ecosystem — OpenIddict on redb.Route, redb-backed stores, MFA, WebAuthn, federation, SCIM, DPoP. Green on PostgreSQL, MSSQL and SQL...
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>The night job that carves its own partitions: cron, a plpgsql function, and a Splitter</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Tue, 21 Jul 2026 17:17:47 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/the-night-job-that-carves-its-own-partitions-cron-a-plpgsql-function-and-a-splitter-6gl</link>
      <guid>https://dev.to/rinat_kozin/the-night-job-that-carves-its-own-partitions-cron-a-plpgsql-function-and-a-splitter-6gl</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%2Fijko2j9h4l5a9pxx5qdr.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%2Fijko2j9h4l5a9pxx5qdr.png" alt="redb.Route.Cron" 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;p&gt;There's a table of GPS points. Vehicles push coordinates every few seconds, a day adds up to millions of rows, and the table is partitioned by time. Which means somebody has to create tomorrow's partition ahead of time and detach the ones older than ninety days. Otherwise, one fine night, an insert dies with &lt;code&gt;no partition of relation "gps_points" found for row&lt;/code&gt; — at precisely the hour nobody is watching.&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%2F1ykj9saojbzay8gxolp0.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%2F1ykj9saojbzay8gxolp0.png" alt="redb.Tsak scheduler" width="800" height="698"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem is as old as the hills, and it's usually solved one of two ways: &lt;code&gt;pg_cron&lt;/code&gt; inside the database, or an &lt;code&gt;IHostedService&lt;/code&gt; with a &lt;code&gt;PeriodicTimer&lt;/code&gt; in the app. The first has an observability problem — the job lives in the database, and your application logs know nothing about it. The second has the problem that a small private infrastructure grows around &lt;code&gt;PeriodicTimer&lt;/code&gt; very quickly: retries, logging, "what if the previous run is still going", "what if there are three nodes".&lt;/p&gt;

&lt;p&gt;I'll show a third way — a route. What follows is a walkthrough of the &lt;code&gt;redb.Route.Quartz&lt;/code&gt; connector, calling a PostgreSQL function through the SQL connector, a splitter with error isolation, and an honest conversation about clustering, because that's where it actually gets interesting.&lt;/p&gt;

&lt;p&gt;All the code in this post uses string URIs. redb.Route has fluent builders too, but a URI reads without knowing the API, and you can paste it into a config file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;redb ecosystem series.&lt;/strong&gt; This is a continuation — newest posts first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/the-amazon-sqs-sns-connector-in-redbroute-at-least-once-and-pubsub-via-sns-sqs-still-leaving-48dc"&gt;The Amazon SQS + SNS connector in redb.Route, at-least-once and pub/sub via SNS→SQS. Still leaving MassTransit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/the-rabbitmq-connector-in-redbroute-the-whole-broker-in-one-uri-rpc-competing-consumers-18m0"&gt;The RabbitMQ connector in redb.Route: the whole broker in one URI — RPC, competing consumers, dead-lettering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/redb-330-an-enterprise-net-stack-you-actually-own-typed-store-a-homegrown-apache-camel-and-3gd1"&gt;redb 3.3.0: an enterprise .NET stack you actually own — typed store, a homegrown Apache Camel, and a runtime with a dashboard (all free)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/rinat_kozin/two-routes-in-an-evening-from-a-debug-worker-to-an-enterprise-runtime-with-redbroute-tsak-m8k"&gt;Two routes in an evening: from a debug worker to an enterprise runtime with redb.Route + Tsak&lt;/a&gt;&lt;/li&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 what really happens under a transaction&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;profile&lt;/a&gt;. Sources: &lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route&lt;/a&gt;. About the database itself: &lt;a href="https://redbase.app/" rel="noopener noreferrer"&gt;redbase.app&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The SQL first
&lt;/h2&gt;

&lt;p&gt;There's no magic at this level, so let's start from the most honest place — the function in the database. It takes a table name and a retention window, creates tomorrow's partition if it isn't there yet, and detaches everything past the window:&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;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;maintain_partitions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tbl&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;keep_days&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="n"&gt;next_day&lt;/span&gt;   &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;part_name&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s_%s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'YYYYMMDD'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt;     &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;make_interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;keep_days&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;dropped&lt;/span&gt;    &lt;span class="nb"&gt;int&lt;/span&gt;  &lt;span class="p"&gt;:&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;old_part&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;BEGIN&lt;/span&gt;
    &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;to_regclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part_name&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;part_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_day&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="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;old_part&lt;/span&gt; &lt;span class="k"&gt;IN&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;relname&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_inherits&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&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;inhrelid&lt;/span&gt; &lt;span class="o"&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;oid&lt;/span&gt;
        &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt; &lt;span class="o"&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;inhparent&lt;/span&gt;
        &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tbl&lt;/span&gt;
          &lt;span class="k"&gt;AND&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;relname&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s_%s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'YYYYMMDD'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;LOOP&lt;/span&gt;
        &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DROP TABLE %I'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old_part&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;dropped&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dropped&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="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;LOOP&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;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s: +1 partition, -%s dropped'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tbl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function returns a string so it shows up in the log. That "so it shows up in the log" is the single concession to convenience here; everything else is ordinary plpgsql you'd have written anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tick: &lt;code&gt;cron://&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;redb.Route.Quartz&lt;/code&gt; connector gives you two schemes. The first is &lt;code&gt;cron:&lt;/code&gt;, a regular Quartz trigger with a cron expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cron://[group/]jobName?schedule=&amp;lt;cron-expression&amp;gt;&amp;amp;&amp;lt;options&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is &lt;code&gt;qtimer:&lt;/code&gt;, a plain periodic trigger for when you don't need cron:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qtimer://[group/]jobName?period=5000&amp;amp;delay=1000&amp;amp;fixedRate=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is deliberately no &lt;code&gt;quartz:&lt;/code&gt; scheme. That's not an oversight: scheduler setup — where jobs are stored, whether it's a cluster or a single node, what the thread pool looks like — is a property of the host, not of the route. Only the schedule belongs in a route URI. If a &lt;code&gt;quartz:&lt;/code&gt; scheme existed, job store settings would immediately start growing into it, and the route would stop being portable.&lt;/p&gt;

&lt;p&gt;The cron expression is the Quartz one, six fields, with seconds. Let's put partition maintenance at 02:30:&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://maintenance/gps-partitions?schedule=0 30 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;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gps-partitions"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expression is validated when the endpoint is created, not when it first fires. A typo in the schedule is an &lt;code&gt;ArgumentException&lt;/code&gt; at application startup, not silence until three in the morning.&lt;/p&gt;

&lt;p&gt;Registering the component is one line at the module entry point:&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;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;CronComponent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Calling the function: &lt;code&gt;sql:&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The SQL connector has a single scheme, &lt;code&gt;sql:&lt;/code&gt;, and the mode is picked with the &lt;code&gt;mode&lt;/code&gt; parameter. To call a PostgreSQL function there's &lt;code&gt;mode=Procedure&lt;/code&gt; with the &lt;code&gt;asFunction=true&lt;/code&gt; flag — the connector then assembles &lt;code&gt;SELECT maintain_partitions(@p1, @p2)&lt;/code&gt;, executes it as a scalar, and puts the result in the message body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sql:maintain_partitions
  ?mode=Procedure
  &amp;amp;dataSource=#pg
  &amp;amp;procedureName=maintain_partitions
  &amp;amp;asFunction=true
  &amp;amp;procedureParams=IN:tbl:String,IN:keep_days:Int32
  &amp;amp;param.keep_days=90
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;procedureParams&lt;/code&gt; declares the parameters in &lt;code&gt;direction:name:type&lt;/code&gt; form, and the declaration order is the argument order in the call. There are three directions — &lt;code&gt;IN&lt;/code&gt;, &lt;code&gt;OUT&lt;/code&gt;, &lt;code&gt;INOUT&lt;/code&gt; — and &lt;code&gt;OUT&lt;/code&gt; values come back into the message headers under their own names after execution.&lt;/p&gt;

&lt;p&gt;A parameter's value is resolved along a chain: first an explicit &lt;code&gt;param.name&lt;/code&gt; from the URI, then a message header with the same name, then the body if it's a dictionary. Here &lt;code&gt;keep_days&lt;/code&gt; is a constant right in the URI, and &lt;code&gt;tbl&lt;/code&gt; will arrive from a header set by the splitter.&lt;/p&gt;

&lt;p&gt;For simple cases there's a shorter path — &lt;code&gt;mode=Execute&lt;/code&gt; (the default) with &lt;code&gt;@name&lt;/code&gt; placeholders directly in the query text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sql:SELECT maintain_partitions(@tbl, @keep_days)
  ?dataSource=#pg
  &amp;amp;outputType=Scalar
  &amp;amp;param.tbl=${header.tbl}
  &amp;amp;param.keep_days=90
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;${header.tbl}&lt;/code&gt; — that's an expression, resolved at runtime from the message header. Placeholders in SQL are &lt;code&gt;@name&lt;/code&gt; only, the colon form isn't supported, and an unsubstituted parameter silently becomes &lt;code&gt;NULL&lt;/code&gt;, so it pays not to mistype the names.&lt;/p&gt;

&lt;p&gt;Both variants work. For the rest of the post I use &lt;code&gt;mode=Procedure&lt;/code&gt; because it's the more illustrative one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The splitter: three tables, and one failure doesn't take down the rest
&lt;/h2&gt;

&lt;p&gt;There's rarely just one time-partitioned table. We have three: points, tracks, and events. The naive move would be a loop inside a processor, but then you're deciding by hand what happens when the second table fails — abort everything or carry on, and how do you find out afterwards what didn't run.&lt;/p&gt;

&lt;p&gt;This is exactly the EIP Splitter pattern. A message carrying a list is split into one message per element, each goes down its own branch, and the branches can be processed in parallel:&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://maintenance/gps-partitions?schedule=0 30 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;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gps-partitions"&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;Body&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;"gps_points"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gps_tracks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gps_events"&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="nf"&gt;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;ParallelProcessing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxParallelism&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;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tbl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;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;DoTry&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:maintain_partitions"&lt;/span&gt;
                &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"?mode=Procedure"&lt;/span&gt;
                &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;dataSource=#pg"&lt;/span&gt;
                &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;procedureName=maintain_partitions"&lt;/span&gt;
                &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;asFunction=true"&lt;/span&gt;
                &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;procedureParams=IN:tbl:String,IN:keep_days:Int32"&lt;/span&gt;
                &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;param.keep_days=90"&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;"[PART] ${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;DoCatch&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&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;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[PART] ${header.tbl}: ${exception.Message}"&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;Error&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;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;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Summary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve lines, and they already contain everything you'd normally bolt on a week later. &lt;code&gt;MaxParallelism(2)&lt;/code&gt; means two tables get serviced at once and the third waits for a free slot; &lt;code&gt;DROP TABLE&lt;/code&gt; takes an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock, and there's no point burying the database under parallel locks. &lt;code&gt;DoTry&lt;/code&gt;/&lt;code&gt;DoCatch&lt;/code&gt; sit &lt;strong&gt;inside&lt;/strong&gt; the split, so the exception is isolated to its own branch: a failing &lt;code&gt;gps_tracks&lt;/code&gt; won't undo the &lt;code&gt;gps_points&lt;/code&gt; that already succeeded, and won't stop &lt;code&gt;gps_events&lt;/code&gt;. After &lt;code&gt;EndSplit&lt;/code&gt;, control lands in &lt;code&gt;Summary&lt;/code&gt;, where you can count how many branches made it and decide whether to page somebody.&lt;/p&gt;

&lt;p&gt;This isn't a trick invented for the article. Exactly this shape runs in production — a job that syncs shipping points from SAP splits the list of points and processes three at a time, because one unreachable point shouldn't take down the whole tick:&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://tsum-points?period=180000&amp;amp;delay=60000"&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-points-timer"&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;PreloadContextAsync&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="nf"&gt;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;ParallelProcessing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxParallelism&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;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ShippingPoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;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;DoTry&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;sqlTo&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;DeserializeXml&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;ProcessPointsAsync&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoCatch&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&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;AddPointSyncError&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;Error&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;"[TSUM-PT] SP=${header.ShippingPoint} failed, skipping: ${exception.Message}"&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;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;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;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildPointSyncSummary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The splitter isn't the only EIP that docks onto a scheduler. A schedule pairs naturally with a Content-Based Router (one thing on weekdays, another on weekends), a Throttler (don't hammer an external API more than N times a second), an Aggregator (collect branch results into a single report), an Idempotent Consumer (more on that below) and a Dead Letter Channel. redb.Route implements nearly the whole Hohpe &amp;amp; Woolf catalogue — Splitter, Aggregator, Resequencer, Multicast, Recipient List, Dynamic Router, Wire Tap, Content Enricher, Claim Check, Saga, Scatter-Gather, Circuit Breaker, Load Balancer, Transactional Client and the rest. The scheduler here is just a source, not a separate world with its own rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the server was down at 02:30
&lt;/h2&gt;

&lt;p&gt;This is where the reason you'd want Quartz rather than a &lt;code&gt;PeriodicTimer&lt;/code&gt; starts.&lt;/p&gt;

&lt;p&gt;The job didn't fire, because the node was down or a deploy ran long. What should happen when the scheduler comes back at 02:47? The answer depends on the job, and it isn't a philosophical question: for partition maintenance, a skip is a catastrophe — tomorrow's partition has to be created, at 02:47 or at 06:00. For a "send the morning report" job, running at noon is worse than not running at all.&lt;/p&gt;

&lt;p&gt;Quartz calls this misfire, and the connector passes the policy straight through the URI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cron://maintenance/gps-partitions?schedule=0 30 2 * * ?&amp;amp;misfireInstruction=CronFireOnceNow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CronFireOnceNow&lt;/code&gt; — catch up, run once, return to the schedule. &lt;code&gt;CronDoNothing&lt;/code&gt; — skip it, wait for the next scheduled time. Simple triggers (&lt;code&gt;qtimer:&lt;/code&gt;) have more policies — five of them — differing in what to do with the accumulated repeat count. But the choice always collapses to one question: does a missed run need catching up, or has it already gone stale?&lt;/p&gt;

&lt;p&gt;The connector's default for &lt;code&gt;qtimer:&lt;/code&gt; is picked by common sense: with &lt;code&gt;fixedRate=true&lt;/code&gt;, catch up (you asked for a fixed rate); otherwise, reschedule from the next tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the previous run is still going
&lt;/h2&gt;

&lt;p&gt;Partitions have piled up, &lt;code&gt;DROP TABLE&lt;/code&gt; is waiting on a lock, the job is hanging. The next fire time arrives. Now what?&lt;/p&gt;

&lt;p&gt;The standard Quartz answer is the &lt;code&gt;[DisallowConcurrentExecution]&lt;/code&gt; attribute on the job class. The connector went a different way: concurrency is governed by the consumer's own semaphore, and if every thread is busy the fire is silently skipped:&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;// QuartzConsumerBase.cs&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;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="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ConfigureAwait&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// all threads busy, skip this fire&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The semaphore size is set in the URI via &lt;code&gt;threads&lt;/code&gt; (default 1). Why not the attribute: &lt;code&gt;[DisallowConcurrentExecution]&lt;/code&gt; is either one run or no control at all, with nothing in between. A semaphore lets you say "up to three concurrent runs of this job" while staying friendly to a cluster, where the limit lives at the job store level rather than in a class attribute.&lt;/p&gt;

&lt;p&gt;If you do want the Quartz semantics, there's a &lt;code&gt;stateful=true&lt;/code&gt; flag that switches the job to a class carrying &lt;code&gt;[DisallowConcurrentExecution]&lt;/code&gt; and &lt;code&gt;[PersistJobDataAfterExecution]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One more detail about shutdown: when a route is stopped, the connector unschedules the trigger and waits for runs already in flight — up to thirty seconds. A job that happens to be dropping a partition right then won't be cut off halfway. And if a job does fire when its route is already gone (the module was unloaded, say), the job notices on startup that its consumer is dead and deletes itself from the scheduler, leaving no litter behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three nodes
&lt;/h2&gt;

&lt;p&gt;The most common question about cron in a distributed application: if there are three nodes, does the job fire three times?&lt;/p&gt;

&lt;p&gt;It does — if every node keeps its own scheduler in memory. That's exactly how the connector's fallback works: no &lt;code&gt;IScheduler&lt;/code&gt; found in the context, so it creates its own, RAM-backed, unique to that context. Fine for local development, not fine for three nodes.&lt;/p&gt;

&lt;p&gt;The right answer is one scheduler per cluster — more precisely, one shared job store. Quartz does this out of the box via &lt;code&gt;AdoJobStore&lt;/code&gt;, and the connector deliberately stays out of the way: it does not create its own scheduler if a ready one is already sitting in the route context. The host drops a clustered one in there, and the route needs no changes at all — the URI stays the same.&lt;/p&gt;

&lt;p&gt;And here's where all the plumbing that usually gets hand-waved away comes into view. &lt;code&gt;AdoJobStore&lt;/code&gt; is a set of &lt;code&gt;QRTZ_*&lt;/code&gt; tables in your database. &lt;code&gt;QRTZ_TRIGGERS&lt;/code&gt; holds the next fire time, &lt;code&gt;QRTZ_FIRED_TRIGGERS&lt;/code&gt; holds who is executing what right now, &lt;code&gt;QRTZ_LOCKS&lt;/code&gt; holds row-mutexes. The "only one node runs the job" mechanism isn't clever consensus — it's &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on a row in &lt;code&gt;QRTZ_LOCKS&lt;/code&gt;: whoever takes the lock first takes the trigger. Each node checks in periodically in &lt;code&gt;QRTZ_SCHEDULER_STATE&lt;/code&gt;, and if a node stops checking in, another one picks up its unfinished jobs — but only those marked &lt;code&gt;recoverableJob=true&lt;/code&gt;. The flag is off by default: restarting a job when you don't know whether it's idempotent is a bad idea.&lt;/p&gt;

&lt;p&gt;The table schema is created by the host at startup; the connection string and dialect come from the application's database configuration. So you don't write the DDL, but the tables are perfectly ordinary, they sit next to yours, they're visible in any client, and when something goes wrong you go in with a plain &lt;code&gt;SELECT&lt;/code&gt; and see which trigger is stuck and on which node.&lt;/p&gt;

&lt;p&gt;And since we brought up &lt;code&gt;recoverableJob&lt;/code&gt;: recovery after a node dies means the job may run twice. For our partition maintenance function that's safe — it's written idempotently (&lt;code&gt;IF to_regclass(...) IS NULL&lt;/code&gt;), and that's a requirement, not an accident. If the job weren't idempotent — say, it credited bonuses — you'd put an Idempotent Consumer in front of it, and redb.Route has one backed by SQL with a unique index. A unique index, not a "smart cache": in a cluster, that's the only thing that saves you from double execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What lands in the message
&lt;/h2&gt;

&lt;p&gt;A scheduler is a source with no message body. The body is &lt;code&gt;null&lt;/code&gt;, the pattern is &lt;code&gt;InOnly&lt;/code&gt;, and everything about the firing lives in the properties:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;What's in it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CamelQuartzFireTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when the job actually fired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CamelQuartzScheduledFireTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when it should have fired per the schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CamelQuartzNextFireTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when it fires next&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CamelQuartzPreviousFireTime&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;when it fired last&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CamelCronSchedule&lt;/code&gt; / &lt;code&gt;CamelCronName&lt;/code&gt; / &lt;code&gt;CamelCronGroup&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the expression, name and group of the job&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap between &lt;code&gt;FireTime&lt;/code&gt; and &lt;code&gt;ScheduledFireTime&lt;/code&gt; is that misfire, in numbers. If they're seventeen minutes apart, the job was catching up.&lt;/p&gt;

&lt;p&gt;The Camel-style names aren't nostalgia. redb.Route deliberately keeps Apache Camel's nomenclature wherever the semantics match, so someone arriving from Java integrations can read the headers without a dictionary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two jobs from production
&lt;/h2&gt;

&lt;p&gt;So this doesn't read like a piece about a spherical cron in a vacuum — here are two routes that run every night in a transport management system.&lt;/p&gt;

&lt;p&gt;Database backup at three:&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://tsum-backup?schedule=0 0 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;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tsum-backup-cron"&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;RunBackupAsync&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dead-route cleanup at four:&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://tsum-cleanup?schedule=0 0 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;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tsum-cleanup-cron"&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;CleanupDeadRoutesAsync&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing spectacular, and that's the point: the schedule is in the URI, the logic is in the processor, retries and logging come from the framework. Note that the schedule is hardcoded in the URI rather than pulled from config — that's allowed, and it's how things live for a while. When you need to change a schedule without a rebuild, you assemble the URI from config with ordinary concatenation, because it's just a string.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full option list
&lt;/h2&gt;

&lt;p&gt;So this doesn't turn into half a post of reference material — everything &lt;code&gt;cron:&lt;/code&gt; understands:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;schedule&lt;/code&gt; (required), &lt;code&gt;timeZone&lt;/code&gt; (IANA name), &lt;code&gt;threads&lt;/code&gt;, &lt;code&gt;misfireInstruction&lt;/code&gt;, &lt;code&gt;stateful&lt;/code&gt;, &lt;code&gt;recoverableJob&lt;/code&gt;, &lt;code&gt;durableJob&lt;/code&gt;, &lt;code&gt;deleteJob&lt;/code&gt;, &lt;code&gt;pauseJob&lt;/code&gt;, &lt;code&gt;startAt&lt;/code&gt;, &lt;code&gt;endAt&lt;/code&gt;, &lt;code&gt;customCalendar&lt;/code&gt;, &lt;code&gt;triggerStartDelay&lt;/code&gt;, &lt;code&gt;prefixJobNameWithEndpointId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;qtimer:&lt;/code&gt; swaps &lt;code&gt;schedule&lt;/code&gt; for &lt;code&gt;period&lt;/code&gt;, &lt;code&gt;delay&lt;/code&gt;, &lt;code&gt;fixedRate&lt;/code&gt;, &lt;code&gt;repeatCount&lt;/code&gt;; everything else is the same, minus the time zone (a simple trigger doesn't need one).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;customCalendar&lt;/code&gt; is worth calling out: it's the Quartz exclusion calendar, registered in the context by name. That's how you get "except holidays" and "business days only" — things a cron expression cannot express at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The scheduler in redb.Route is a message source, not a subsystem with its own rules. A job is a route, which means everything available to any route is available to it: the splitter, exception handling, transactions, retries, metrics. The trigger is described by a single URI string, and there's nothing about infrastructure in that string — only the schedule.&lt;/p&gt;

&lt;p&gt;The infrastructure isn't hidden, either. The cluster runs on &lt;code&gt;QRTZ_*&lt;/code&gt; tables and a row lock in the database, idempotency in a cluster comes from a unique index, and partition maintenance is an ordinary plpgsql function that you wrote and can read. The framework here spares you the glue code, not the understanding of what's happening in your database.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-route&lt;/a&gt; · Docs: &lt;a href="https://redbase.app/" rel="noopener noreferrer"&gt;redbase.app&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;If this was useful — a ⭐ on &lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps others find it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>c</category>
      <category>opensource</category>
      <category>postgres</category>
    </item>
    <item>
      <title>The Amazon SQS + SNS connector in redb.Route, at-least-once and pub/sub via SNS SQS. Still leaving MassTransit</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Thu, 16 Jul 2026 13:24:18 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/the-amazon-sqs-sns-connector-in-redbroute-at-least-once-and-pubsub-via-sns-sqs-still-leaving-48dc</link>
      <guid>https://dev.to/rinat_kozin/the-amazon-sqs-sns-connector-in-redbroute-at-least-once-and-pubsub-via-sns-sqs-still-leaving-48dc</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%2F1ucok93f7rbfvfab7r6x.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%2F1ucok93f7rbfvfab7r6x.jpg" alt="redb.Route.SQS" width="784" height="1168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;series: "redb.Route"&lt;/p&gt;

&lt;p&gt;We've done Kafka and RabbitMQ earlier in this series. Next up: &lt;strong&gt;Amazon SQS&lt;/strong&gt; — and riding along in the same package, &lt;strong&gt;SNS&lt;/strong&gt;. Two transports, because in AWS-land they travel together. The &lt;code&gt;redb.Route.Sqs&lt;/code&gt; connector sits on the native &lt;strong&gt;AWS SDK for .NET v4&lt;/strong&gt; (&lt;code&gt;AWSSDK.SQS&lt;/code&gt;, &lt;code&gt;AWSSDK.SimpleNotificationService&lt;/code&gt;), but you don't write a "client" — you write routes, and the whole queue collapses into a single URI:&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;"sqs://orders?waitTimeSeconds=20&amp;amp;concurrentConsumers=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;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order 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="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;Read it once and the whole story's there: long-poll the &lt;code&gt;orders&lt;/code&gt; queue in 20-second pulls, four consumers wide, log it, pass it on. No "new up a client," no &lt;code&gt;ReceiveMessage&lt;/code&gt;, no "remember to &lt;code&gt;DeleteMessage&lt;/code&gt; after you've handled it" — the connector does all of that.&lt;/p&gt;

&lt;p&gt;About "leaving MassTransit," same disclaimer as last time: it's not that MassTransit can't do this. It's had SQS/SNS for years — &lt;a href="https://www.nuget.org/packages/MassTransit.AmazonSQS" rel="noopener noreferrer"&gt;&lt;code&gt;MassTransit.AmazonSQS&lt;/code&gt;&lt;/a&gt; — and it wires up the same SNS→SQS pairing. The difference is the &lt;strong&gt;model&lt;/strong&gt;. MassTransit is a bus — message contracts, consumers, bus config, the whole worldview bought at once. redb.Route is explicit routes in the Apache Camel spirit: an endpoint is a URI, the integration patterns are steps in a route, the transport is an abstraction underneath. This post is about what SQS and SNS look like in that second model.&lt;/p&gt;

&lt;p&gt;And the thing everyone's actually asking about right now — worth stating precisely rather than gleefully. &lt;strong&gt;MassTransit went commercial with v9&lt;/strong&gt;: the line now ships under &lt;a href="https://massient.com/" rel="noopener noreferrer"&gt;Massient, Inc.&lt;/a&gt;, and &lt;code&gt;MassTransit.AmazonSQS&lt;/code&gt; 9.0.0 on NuGet is already from there. But no spin: &lt;strong&gt;v8 stays Apache 2.0&lt;/strong&gt;, with security patches and critical fixes through at least the end of 2026; v9 isn't a closed binary — it's &lt;strong&gt;source-available&lt;/strong&gt;; and organizations under $1M USD annual revenue (plus non-profits under $1M in expenses) get it at a &lt;strong&gt;100% discount&lt;/strong&gt; — free, minus commercial support. So "MassTransit is paid now" is false for a small team and true for a large one, where it turns into a real decision: pay, ride v8 to the end of its support, or look around. &lt;code&gt;redb.Route&lt;/code&gt; is Apache 2.0, this SQS connector included — but pick it because the routing model fits your head, not because of a license line. A license is a reason to look around; it isn't an argument about architecture.&lt;/p&gt;

&lt;p&gt;All the code is in English; the examples aren't invented — they run against &lt;strong&gt;LocalStack&lt;/strong&gt; (a Docker container on &lt;code&gt;http://localhost:4566&lt;/code&gt;), the same way the connector's integration tests do.&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;The RabbitMQ connector — the whole broker in one URI: RPC, competing consumers, dead-lettering &lt;em&gt;(previous in the series)&lt;/em&gt;
&lt;/li&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://redbase.app/" rel="noopener noreferrer"&gt;redbase.app&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  One package, two transports
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;redb.Route.Sqs&lt;/code&gt; registers &lt;strong&gt;two schemes&lt;/strong&gt;, and they don't play the same role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sqs://&lt;/code&gt;&lt;/strong&gt; — a queue. Works as both a consumer (in &lt;code&gt;From(...)&lt;/code&gt;) and a producer (in &lt;code&gt;.To(...)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sns://&lt;/code&gt;&lt;/strong&gt; — a topic. &lt;strong&gt;Publisher-only.&lt;/strong&gt; SNS has no pull consumer — delivery is push (to SQS / HTTP / email / SMS). Try to &lt;code&gt;From("sns://...")&lt;/code&gt; and it throws &lt;code&gt;NotSupportedException&lt;/code&gt; on purpose: to "read" a topic, you subscribe an SQS queue to it and read &lt;em&gt;that&lt;/em&gt; with &lt;code&gt;sqs://&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why they share a package: at AWS they're a pair. SQS is a durable point-to-point queue (one message, one consumer). SNS is a publish-subscribe topic (one message, every subscriber). The canonical AWS fan-out is an SNS topic with several SQS queues hanging off it — so one connector covers both: &lt;code&gt;sqs://&lt;/code&gt; and &lt;code&gt;sns://&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wiring it into 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;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;AddRedbRouteSqs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// registers both sqs:// and sns://&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;AddRouteBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyRoutes&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Anatomy of the URI
&lt;/h2&gt;

&lt;p&gt;An endpoint is a string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sqs://queue-name?region=us-east-1&amp;amp;waitTimeSeconds=20&amp;amp;concurrentConsumers=4
sns://topic-name?region=us-east-1&amp;amp;autoCreateTopic=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scheme, the queue or topic name in the path, then query parameters (names are in the tables below). The fluent builder gives you the same thing: &lt;code&gt;Sqs.Queue("orders").WaitTimeSeconds(20)...&lt;/code&gt; and &lt;code&gt;Sns.Topic("events").Region("us-east-1")...&lt;/code&gt; build that exact string, URL-encoding included.&lt;/p&gt;

&lt;p&gt;Three things are AWS-specific and worth a pause:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't repeat your credentials.&lt;/strong&gt; Twenty queues on one account? Register an &lt;code&gt;AwsConnectionFactory&lt;/code&gt; in the registry and point at it with &lt;code&gt;connectionFactory=name&lt;/code&gt;. One factory builds &lt;strong&gt;both&lt;/strong&gt; the SQS and the SNS client — they share credentials, region and service URL:&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-aws"&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;AwsConnectionFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UseDefaultCredentialsProvider&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;// IAM role on the instance / SSO / env vars&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;"sqs://orders?connectionFactory=prod-aws&amp;amp;concurrentConsumers=4"&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;"sns://events?connectionFactory=prod-aws"&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;LocalStack / ElasticMQ via &lt;code&gt;serviceUrl=&lt;/code&gt;.&lt;/strong&gt; An explicit URL overrides the regional endpoint (the region sticks around only to sign the request). That's your local-dev and test mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sqs://orders?serviceUrl=http://localhost:4566&amp;amp;region=us-east-1&amp;amp;accessKey=test&amp;amp;secretKey=test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Queues and topics get created on the spot.&lt;/strong&gt; &lt;code&gt;autoCreateQueue=true&lt;/code&gt; (or &lt;code&gt;autoCreateTopic=true&lt;/code&gt;) creates the resource on startup if it's missing. A name ending in &lt;code&gt;.fifo&lt;/code&gt; comes up as a &lt;strong&gt;FIFO&lt;/strong&gt; resource automatically, with &lt;code&gt;ContentBasedDeduplication=true&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every parameter
&lt;/h2&gt;

&lt;p&gt;The reason to bookmark this post. Names are exactly as they appear in the URI; defaults are what you get out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection and credentials (shared by &lt;code&gt;sqs://&lt;/code&gt; and &lt;code&gt;sns://&lt;/code&gt;)
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;region&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;us-east-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AWS region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;serviceUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Explicit endpoint (LocalStack/ElasticMQ); overrides the region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;accessKey&lt;/code&gt; / &lt;code&gt;secretKey&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Static keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sessionToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Temporary (STS) credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;profileName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;A named profile from the shared credentials file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useDefaultCredentialsProvider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The default AWS chain (env / IAM role / SSO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connectionFactory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;A factory in the registry; overrides URI credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retryCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SDK retries on transient errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connectionTimeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;30000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP client timeout, ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;proxyHost&lt;/code&gt; / &lt;code&gt;proxyPort&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;HTTP proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Resolution order: the default chain → a profile → a session token → static keys. &lt;code&gt;Validate()&lt;/code&gt; insists on at least one path — &lt;code&gt;accessKey&lt;/code&gt;+&lt;code&gt;secretKey&lt;/code&gt;, &lt;code&gt;profileName&lt;/code&gt;, &lt;code&gt;useDefaultCredentialsProvider=true&lt;/code&gt;, or a registered &lt;code&gt;connectionFactory&lt;/code&gt; — otherwise it throws at startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQS — consumer (&lt;code&gt;From("sqs://...")&lt;/code&gt;)
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;waitTimeSeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long-poll (0–20). Fewer empty receives, fewer API calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxNumberOfMessages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many to pull per receive (1–10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;visibilityTimeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; (queue default)&lt;/td&gt;
&lt;td&gt;How long a message stays hidden while you work it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;concurrentConsumers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of competing receive loops (see the concurrency recipe)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extendMessageVisibility&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep extending visibility while the handler runs (a heartbeat); needs &lt;code&gt;visibilityTimeout &amp;gt; 0&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deleteAfterRead&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete the message after a clean pass (this is the ack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resetVisibilityOnFailure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On error, reset visibility to 0 → immediate redelivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transacted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defer the delete into the route transaction (&lt;code&gt;.Transacted()&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;attributeNames&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;All&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which system attributes to request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messageAttributeNames&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;All&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which message attributes to request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pause (ms) after an empty receive before polling again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initialDelay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delay (ms) before the first poll&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  SQS — producer (&lt;code&gt;.To("sqs://...")&lt;/code&gt;)
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;delaySeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delivery delay (0–900). FIFO queues reject a per-message delay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messageGroupId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;FIFO group id. A constant or a &lt;code&gt;${...}&lt;/code&gt; expression (per message)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messageDeduplicationId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;FIFO dedup id. Skip it if the queue has content-based dedup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enableBatch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send an &lt;code&gt;IEnumerable&lt;/code&gt; body as one &lt;code&gt;SendMessageBatch&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;batchMaxMessages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Batch size (SQS's hard cap is 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;autoCreateQueue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create the queue on startup (FIFO if the name ends in &lt;code&gt;.fifo&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queueUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;An explicit queue URL instead of resolving by name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  SNS — publisher (&lt;code&gt;.To("sns://...")&lt;/code&gt;)
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;autoCreateTopic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create the topic on startup (FIFO on &lt;code&gt;.fifo&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;topicArn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;An explicit topic ARN instead of resolving by name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;subject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Subject (for email delivery). Supports &lt;code&gt;${...}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messageStructure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;json&lt;/code&gt; — per-protocol payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;messageGroupId&lt;/code&gt; / &lt;code&gt;messageDeduplicationId&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;For FIFO topics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;subscribeSnsToSqs&lt;/code&gt; + &lt;code&gt;subscribeQueueArn&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On startup, subscribe an SQS queue (by ARN) to this topic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rawMessageDelivery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On that subscription, deliver the bare payload (see the pub/sub recipe)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Headers and tracing through the broker
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sqs://&lt;/code&gt; consumer stamps message metadata onto the headers (prefixed &lt;code&gt;redbSqs.&lt;/code&gt;), and incoming &lt;strong&gt;message attributes&lt;/strong&gt; land under &lt;code&gt;redbSqs.attr.&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;Header&lt;/th&gt;
&lt;th&gt;What's in it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbSqs.queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The queue name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbSqs.messageId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Message id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbSqs.receiptHandle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Receipt handle (needed to delete or change visibility)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbSqs.approximateReceiveCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many times this message has been delivered ("is this a retry?")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;redbSqs.messageGroupId&lt;/code&gt; / &lt;code&gt;redbSqs.sequenceNumber&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;FIFO group + sequence number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbSqs.sentTimestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When it was sent (epoch millis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbSqs.attr.&amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An incoming user message attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Going the other way, the &lt;strong&gt;header → message-attribute&lt;/strong&gt; mapping on the producer goes like this: user headers ride out as attributes as-is; an incoming &lt;code&gt;redbSqs.attr.&amp;lt;name&amp;gt;&lt;/code&gt; is forwarded under the bare &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; (the prefix is stripped, so sqs→sqs and sqs→sns bridges keep their attributes); the internal &lt;code&gt;redbSqs.*&lt;/code&gt; / &lt;code&gt;redbSns.*&lt;/code&gt; keys are dropped, so one hop's metadata doesn't leak into the next.&lt;/p&gt;

&lt;p&gt;Tracing comes for free: before sending, the producer injects the W3C &lt;code&gt;traceparent&lt;/code&gt;/&lt;code&gt;tracestate&lt;/code&gt; as message attributes via the standard &lt;code&gt;DistributedContextPropagator&lt;/code&gt;, and the consumer on the far end picks them up and continues the trace. Over plain SQS (&lt;code&gt;sqs://&lt;/code&gt; → &lt;code&gt;sqs://&lt;/code&gt;) the chain is unbroken with zero setup. (There's a wrinkle when SNS is in the middle — see the pub/sub recipe.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Straight talk on delivery: at-least-once, visibility, transacted
&lt;/h2&gt;

&lt;p&gt;This is where the marketing likes to say "exactly-once." Let's read the code instead.&lt;/p&gt;

&lt;p&gt;The SQS consumer is &lt;strong&gt;at-least-once&lt;/strong&gt;. A message is deleted (&lt;code&gt;DeleteMessage&lt;/code&gt;) only &lt;strong&gt;after&lt;/strong&gt; it clears the route. The settle logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clean pass + &lt;code&gt;deleteAfterRead=true&lt;/code&gt; → &lt;code&gt;DeleteMessage&lt;/code&gt; (the ack);&lt;/li&gt;
&lt;li&gt;failed + &lt;code&gt;resetVisibilityOnFailure=true&lt;/code&gt; → &lt;code&gt;ChangeMessageVisibility(0)&lt;/code&gt; → it comes right back (fast retry);&lt;/li&gt;
&lt;li&gt;failed without that flag → it simply times out on visibility and gets redelivered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;visibilityTimeout&lt;/code&gt;&lt;/strong&gt; is how long the message is hidden while you work it. Long handler? There's &lt;strong&gt;&lt;code&gt;extendMessageVisibility=true&lt;/code&gt;&lt;/strong&gt;: a background heartbeat bumps the visibility every &lt;code&gt;visibilityTimeout / 2&lt;/code&gt;, so slow work never triggers a redelivery out from under you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;transacted=true&lt;/code&gt;&lt;/strong&gt; rolls the ack into the route transaction. The delete doesn't happen immediately — a deferred &lt;code&gt;SqsAckAction&lt;/code&gt; is registered, whose &lt;code&gt;Commit&lt;/code&gt; deletes the message and whose &lt;code&gt;Rollback&lt;/code&gt; resets visibility to 0 (immediate redelivery). Commit and rollback ride along with your redb work at the &lt;code&gt;.Transacted()&lt;/code&gt; boundary:&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;"sqs://orders?transacted=true&amp;amp;visibilityTimeout=60"&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;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="p"&gt;=&amp;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;SaveAsync&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;ex&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;   &lt;span class="c1"&gt;// write to the DB&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;// DB committed → the message is deleted; DB failed → it comes back&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's &lt;strong&gt;not&lt;/strong&gt; here is cross-queue "read from A, write to B" atomicity at the broker level — SQS doesn't have it to give. Crash in the gap between "work done" and "delete," and after a restart you get a &lt;strong&gt;repeat&lt;/strong&gt; — at-least-once, not exactly-once. So make your handler idempotent; &lt;code&gt;redbSqs.approximateReceiveCount&lt;/code&gt; is there to help. The honest label is "at-least-once with a transactional ack at the route level" — no exactly-once fairy dust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ordering&lt;/strong&gt; holds only on a FIFO queue (&lt;code&gt;.fifo&lt;/code&gt;) and only with &lt;code&gt;concurrentConsumers=1&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recipe 1: producer, consumer, FIFO, batch
&lt;/h2&gt;

&lt;p&gt;The bare minimum:&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 — long-poll, delete after success&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;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&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;WaitTimeSeconds&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;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HandleOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Producer — send the body to the queue&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;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&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;Region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FIFO — the name ends in &lt;code&gt;.fifo&lt;/code&gt;, and a group id is required (a constant or a per-message expression):&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;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders.fifo"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MessageGroupId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${header.customerId}"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batch — an &lt;code&gt;IEnumerable&lt;/code&gt; body goes out as a single &lt;code&gt;SendMessageBatch&lt;/code&gt; (in chunks of &lt;code&gt;batchMaxMessages&lt;/code&gt;, SQS's cap being 10):&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://tick?period=5000"&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;_&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="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="s"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&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="n"&gt;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&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;EnableBatch&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;   &lt;span class="c1"&gt;// one call instead of five&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Recipe 2: competing consumers (concurrency)
&lt;/h2&gt;

&lt;p&gt;One parameter — &lt;code&gt;concurrentConsumers&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="n"&gt;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&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;ConcurrentConsumers&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="nf"&gt;MaxNumberOfMessages&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="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HandleOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// up to 8 messages handled at once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;SQS-native&lt;/strong&gt; concurrency model: the connector spins up N independent receive loops (one task each), and each pulls and handles its own message. Setting &lt;code&gt;maxNumberOfMessages=1&lt;/code&gt; alongside it makes each of the N workers hold exactly one message at a time, so the pool saturates honestly instead of one loop grabbing a batch of 10. The default is &lt;code&gt;1&lt;/code&gt; (strictly serial). Go &lt;code&gt;N &amp;gt; 1&lt;/code&gt; and per-queue ordering is off the table, and your handler had better be thread-safe.&lt;/p&gt;

&lt;p&gt;How much this actually parallelizes shows up in the &lt;code&gt;SqsRpcDemo&lt;/code&gt; (more on it below): 12 requests, roughly 300 ms of work each, a pool of 4 — the whole batch clears in about 1.2 s instead of the ~3.6 s a serial consumer would spend, and the measured peak concurrency pins at exactly 4.&lt;/p&gt;

&lt;p&gt;And if what you want to parallelize is the &lt;em&gt;processing&lt;/em&gt; inside the route rather than the &lt;em&gt;intake&lt;/em&gt; (a serial source, heavy work), there's an orthogonal EIP step, &lt;code&gt;.Threads(N)&lt;/code&gt;. &lt;code&gt;concurrentConsumers&lt;/code&gt; scales reading off the queue; &lt;code&gt;.Threads(N)&lt;/code&gt; scales the work; the &lt;code&gt;CONCURRENCY.md&lt;/code&gt; guide walks through the difference. They compose freely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recipe 3: RPC (request/reply) — and why SQS won't hand it to you
&lt;/h2&gt;

&lt;p&gt;Here's the honest contrast with the last post. On RabbitMQ, RPC flipped on with &lt;strong&gt;one flag&lt;/strong&gt; — &lt;code&gt;replyTo=true&lt;/code&gt; on the client and &lt;strong&gt;zero&lt;/strong&gt; config on the server, because the broker does reply queues itself. &lt;strong&gt;SQS has none of that&lt;/strong&gt; — no protocol-level reply-to, no built-in waiting for an answer. RPC over SQS is something you build, using the classic correlation pattern: a &lt;strong&gt;reply queue&lt;/strong&gt;, a &lt;code&gt;correlationId&lt;/code&gt;, and matching the answer back. That's exactly what the &lt;code&gt;SqsRpcDemo&lt;/code&gt; shows.&lt;/p&gt;

&lt;p&gt;The client tags each request with two message attributes — &lt;code&gt;correlationId&lt;/code&gt; and &lt;code&gt;replyTo&lt;/code&gt; (the queue to answer on) — and drops it on the request queue. The worker computes a result and sends it to the queue named in &lt;code&gt;replyTo&lt;/code&gt;; the &lt;code&gt;correlationId&lt;/code&gt; rides back on its own (the incoming &lt;code&gt;redbSqs.attr.correlationId&lt;/code&gt; is forwarded straight back out as an attribute). The worker is a plain consumer plus a dynamic &lt;code&gt;.ToD(...)&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;// Worker: N competing consumers on the request queue, reply to the queue named in replyTo&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;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rpc-requests"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ConcurrentConsumers&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="nf"&gt;MaxNumberOfMessages&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="nf"&gt;Process&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;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;n&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="nf"&gt;Parse&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="nf"&gt;ToString&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;300&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;// simulated work&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="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="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&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="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// the reply body&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;// dynamic destination — the reply queue comes from the request's replyTo attribute&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToD&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ReplyTo&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="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the client side a separate route reads the reply queue and matches the &lt;code&gt;correlationId&lt;/code&gt; back to the pending 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="n"&gt;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rpc-replies"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;MaxNumberOfMessages&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;Process&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;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Attr&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;"correlationId"&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;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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Pending&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;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;tcs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;tcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrySetResult&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="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="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;The takeaway: RPC on SQS works, but it's &lt;strong&gt;assembled from primitives&lt;/strong&gt; (attributes + &lt;code&gt;.ToD&lt;/code&gt; + correlation), not toggled with a flag. When you want RPC out of the box, you reach for a broker with a native reply path (RabbitMQ). SQS is honest about what it is: durable queues and redelivery, and request/reply is a thing you wire up on top. The full, runnable version lives in &lt;code&gt;redb.Route/demos/SqsRpcDemo&lt;/code&gt; (it runs against LocalStack).&lt;/p&gt;




&lt;h2&gt;
  
  
  Recipe 4 (EIP): Publish-Subscribe via SNS→SQS fan-out
&lt;/h2&gt;

&lt;p&gt;Here's the headline pattern, the whole reason SQS keeps SNS company. &lt;strong&gt;Publish-Subscribe Channel&lt;/strong&gt;, straight out of Hohpe &amp;amp; Woolf: the publisher sends &lt;strong&gt;one&lt;/strong&gt; message and &lt;strong&gt;every&lt;/strong&gt; subscriber gets it — independently, into its own queue, with its own retries. At AWS you don't do this in code, you do it with topology: an SNS topic with several SQS queues subscribed to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;publish "order" ──▶ SNS topic ──┬──▶ SQS "orders-billing"  ──▶ billing route
                                 └──▶ SQS "orders-shipping" ──▶ shipping route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The publisher just publishes to the topic, and every subscribed queue gets a copy — each with its own independent consumer:&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;// Publisher — publish an order event; SNS fans it out to every subscribed queue:&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;"timer://orders?period=5000"&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;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BuildOrder&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;Sns&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-events"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Two independent subscribers — each its own SQS queue, each its own consumer:&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;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders-billing"&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;Charge&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;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders-shipping"&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;Ship&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subscriptions themselves the connector can set up from the URI — &lt;code&gt;subscribeSnsToSqs&lt;/code&gt; + the queue ARN; the subscribe runs when the &lt;code&gt;sns://&lt;/code&gt; producer starts. Each &lt;code&gt;sns://&lt;/code&gt; endpoint subscribes &lt;strong&gt;one&lt;/strong&gt; queue (a single ARN), so a two-queue fan-out is two subscriptions on the one topic:&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;// Subscribe each queue to the topic with raw delivery (bare payload, no envelope):&lt;/span&gt;
&lt;span class="n"&gt;Sns&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-events"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;SubscribeSnsToSqs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billingArn&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;RawMessageDelivery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;Sns&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-events"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;SubscribeSnsToSqs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shippingArn&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;RawMessageDelivery&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the exact fan-out wiring (creating the queues, reading their ARNs, subscribing, publishing), see &lt;code&gt;redb.Route/demos/SqsPubSubDemo&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚠️ The catch: the envelope vs. raw
&lt;/h3&gt;

&lt;p&gt;By default SNS does &lt;strong&gt;not&lt;/strong&gt; drop your payload into the queue as-is — it wraps it in a &lt;strong&gt;JSON notification envelope&lt;/strong&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;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Notification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MessageId"&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;"TopicArn"&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;"Message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your payload, as a string&amp;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;"MessageAttributes"&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="err"&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;So the subscribing queue receives the &lt;strong&gt;envelope&lt;/strong&gt;, not your message, and your SNS attributes sit inside it rather than as SQS attributes. That's standard AWS SNS behavior (the subscription's &lt;strong&gt;Raw Message Delivery = OFF&lt;/strong&gt;), and it earns its keep in exactly one case: when a single queue listens to several topics and you need the envelope's &lt;code&gt;TopicArn&lt;/code&gt; to tell them apart. For an ordinary fan-out it's just in the way — you end up unwrapping the envelope, and the SNS→SQS trace breaks (the &lt;code&gt;traceparent&lt;/code&gt; goes inside the envelope, while the SQS consumer looks for it among the SQS attributes).&lt;/p&gt;

&lt;p&gt;The fix is the subscription's &lt;strong&gt;Raw Message Delivery = true&lt;/strong&gt;: the queue gets the &lt;strong&gt;bare payload&lt;/strong&gt;, and the SNS attributes become SQS attributes (and the trace is whole again). In the connector that's &lt;code&gt;.RawMessageDelivery()&lt;/code&gt; — it sets &lt;code&gt;RawMessageDelivery=true&lt;/code&gt; on that &lt;code&gt;subscribeSnsToSqs&lt;/code&gt; subscription. The &lt;code&gt;SqsPubSubDemo&lt;/code&gt; prints the first delivered body: with &lt;code&gt;.RawMessageDelivery()&lt;/code&gt; it's &lt;code&gt;{"orderId":1,"amount":100}&lt;/code&gt;, not an envelope.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On &lt;code&gt;rawMessageDelivery&lt;/code&gt;.&lt;/strong&gt; This option lands in the &lt;strong&gt;next NuGet release&lt;/strong&gt; of &lt;code&gt;redb.Route.Sqs&lt;/code&gt;. You can already grab it from source right now — &lt;a href="https://github.com/redbase-app/redb-route/tree/main/redb.Route.Sqs" rel="noopener noreferrer"&gt;the &lt;code&gt;redb.Route.Sqs&lt;/code&gt; connector on GitHub&lt;/a&gt; (the connector folder — &lt;code&gt;SnsEndpointOptions&lt;/code&gt; / &lt;code&gt;SnsProducer&lt;/code&gt; / the &lt;code&gt;RawMessageDelivery&lt;/code&gt; fluent method). Without it, SNS→SQS still works; you just get the JSON envelope and unwrap it yourself (&lt;code&gt;envelope.Message&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Recipe 5 (a cousin): Multicast — the same fan-out, but inside the route
&lt;/h2&gt;

&lt;p&gt;Publish-Subscribe copies the message &lt;strong&gt;on the broker&lt;/strong&gt; (SNS hands a copy to each subscribed queue). There's a close cousin that copies &lt;strong&gt;inside the route&lt;/strong&gt; — &lt;strong&gt;Multicast&lt;/strong&gt;: your step sends a copy of the exchange to several destinations 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="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqs://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;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;"sqs://orders-billing"&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;"sqs://orders-shipping"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks similar, but the difference is fundamental — &lt;strong&gt;where the copying happens, and who knows about whom&lt;/strong&gt;:&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;SNS→SQS (pub/sub)&lt;/th&gt;
&lt;th&gt;Multicast&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where it copies&lt;/td&gt;
&lt;td&gt;on the broker (SNS)&lt;/td&gt;
&lt;td&gt;in the route (your process)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who knows the recipients&lt;/td&gt;
&lt;td&gt;SNS (the subscriptions)&lt;/td&gt;
&lt;td&gt;the route (the &lt;code&gt;.To&lt;/code&gt; list)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adding a recipient&lt;/td&gt;
&lt;td&gt;subscribe another queue, leave the publisher alone&lt;/td&gt;
&lt;td&gt;edit the route's code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coupling&lt;/td&gt;
&lt;td&gt;fully decoupled&lt;/td&gt;
&lt;td&gt;recipients are baked into the route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallelism / aggregating replies&lt;/td&gt;
&lt;td&gt;none (fire-and-forget per subscription)&lt;/td&gt;
&lt;td&gt;yes: &lt;code&gt;.Parallel().MaxParallelism(N)&lt;/code&gt;, merge the branches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule of thumb: need to &lt;strong&gt;decouple the publisher from the subscribers&lt;/strong&gt; (anyone can subscribe later, the publisher never knows) — SNS→SQS. Need to &lt;strong&gt;fan out to a fixed list right here and maybe collect the answers&lt;/strong&gt; — Multicast (or its sibling Scatter-Gather from the Kafka post). One is broker topology, the other is a route step; you pick by where the fan-out logic should live.&lt;/p&gt;




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

&lt;p&gt;SQS and SNS in redb.Route are routes where the whole queue and the whole topic are one URI: &lt;code&gt;From("sqs://…")&lt;/code&gt; / &lt;code&gt;.To("sqs://…")&lt;/code&gt; / &lt;code&gt;.To("sns://…")&lt;/code&gt;, with all the AWS SDK plumbing, the receive/delete dance, visibility, subscriptions and correlation tucked behind the string's parameters. Long-poll, FIFO, batching, competing consumers on a single parameter, at-least-once with a transactional ack, SNS→SQS fan-out with a choice of "envelope or bare payload" — the set is complete, and what you're left with is a short, legible string.&lt;/p&gt;

&lt;p&gt;Straight talk on the trade-offs: there's no exactly-once (SQS itself doesn't offer it) — you get at-least-once plus idempotency; RPC isn't out of the box, it's assembled from attributes and &lt;code&gt;.ToD&lt;/code&gt;; and an SNS subscription wraps your payload in an envelope by default. All of that is AWS being AWS, not the connector cutting corners — and the connector is upfront about every bit of it.&lt;/p&gt;

&lt;p&gt;Two runnable examples — &lt;code&gt;redb.Route/demos/SqsRpcDemo&lt;/code&gt; (RPC + concurrency) and &lt;code&gt;redb.Route/demos/SqsPubSubDemo&lt;/code&gt; (SNS→SQS fan-out + raw delivery); both come up on LocalStack (&lt;code&gt;http://localhost:4566&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;/&lt;code&gt;test&lt;/code&gt;) and run as a self-test. If something bites in your scenario, say so in the comments.&lt;/p&gt;

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




&lt;p&gt;If this was useful — a ⭐ on &lt;a href="https://github.com/redbase-app/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps others find it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aws</category>
      <category>sqs</category>
    </item>
    <item>
      <title>redb.Identity, the full tour: a transport-agnostic OAuth 2.1 / OpenID Connect server for .NET — protocol transport, and no migrations</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Tue, 14 Jul 2026 15:35:16 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redbidentity-the-full-tour-a-transport-agnostic-oauth-21-openid-connect-server-for-net--1bcg</link>
      <guid>https://dev.to/rinat_kozin/redbidentity-the-full-tour-a-transport-agnostic-oauth-21-openid-connect-server-for-net--1bcg</guid>
      <description>&lt;p&gt;series  redb.Route&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%2F1k3iclxtbnvl5yp76grp.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%2F1k3iclxtbnvl5yp76grp.png" alt="redb.Identity" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up — this is the long one.&lt;/strong&gt; The most complete write-up of redb.Identity: from the inside, with code, no shortcuts. Budget around an 30min — it really is that long. It covers the full standards catalogue, audited line by line, the production-grade internals (live JWKS rotation with no restart, cluster-safe primitives, the password-hashing pipeline), the audit pipeline, logical-level backup, host-agnostic deployment, and the addon architecture that lets your own service sit next to Identity in the same database and transaction. Want the overview instead? There's a shorter version. Otherwise — grab a coffee.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are three usual ways to do OAuth/OIDC in .NET, and every one of them makes you give something up.&lt;/p&gt;

&lt;p&gt;The first is the &lt;strong&gt;ASP.NET-bound&lt;/strong&gt; camp — Duende IdentityServer, ASP.NET Identity, the OpenIddict samples. Powerful, but every endpoint is an HTTP middleware. Want to call &lt;code&gt;token&lt;/code&gt; from a worker service or a bus consumer? Spin up an HTTP listener and loop back to yourself. Want to test the issuance pipeline in isolation? Reach for &lt;code&gt;WebApplicationFactory&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second is the &lt;strong&gt;full IAM platforms&lt;/strong&gt; — Keycloak, Auth0, Okta. Feature-rich, but each one is a standalone service with its own runtime, its own admin UI, its own database, its own config model, its own deployment story. Multi-tenant, sure. Embeddable, no.&lt;/p&gt;

&lt;p&gt;The third is &lt;strong&gt;roll your own&lt;/strong&gt; — and reimplement Code+PKCE, refresh-token rotation, consent storage, session revocation, MFA replay protection, JWKS rotation, key-ring sharing across replicas, and RFC 8417 back-channel logout, for the third time this decade.&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%2Fht6iikoor4u0j1m34tic.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%2Fht6iikoor4u0j1m34tic.png" alt="redb.Identity" width="800" height="978"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;redb.Identity is the missing fourth option, and it comes down to one word: transport-agnostic. The protocol is decoupled from the transport.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is, first and foremost, a &lt;strong&gt;full OAuth 2.1 / OIDC server&lt;/strong&gt; — stand it up over HTTP, expose it to external clients, discovery, JWKS, the works. It's just that internally every endpoint is a redb.Route route, not an HTTP middleware. And &lt;code&gt;direct-vm://&lt;/code&gt; is redb.Route's &lt;strong&gt;internal, cross-context transport&lt;/strong&gt; — same family as &lt;code&gt;http://&lt;/code&gt;, &lt;code&gt;rabbitmq://&lt;/code&gt;, &lt;code&gt;kafka://&lt;/code&gt;, except the network hop is swapped for a call between &lt;code&gt;RouteContext&lt;/code&gt;s in-process.&lt;/p&gt;

&lt;p&gt;Because of that, the same &lt;code&gt;token&lt;/code&gt; can be reached over HTTP, over gRPC, over a queue — or, if the caller lives in the same worker, with no network at all, over that same internal transport. That last one — "no network hop, just a call from the next context" — is what gives you the embedded mode. But it's a &lt;em&gt;consequence&lt;/em&gt; of being transport-agnostic, not the whole story: most of the time Identity runs as a normal HTTP server, and the in-process call is a nice bonus where you want it. The standards conformance and storage model are a full IS server's; the only thing that changes is which transport you reach it over.&lt;/p&gt;

&lt;p&gt;It's open source under Apache 2.0 as of today: &lt;a href="https://github.com/redbase-app/redb-identity" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-identity&lt;/a&gt;, packages on nuget.org (&lt;code&gt;redb.Identity.*&lt;/code&gt;, version 1.2.0). What follows is the substance — code, no marketing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part of the redb / redb.Route series.&lt;/strong&gt; Identity sits on top of the whole stack, so if a layer&lt;br&gt;
below is unfamiliar, these are the ones worth having open in a tab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The runtime it ships in&lt;/strong&gt; — &lt;a href="https://dev.to/rinat_kozin/redb-330-an-enterprise-net-stack-you-actually-own-typed-store-a-homegrown-apache-camel-and-3gd1"&gt;redb 3.3.0: an enterprise .NET stack you actually own&lt;/a&gt;, and &lt;a href="https://dev.to/rinat_kozin/two-routes-in-an-evening-from-a-debug-worker-to-an-enterprise-runtime-with-redbroute-tsak-m8k"&gt;Two routes in an evening: from a debug worker to an enterprise runtime&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The router every endpoint here is a route on&lt;/strong&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&lt;/a&gt;, and the &lt;a href="https://dev.to/rinat_kozin/the-rabbitmq-connector-in-redbroute-the-whole-broker-in-one-uri-rpc-competing-consumers-18m0"&gt;RabbitMQ connector&lt;/a&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;Kafka connector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The store that makes "no migrations" true&lt;/strong&gt; — &lt;a href="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"&gt;REDB inside, part 1: the 13 tables the whole engine runs on&lt;/a&gt; and &lt;a href="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"&gt;part 1.1: why they stay fast&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;profile&lt;/a&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;/blockquote&gt;




&lt;h2&gt;
  
  
  Skipping ahead: the receipts
&lt;/h2&gt;

&lt;p&gt;A post like this is easy to write and hard to trust. "Full OAuth 2.1 / OIDC server" is a sentence anyone can type. So before the architecture, before a single line of code — here is the part you'd otherwise have to take on faith.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;the official OpenID Foundation conformance suite&lt;/strong&gt; — the same suite the OIDF uses to certify providers — run against a live redb.Identity over native HTTPS. Not our tests. Theirs.&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%2Fsnf93g4si4qznwqhdz86.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%2Fsnf93g4si4qznwqhdz86.png" alt="redb.Identity" width="800" height="916"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic OP: 35 modules, zero failures.&lt;/strong&gt; The 4 REVIEW modules pass — the suite just wants a human to upload a screenshot as certification evidence. The 1 SKIPPED is request objects, which we honestly don't advertise. The 1 WARNING is two extra claims in the id_token that we put there on purpose, and I explain exactly why further down rather than hiding it.&lt;/p&gt;

&lt;p&gt;And it was no rubber stamp. The suite found &lt;strong&gt;real defects in our server&lt;/strong&gt; — including PII leaking into the id_token — and they're all fixed. That story is in the conformance section, unvarnished.&lt;/p&gt;

&lt;p&gt;One thing I won't claim: we do &lt;strong&gt;not&lt;/strong&gt; carry the &lt;strong&gt;OpenID Certified™&lt;/strong&gt; mark. That's a trademark, awarded through a formal (and paid) submission to the OIDF. What's true is exactly what's on the screen — the server is run against the official suite, and the results are in the repo.&lt;/p&gt;

&lt;p&gt;Now, the architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  The layers underneath, in one breath
&lt;/h2&gt;

&lt;p&gt;redb.Identity doesn't stand on nothing — it stands on our ecosystem. Very briefly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;redb&lt;/strong&gt; — a typed store for .NET. You write a plain POCO, tag it with &lt;code&gt;[RedbScheme]&lt;/code&gt;, and work with it through full LINQ, server-side, &lt;strong&gt;with no migrations&lt;/strong&gt; and no &lt;code&gt;Include&lt;/code&gt;. Providers: PostgreSQL, MSSQL, SQLite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;redb.Route&lt;/strong&gt; — an integration engine in the Apache Camel spirit: &lt;code&gt;From(...)…​.To(...)&lt;/code&gt; routes, 30+ connectors, enterprise integration patterns, transactions, telemetry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;redb.Tsak&lt;/strong&gt; — a runtime that turns those routes into a production service: dashboard, hot-reloadable modules (&lt;code&gt;.tpkg&lt;/code&gt;), a cluster with a coordinator.&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%2F22nyp2stbh51im3t38n7.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%2F22nyp2stbh51im3t38n7.png" alt="redb.Tsak" width="800" height="672"&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%2Ftq7drt7jludc53aoazul.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%2Ftq7drt7jludc53aoazul.png" alt="redb.Tsak" width="800" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;redb.Identity is the fourth layer on top. It &lt;strong&gt;doesn't own a database schema&lt;/strong&gt; and &lt;strong&gt;doesn't make its own HTTP stack mandatory&lt;/strong&gt;. It takes the redb.Route engine, drops the OpenIddict pipeline onto it, and hands storage to redb. What falls out of that is the rest of this post.&lt;/p&gt;

&lt;p&gt;All the code samples are in English (they always were); everything's in the repo, so you can reproduce it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Killer feature #1: protocol ≠ transport
&lt;/h2&gt;

&lt;p&gt;Every public endpoint in Identity is registered on a &lt;code&gt;direct-vm://identity-*&lt;/code&gt; route. &lt;code&gt;direct-vm&lt;/code&gt; is redb.Route's in-process, cross-context, &lt;strong&gt;synchronous zero-copy&lt;/strong&gt; transport. Three things fall out of that for free.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Another module in the same worker calls Identity with no network at all
&lt;/h3&gt;

&lt;p&gt;Picture a single Tsak worker running Identity next to your business module. The business module needs a service-account token to reach a downstream API. In the ASP.NET world that's an HTTP call to yourself over loopback. Here it's a plain in-process 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="c1"&gt;// Inside another .tpkg module loaded into the same Tsak worker&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;CheckoutRoutes&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RouteBuilder&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IProducerTemplate&lt;/span&gt; &lt;span class="n"&gt;_identity&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="c1"&gt;// request-reply into Identity, in-process&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_identity&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;Context&lt;/span&gt;&lt;span class="p"&gt;!);&lt;/span&gt;
        &lt;span class="n"&gt;_identity&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="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rabbitmq:checkout.orders"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Need a service-account access token to call a downstream API?&lt;/span&gt;
            &lt;span class="c1"&gt;// Just hit Identity's token endpoint right here — same process, no socket.&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="k"&gt;async&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;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;token&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;_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TokenResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;IdentityEndpoints&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="c1"&gt;// "direct-vm://identity-token"&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;grant_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"client_credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"checkout-svc"&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="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="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&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="n"&gt;AccessToken&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="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://pricing-api/quote"&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;strong&gt;What's happening here.&lt;/strong&gt; The message travelling down the route is the &lt;strong&gt;order&lt;/strong&gt; off the queue — that's the body. Halfway through we make a &lt;strong&gt;side call&lt;/strong&gt;: ask Identity for a service-account token, take &lt;code&gt;access_token&lt;/code&gt; out of the reply, and hang it on the current message as a &lt;strong&gt;header&lt;/strong&gt;. The order itself is untouched. At the end the order — now carrying &lt;code&gt;Authorization&lt;/code&gt; — goes off to somebody else's API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the token is fetched inside &lt;code&gt;.Process(...)&lt;/code&gt; rather than with &lt;code&gt;.To(IdentityEndpoints.Token)&lt;/code&gt; in the DSL&lt;/strong&gt; is a fair question, and the answer matters. Because &lt;code&gt;.To()&lt;/code&gt; is a &lt;strong&gt;pipeline&lt;/strong&gt; step: the reply becomes the &lt;strong&gt;new body&lt;/strong&gt;. Write 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="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rabbitmq:checkout.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;IdentityEndpoints&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="c1"&gt;// ← the body is no longer the order, it's a TokenResponse&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://pricing-api/quote"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← pricing-api receives the TOKEN instead of the order&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— and &lt;strong&gt;the order is gone&lt;/strong&gt;: the downstream API gets a JSON blob with an access_token where the cart items should be.&lt;/p&gt;

&lt;p&gt;What you want instead is to step aside, take one field out of the reply, attach it to the current message and &lt;strong&gt;leave the body alone&lt;/strong&gt;. That's the &lt;strong&gt;Content Enricher&lt;/strong&gt; EIP, and &lt;code&gt;RequestBody(...)&lt;/code&gt; inside a processor is the canonical way to express it (in Apache Camel it's exactly &lt;code&gt;ProducerTemplate.requestBody&lt;/code&gt;). redb.Route does have a declarative &lt;code&gt;.Enrich(uri, merge)&lt;/code&gt; — but it forwards the &lt;strong&gt;current&lt;/strong&gt; exchange to the resource, so the order would land on the token endpoint, and Identity expects &lt;code&gt;grant_type=client_credentials&lt;/code&gt;. Enrich is for "enrich &lt;em&gt;this&lt;/em&gt; message"; ours is a side call &lt;strong&gt;with a different payload&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And yes, that last line really is &lt;code&gt;http://&lt;/code&gt;&lt;/strong&gt; — as it should be. &lt;code&gt;pricing-api&lt;/code&gt; is somebody else's service; you reach it over the network and no architecture changes that. The point is that there is now &lt;strong&gt;one&lt;/strong&gt; network call instead of two:&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;ASP.NET-bound IS&lt;/th&gt;
&lt;th&gt;here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;get a token&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;HTTP to yourself&lt;/strong&gt; (loopback)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;direct-vm://&lt;/code&gt; — a method call, no network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;call pricing-api&lt;/td&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;HTTP (unavoidable)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We removed the &lt;strong&gt;first&lt;/strong&gt; one — the one that should never have existed. Making an HTTP request to your own process, serializing JSON and shaking TLS hands with yourself, is a tax you pay purely because &lt;code&gt;token&lt;/code&gt; happens to be an HTTP middleware.&lt;/p&gt;

&lt;p&gt;No HTTP listener. No TLS handshake. No JSON over the loopback. The exchange flows straight from your route into the Identity processor and back — same thread, same &lt;code&gt;IExchange&lt;/code&gt; instance. Total cost: a method call plus the OpenIddict work you'd be doing anyway.&lt;/p&gt;

&lt;p&gt;This is exactly the thing ASP.NET-bound IS servers can't do: for them &lt;code&gt;token&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; an HTTP middleware, and calling it "from the inside" without a socket isn't a thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Facades are pure transport adapters — take the ones you need
&lt;/h3&gt;

&lt;p&gt;HTTP is the &lt;strong&gt;first&lt;/strong&gt; facade, not the only one and not a requirement. A facade is a thin bridge with &lt;strong&gt;no business logic&lt;/strong&gt; in 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="c1"&gt;// HTTP facade — ships in 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="n"&gt;Http&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;"0.0.0.0:5000"&lt;/span&gt;&lt;span class="p"&gt;))&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;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TokenController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// POST /connect/token&lt;/span&gt;
    &lt;span class="c1"&gt;// ↓ The controller's entire job:&lt;/span&gt;
    &lt;span class="c1"&gt;//   exchange.To(IdentityEndpoints.Token)   // direct-vm://identity-token&lt;/span&gt;

&lt;span class="c1"&gt;// gRPC facade (planned) — same pattern&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;Grpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0.0.0.0:5001/IdentityService"&lt;/span&gt;&lt;span class="p"&gt;))&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;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="nf"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"grpc.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;"Token"&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;IdentityEndpoints&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="c1"&gt;// RabbitMQ RPC facade (planned)&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;"rabbitmq:identity.rpc.token"&lt;/span&gt;&lt;span class="p"&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;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IdentityEndpoints&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a transport is dropping another &lt;code&gt;.tpkg&lt;/code&gt; that points at &lt;code&gt;direct-vm://&lt;/code&gt;. Core never gets touched. Removing a transport is &lt;code&gt;rm facade.tpkg&lt;/code&gt;. There's nothing in a facade to break — no logic lives there.&lt;/p&gt;

&lt;p&gt;Under the hood the shipped HTTP facade (&lt;code&gt;redb.Identity.Http&lt;/code&gt;) is exactly that thin: parse, forward, serialize.&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;// HttpFacadeRouteBuilder.cs&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:&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;/connect/token?inOut=true&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;ClientCorsParams&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;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http-token"&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;HttpIdentityProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropagateCorrelationId&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;ClientAuthHttpProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtractClientCredentials&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;IdentityEndpoints&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="c1"&gt;// ← forward into direct-vm&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;HttpIdentityProcessors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SerializeJsonResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want to swap HTTP for RabbitMQ entirely? In an ASP.NET stack that's a rewrite. Here it's &lt;strong&gt;drop the HTTP facade, keep Core.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A browser shows up exactly where the spec demands one
&lt;/h3&gt;

&lt;p&gt;The only OAuth interaction that fundamentally needs a browser is the authorization-code redirect (and its sibling, the device-code verification URL). Everything else — &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;refresh_token&lt;/code&gt;, &lt;code&gt;introspect&lt;/code&gt;, &lt;code&gt;revoke&lt;/code&gt;, &lt;code&gt;userinfo&lt;/code&gt;, the device_code request, the management API, MFA verify, SCIM — is transport-neutral and works over gRPC, AMQP, MQ, or a direct call.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flow / endpoint&lt;/th&gt;
&lt;th&gt;Needs HTTP + browser&lt;/th&gt;
&lt;th&gt;Works on any transport&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;client_credentials&lt;/code&gt; (M2M)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;authorization_code&lt;/code&gt; — redirect to &lt;code&gt;/authorize&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;authorization_code&lt;/code&gt; — &lt;code&gt;code → token&lt;/code&gt; exchange&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;refresh_token&lt;/code&gt;, &lt;code&gt;revoke&lt;/code&gt;, &lt;code&gt;introspect&lt;/code&gt;, &lt;code&gt;userinfo&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;device_code&lt;/code&gt; — initial request&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;device_code&lt;/code&gt; — user verification URL&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Management API, SCIM 2.0, audit query&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The same &lt;code&gt;TokenEndpointProcessor&lt;/code&gt; runs whether the call came in over HTTP, gRPC, RabbitMQ, or a direct &lt;code&gt;direct-vm&lt;/code&gt; call from the next module. &lt;strong&gt;One pipeline, one set of tests, one audit trail.&lt;/strong&gt; In practice that also buys you cheap integration tests (most of the 1768-test suite drives &lt;code&gt;direct-vm&lt;/code&gt; and never spins up Kestrel) and multi-port isolation — public OIDC endpoints on one port, the management API and SCIM on another, firewalled independently, same pipeline behind both.&lt;/p&gt;




&lt;h2&gt;
  
  
  Killer feature #2: no migrations
&lt;/h2&gt;

&lt;p&gt;This one comes up from redb, but it lands especially well for an identity server. redb.Identity &lt;strong&gt;doesn't own a schema&lt;/strong&gt;. It sits on redb, where a "table" is a plain C# class with an attribute. Here's a user:&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;// src/redb.Identity.Core/Models/UserProps.cs&lt;/span&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;"identity.user"&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;UserProps&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Standard OIDC profile claims&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;GivenName&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;FamilyName&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;Picture&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;bool&lt;/span&gt;    &lt;span class="n"&gt;EmailVerified&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;// Structured OIDC address (§5.1.1) — a nested redb object, not a JSON blob&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;AddressClaim&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Address&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;// Arbitrary tenant claims — each pair becomes its own props row,&lt;/span&gt;
    &lt;span class="c1"&gt;// queryable and indexable without an ALTER TABLE.&lt;/span&gt;
    &lt;span class="k"&gt;public&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;CustomClaims&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;// Multi-provider federation links — native props rows, hot reverse lookup&lt;/span&gt;
    &lt;span class="c1"&gt;// via RedbObject.value_string = "{providerId}:{sub}".&lt;/span&gt;
    &lt;span class="k"&gt;public&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;ExternalIdentity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;ExternalIdentities&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;ScimExternalId&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;// RFC 7643 §3.1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need to add a field to your user — a &lt;code&gt;LoyaltyTier&lt;/code&gt;, a &lt;code&gt;ManagerSubject&lt;/code&gt;, a &lt;code&gt;DepartmentCode&lt;/code&gt;? You literally add it to the class. No migration, no ticket to a DBA, no downtime. redb reads and writes it the next instant; once it's in production data, queries can filter and project it.&lt;/p&gt;

&lt;p&gt;A word on &lt;code&gt;CustomClaims&lt;/code&gt;. That's not an &lt;code&gt;nvarchar(max)&lt;/code&gt; blob or a &lt;code&gt;jsonb&lt;/code&gt; column you later hand-index with GIN. It's a &lt;strong&gt;&lt;code&gt;Dictionary&amp;lt;string,string&amp;gt;&lt;/code&gt; on props&lt;/strong&gt;, and every key is its own queryable, indexable row. &lt;code&gt;ContainsKey&lt;/code&gt;, the indexer, nested access — all of it runs as native LINQ server-side. A typical EF-Core identity server keeps custom claims either as a blob or normalized into eight satellite tables with joins.&lt;/p&gt;

&lt;p&gt;There's a hot/cold split too: login, password hash, and status live in the relational &lt;code&gt;_users&lt;/code&gt; table (hot keys, narrow indexes), while the cold OIDC profile lives in props rows linked by &lt;code&gt;RedbObject.key = _users._id&lt;/code&gt;. No over-indexed wide rows, no JSON-blob lookups on the hot path.&lt;/p&gt;

&lt;p&gt;The whole thing is composed from &lt;strong&gt;24 typed redb schemes&lt;/strong&gt; (&lt;code&gt;identity.application&lt;/code&gt;, &lt;code&gt;identity.scope&lt;/code&gt;, &lt;code&gt;identity.token&lt;/code&gt;, &lt;code&gt;identity.session&lt;/code&gt;, &lt;code&gt;identity.user&lt;/code&gt;, &lt;code&gt;identity.group&lt;/code&gt;, &lt;code&gt;identity.mfa&lt;/code&gt;, &lt;code&gt;identity.webauthn_*&lt;/code&gt;, &lt;code&gt;identity.federation_provider&lt;/code&gt;, &lt;code&gt;identity.claim_mapper&lt;/code&gt;, &lt;code&gt;identity.dpop_consumed_jti&lt;/code&gt;, &lt;code&gt;identity.signing_key&lt;/code&gt;, …). All plain C# classes. &lt;code&gt;InitializeAsync()&lt;/code&gt; syncs the schema. No migration files.&lt;/p&gt;

&lt;h3&gt;
  
  
  RTTI at the database level — what the storage model actually is
&lt;/h3&gt;

&lt;p&gt;Most identity servers spread storage across half a dozen moving parts: EF Core + migrations for users/apps/claims, Redis for session cache and MFA OTP, the filesystem or Azure Key Vault for the DataProtection key ring, a separate table for signing keys, Elasticsearch for the audit log, and something else again for rate-limit counters. Each part is its own client, its own init, its own health check, its own backup plan, its own migration story.&lt;/p&gt;

&lt;p&gt;Here a single store — REDB — handles &lt;strong&gt;all of it&lt;/strong&gt;, because it keeps typed data schemes declared &lt;em&gt;in the database itself&lt;/em&gt; — runtime type information at the DB level:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language concept&lt;/th&gt;
&lt;th&gt;In REDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;class Foo { int X; string Y; }&lt;/code&gt; (type declaration)&lt;/td&gt;
&lt;td&gt;a row in &lt;code&gt;_schemes&lt;/code&gt; describing the fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;new Foo { X = 42, Y = "bar" }&lt;/code&gt; (instance)&lt;/td&gt;
&lt;td&gt;a row in &lt;code&gt;_objects&lt;/code&gt; + typed slots in &lt;code&gt;_values&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;typeof(Foo)&lt;/code&gt; / &lt;code&gt;obj.GetType()&lt;/code&gt; (RTTI)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_objects._id_scheme → _schemes._id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;where T : class, new()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the C#-side &lt;code&gt;RedbObject&amp;lt;TProps&amp;gt;&lt;/code&gt; proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The C# classes (&lt;code&gt;UserProps&lt;/code&gt;, &lt;code&gt;ApplicationProps&lt;/code&gt;, …) are just a type-safe view over the schemes. What that buys the identity server:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One query language for everything.&lt;/strong&gt; Finding a user, a token by &lt;code&gt;reference_id&lt;/code&gt;, an authorization by subject GUID — one &lt;code&gt;IRedbService&lt;/code&gt;, one LINQ provider, one transactional context:&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="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserProps&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;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EmailVerified&lt;/span&gt;&lt;span class="p"&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;o&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;coreUserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&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;TokenProps&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;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="n"&gt;referenceId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every OpenIddict store (&lt;code&gt;RedbApplicationStore&lt;/code&gt;, &lt;code&gt;RedbAuthorizationStore&lt;/code&gt;, &lt;code&gt;RedbTokenStore&lt;/code&gt;, &lt;code&gt;RedbScopeStore&lt;/code&gt;) plus ours (&lt;code&gt;PropsSigningKeyStore&lt;/code&gt;, &lt;code&gt;PropsServerSideOtpStore&lt;/code&gt;, &lt;code&gt;PropsWebAuthnChallengeStore&lt;/code&gt;, &lt;code&gt;PropsPasswordHistoryStore&lt;/code&gt;) sits on &lt;strong&gt;one&lt;/strong&gt; interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One backup story.&lt;/strong&gt; Back up Postgres and you've backed up the &lt;em&gt;entire&lt;/em&gt; identity stack — keys (DataProtection-encrypted in the same DB), sessions, signing keys, audit. There's no "oh, and that one component is still in Redis, back that up separately."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenant isolation via schemes.&lt;/strong&gt; Want tenant A on one shape of &lt;code&gt;UserProps&lt;/code&gt; and tenant B on a slightly different one? Per-tenant schemes — the split is physical, through &lt;code&gt;_id_scheme&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pro mode with PVT.&lt;/strong&gt; The Pro build ships &lt;strong&gt;Precomputed Value Tables&lt;/strong&gt; — materialized indexes for hot-path queries. A lookup on &lt;code&gt;value_string == clientId&lt;/code&gt; goes through the PVT index instead of an unrolled join over &lt;code&gt;_values&lt;/code&gt;. Details in the &lt;a href="https://habr.com/ru/articles/1045208/" rel="noopener noreferrer"&gt;REDB indexing post&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Killer feature #3: the same server — also as an embedded library
&lt;/h2&gt;

&lt;p&gt;This one falls straight out of the first two: once the protocol is decoupled from the transport and &lt;code&gt;direct-vm://&lt;/code&gt; is the internal cross-context transport, one of the available modes is &lt;strong&gt;no network facade at all&lt;/strong&gt;. Not a replacement for "a normal HTTP server" — an extra option for when Identity lives in the same worker as whatever calls it.&lt;/p&gt;

&lt;p&gt;"Stand up an identity server" usually means run a separate process: its own runtime, its own port, its own admin, its own DB, its own deploy. Even the "embeddable" ASP.NET libraries still need an HTTP pipeline to call them.&lt;/p&gt;

&lt;p&gt;With redb.Identity you can take &lt;strong&gt;just the engine&lt;/strong&gt; — &lt;code&gt;redb.Identity.Core&lt;/code&gt; — and zero facades. No Kestrel, no HTTP. You reference the package, bring up the OpenIddict pipeline inside your process, and hit the endpoints over &lt;code&gt;direct-vm://&lt;/code&gt; from your own 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;// In your app: you need a client token for an internal call —&lt;/span&gt;
&lt;span class="c1"&gt;// no network hop, no Identity HTTP server standing up at all.&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="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="c1"&gt;// ctx — your IRouteContext&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;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;token&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="n"&gt;RequestBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TokenResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IdentityEndpoints&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="c1"&gt;// "direct-vm://identity-token"&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;grant_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"client_credentials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"internal-svc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_secret&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That changes the embedding model. The identity server stops being "a service next door" and becomes &lt;strong&gt;a library inside your process&lt;/strong&gt; — one that happens to have a full OAuth 2.1 / OIDC engine under it, but you talk to it with a method call, not a network request. When the day comes to expose those same endpoints to external clients, you add an HTTP facade as one &lt;code&gt;.tpkg&lt;/code&gt;, and the exact same processors start answering over HTTP too. Core doesn't change.&lt;/p&gt;

&lt;p&gt;Neither Duende, nor Keycloak, nor Auth0 does this: the first has HTTP-middleware endpoints, the rest are external services with a network hop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two contexts, one worker
&lt;/h2&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%2Fkr7r0z5yh0gxxvittrld.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%2Fkr7r0z5yh0gxxvittrld.png" alt="redb.Tsak" width="800" height="487"&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%2Finodj6mhre72n4amgc3q.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%2Finodj6mhre72n4amgc3q.png" alt="redb.Tsak" width="799" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Architecturally it's two &lt;code&gt;RouteContext&lt;/code&gt;s inside one Tsak worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────────────────────────────────────────────┐
│                             Tsak worker                                 │
│                                                                         │
│  ┌───────────────────────────────────────────────────────────────────┐ │
│  │  RouteContext "identity"   ← redb.Identity.Core.Module.tpkg        │ │
│  │  ─────────────────────────────────────────────────────────────    │ │
│  │  • ~50 direct-vm:// routes                                         │ │
│  │  • OpenIddict server pipeline (token, authorize, userinfo, ...)    │ │
│  │  • redb stores: Users, Apps, Scopes, Tokens, Sessions, Audit       │ │
│  │  • DataProtection key-ring (RedbXmlRepository)                     │ │
│  │  • Signing keys (RSA 2048, encrypted at rest)                     │ │
│  │  • MFA: TOTP / SMS-Email OTP / WebAuthn / recovery codes           │ │
│  │  • cleanup timers (.Cluster(true) → leader-only in a cluster)      │ │
│  └───────────────────────────────────────────────────────────────────┘ │
│                        ▲  direct-vm:// (synchronous, in-process)         │
│                        │                                                 │
│  ┌───────────────────────────────────────────────────────────────────┐ │
│  │  RouteContext "identity.http"  ← redb.Identity.Http.tpkg           │ │
│  │  • Kestrel → redb.Route.Http → RedbController dispatcher           │ │
│  │  • /connect/token, /authorize, /userinfo, /introspect, ...         │ │
│  │  • /api/v1/identity/* (management), /me/* (self-service)           │ │
│  │  • /scim/v2/Users, /Groups, /Bulk                                  │ │
│  └───────────────────────────────────────────────────────────────────┘ │
│                                                                         │
│  Your modules → call IdentityEndpoints.Token / .ManageUsers / ...       │
│  over direct-vm:// directly, no HTTP                                     │
└───────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
                 redb store (PostgreSQL / MSSQL / SQLite)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth calling out: &lt;strong&gt;project-reference isolation&lt;/strong&gt;. The HTTP facade compiles &lt;strong&gt;without seeing a single type from &lt;code&gt;redb.Identity.Core&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;redb.Identity.Http  ─project-ref→  redb.Identity.Contracts       ✅
redb.Identity.Http  ─project-ref→  redb.Identity.DataProtection  ✅
redb.Identity.Http  ─project-ref→  redb.Identity.Core            ❌ FORBIDDEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The facade only sees wire DTOs and endpoint constants (&lt;code&gt;redb.Identity.Contracts&lt;/code&gt;) plus a couple of SPI interfaces. Core's internal types never leak across the ABI boundary — CI fails the build if a &lt;code&gt;using redb.Identity.Core;&lt;/code&gt; ever shows up under &lt;code&gt;redb.Identity.Http/&lt;/code&gt;. That's what makes the ".tpkg story" honest: the two packages build and version independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anatomy of a token request
&lt;/h3&gt;

&lt;p&gt;Since every endpoint is a route, it's worth seeing what one is made of. The token endpoint isn't a "controller method" — it's a declarative redb.Route pipeline:&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;// IdentityCoreRouteBuilder.tokenRoute (RouteId: identity-token)&lt;/span&gt;
&lt;span class="nf"&gt;WithRedbTx&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;                        &lt;span class="c1"&gt;// atomic write boundary (one tx per issuance)&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;trustedProxy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// sanitize X-Forwarded-For BEFORE rate-limit&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;perIpThrottle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;// per-IP limit (optional)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnException&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// RFC 6749 error → HTTP status mapping&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="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;             &lt;span class="c1"&gt;// per-client token bucket&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Traced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"identity.token-request"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Metered&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;TokenEndpointProcessor&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="n"&gt;timeProvider&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// ← OpenIddict pipeline&lt;/span&gt;
      &lt;span class="c1"&gt;//   ↑ inside: redb stores — RedbTokenStore, RedbApplicationStore, ...&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndTraced&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;"direct-vm://identity-events"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// audit + multicast, fire-and-forget&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things are visible here that ASP.NET usually smears across middleware, filters, and attributes: the transactional boundary (&lt;code&gt;WithRedbTx&lt;/code&gt; — the token is issued and persisted whole, or it rolls back), rate limiting &lt;strong&gt;before&lt;/strong&gt; the request reaches the expensive work, and audit as a branch (&lt;code&gt;WireTap&lt;/code&gt;) that can't block issuance or take it down. Same &lt;code&gt;TokenEndpointProcessor&lt;/code&gt;, whether the call arrived over HTTP, gRPC, or &lt;code&gt;direct-vm&lt;/code&gt;. &lt;strong&gt;One pipeline, one audit trail, one test suite.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Storage: what it buys you over EF-Core-on-tables
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;redb.Identity&lt;/th&gt;
&lt;th&gt;Typical EF-Core identity server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage engines&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL, MSSQL &lt;strong&gt;and&lt;/strong&gt; SQLite from one codebase — swap by the provider package&lt;/td&gt;
&lt;td&gt;one provider per build; switching means rewriting the EF model + migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Schema evolution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no migrations. Add a field to a &lt;code&gt;*Props&lt;/code&gt; class — the scheme picks it up on next &lt;code&gt;InitializeAsync&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;generate a migration, review the SQL, run &lt;code&gt;Update-Database&lt;/code&gt;, hope rollback works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom claims / tenant extensions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dictionary&amp;lt;string,string&amp;gt;? CustomClaims&lt;/code&gt; — each key is a queryable, indexable row&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jsonb&lt;/code&gt;/&lt;code&gt;nvarchar(max)&lt;/code&gt; blob; you write the GIN / computed-column indexes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-provider federation links&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dictionary&amp;lt;string, ExternalIdentity&amp;gt;&lt;/code&gt; — native props rows, hot reverse lookup&lt;/td&gt;
&lt;td&gt;one-to-many join table, scaffolding per provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hot/cold split&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;hot keys in relational &lt;code&gt;_users&lt;/code&gt;; cold profile in props linked by &lt;code&gt;key = _users._id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;either one wide table or eight satellite tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-tenant data isolation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GetRedbService("identity")&lt;/code&gt; — the named instance can target a separate DB / schema / connection&lt;/td&gt;
&lt;td&gt;single &lt;code&gt;DbContext&lt;/code&gt;; isolation means separate ASP.NET apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Providers: zero code change between rows
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;OSS package&lt;/th&gt;
&lt;th&gt;Pro package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL 13+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redb.Postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redb.Postgres.Pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft SQL Server 2019+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redb.MSSql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redb.MSSql.Pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite 3.44+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redb.SQLite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redb.SQLite.Pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Identity references only the &lt;code&gt;redb.Core&lt;/code&gt; OSS abstraction — &lt;strong&gt;the host worker picks the provider&lt;/strong&gt;, Identity code never names one. And this isn't a claim: the &lt;strong&gt;same test suite (1768 tests) is green on all three&lt;/strong&gt; — &lt;code&gt;Passed: 1767, Skipped: 1, Failed: 0&lt;/code&gt; on PostgreSQL, MSSQL and SQLite, flipped with a single &lt;code&gt;REDB_PROVIDER&lt;/code&gt; env-var. (The one skip is a known host-teardown probe on PG, not a product gap.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Standards — the full catalogue
&lt;/h2&gt;

&lt;p&gt;No skimping here, because standards conformance is why you reach for an IS server in the first place. &lt;strong&gt;Close to 40 standards&lt;/strong&gt; — not "mentioned in the code," but implemented: every assertion sits next to its spec reference, and every contract has a &lt;code&gt;demo_*.ps1&lt;/code&gt; probe against a live server.&lt;/p&gt;

&lt;p&gt;Before publishing, we &lt;strong&gt;audited this catalogue line by line&lt;/strong&gt; — against the code, against live discovery, against the demos. Several rows did not survive: some are now worded precisely, some were struck and moved to the "what's missing" section below, and three turned out to be cheaper to &lt;em&gt;build&lt;/em&gt; than to delete — which is where the &lt;code&gt;claims&lt;/code&gt; parameter, the SCIM Enterprise extension and loopback redirects came from. The limitations list at the end isn't a disclaimer; it's the output of the same audit. Anything we overclaimed here, you could disprove from our own &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; in ten seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  OAuth 2.0 / 2.1 core
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RFC / spec&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Demo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 6749&lt;/strong&gt; OAuth 2.0 Framework&lt;/td&gt;
&lt;td&gt;grant types, scopes, error responses&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_client_credentials.ps1&lt;/code&gt;, &lt;code&gt;demo_authcode_pkce.ps1&lt;/code&gt;, &lt;code&gt;demo_refresh_rotation.ps1&lt;/code&gt;, &lt;code&gt;demo_password_ropc.ps1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 6750&lt;/strong&gt; Bearer Token Usage&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Authorization: Bearer&lt;/code&gt;, &lt;code&gt;WWW-Authenticate: Bearer&lt;/code&gt; challenge&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_userinfo.ps1&lt;/code&gt; — asserts WWW-Authenticate on missing/empty/garbage/tampered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 6585&lt;/strong&gt; Additional HTTP Status Codes&lt;/td&gt;
&lt;td&gt;429 Too Many Requests + Retry-After&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_throttle_rfc6585.ps1&lt;/code&gt; — parallel burst + recovery + per-key isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7009&lt;/strong&gt; Token Revocation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/connect/revoke&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_introspect_revoke.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7521&lt;/strong&gt; Assertion Framework&lt;/td&gt;
&lt;td&gt;assertions for &lt;strong&gt;client authentication&lt;/strong&gt; (&lt;code&gt;client_assertion&lt;/code&gt;). No assertion &lt;em&gt;grant&lt;/em&gt; types (&lt;code&gt;jwt-bearer&lt;/code&gt;, &lt;code&gt;saml2-bearer&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;covered via RFC 7523&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7523&lt;/strong&gt; JWT Bearer Assertion (private_key_jwt)&lt;/td&gt;
&lt;td&gt;client assertion on token / introspect / revoke / par / device&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_private_key_jwt.ps1&lt;/code&gt; — DCR with inline JWKS + token + introspect + tamper-negative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7591&lt;/strong&gt; Dynamic Client Registration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /connect/register&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_dcr_lifecycle.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7592&lt;/strong&gt; DCR Management Protocol&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET/PUT/DELETE /connect/register/{client_id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_dcr_lifecycle.ps1&lt;/code&gt; (same file)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7636&lt;/strong&gt; PKCE&lt;/td&gt;
&lt;td&gt;code_verifier / code_challenge / S256 (&lt;code&gt;plain&lt;/code&gt; rejected per OAuth 2.1)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_authcode_pkce.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7662&lt;/strong&gt; Token Introspection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/connect/introspect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_introspect_revoke.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 8252&lt;/strong&gt; OAuth 2.0 for Native Apps&lt;/td&gt;
&lt;td&gt;PKCE-required public clients + &lt;strong&gt;§7.3 loopback redirect: the port is not compared&lt;/strong&gt; (without it no &lt;code&gt;az login&lt;/code&gt;-style CLI can work — they get an ephemeral port from the OS at launch)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_loopback_redirect.ps1&lt;/code&gt; — 14 assertions, most of them negative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 8414&lt;/strong&gt; Authorization Server Metadata&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_discovery_jwks.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 8628&lt;/strong&gt; Device Authorization Grant&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/connect/deviceauthorization&lt;/code&gt;, 5s polling&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_device_code.ps1&lt;/code&gt; + non-interactive &lt;code&gt;demo_device_code_ci.ps1&lt;/code&gt; for CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 8693&lt;/strong&gt; Token Exchange&lt;/td&gt;
&lt;td&gt;impersonation + delegation chain (&lt;code&gt;act&lt;/code&gt;). Opt-in; &lt;code&gt;subject_token_type&lt;/code&gt; is &lt;code&gt;access_token&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_token_exchange.ps1&lt;/code&gt; — positive + negative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 8725&lt;/strong&gt; JWT BCP&lt;/td&gt;
&lt;td&gt;algorithm allow-listing for DPoP proofs and client assertions; &lt;code&gt;none&lt;/code&gt; is rejected&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_dpop.ps1&lt;/code&gt; (DPoP proof), discovery (&lt;code&gt;*_auth_signing_alg_values_supported&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 9068&lt;/strong&gt; JWT Profile for Access Tokens&lt;/td&gt;
&lt;td&gt;typed &lt;code&gt;at+jwt&lt;/code&gt;. Access tokens are &lt;strong&gt;JWE-encrypted by default&lt;/strong&gt; — a resource server either introspects, or you flip &lt;code&gt;DisableAccessTokenEncryption&lt;/code&gt; for local validation&lt;/td&gt;
&lt;td&gt;token header visible in &lt;code&gt;demo_jwt.ps1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 9126&lt;/strong&gt; Pushed Authorization Requests (PAR)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/connect/par&lt;/code&gt;, per-client &lt;code&gt;require_pushed_authorization_requests&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_par.ps1&lt;/code&gt; + &lt;code&gt;demo_par_per_client.ps1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 9207&lt;/strong&gt; Authorization Response &lt;code&gt;iss&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;iss&lt;/code&gt; in the auth response&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_auth_extras.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 9449&lt;/strong&gt; DPoP&lt;/td&gt;
&lt;td&gt;proof-JWT validation, replay cache, &lt;code&gt;DPoP-Nonce&lt;/code&gt;, resource-server validator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_dpop.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  OpenID Connect 1.0 family
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spec&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Demo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OIDC Core 1.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;code flow&lt;/strong&gt; (OAuth 2.1 — no implicit, no hybrid). id_token (RS256), sub/aud/exp/iat, nonce, prompt, max_age, acr_values, claim mappers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_discovery_jwks.ps1&lt;/code&gt;, &lt;code&gt;demo_claim_probes.ps1&lt;/code&gt;, &lt;code&gt;demo_prompt_max_age.ps1&lt;/code&gt;, &lt;code&gt;demo_acr_values.ps1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;OIDC Core §5.1&lt;/strong&gt; full &lt;code&gt;profile&lt;/code&gt; set&lt;/td&gt;
&lt;td&gt;all 14 claims; &lt;code&gt;updated_at&lt;/code&gt; a JSON &lt;strong&gt;number&lt;/strong&gt;, &lt;code&gt;*_verified&lt;/code&gt; JSON &lt;strong&gt;booleans&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_claim_probes.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;OIDC Core §5.4&lt;/strong&gt; claim delivery&lt;/td&gt;
&lt;td&gt;scope-derived claims (profile/email/phone/address) are served from &lt;strong&gt;UserInfo&lt;/strong&gt;, not baked into the id_token — the id_token stays a statement about the authentication event, not a PII container&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_claim_probes.ps1&lt;/code&gt; — asserts the &lt;strong&gt;absence&lt;/strong&gt; of PII in the id_token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;OIDC Core §5.5&lt;/strong&gt; &lt;code&gt;claims&lt;/code&gt; parameter&lt;/td&gt;
&lt;td&gt;an RP names the &lt;strong&gt;exact&lt;/strong&gt; claims it needs instead of pulling the whole &lt;code&gt;profile&lt;/code&gt; scope to get one &lt;code&gt;name&lt;/code&gt;, and picks the channel: &lt;code&gt;userinfo&lt;/code&gt; or &lt;code&gt;id_token&lt;/code&gt;. &lt;code&gt;essential&lt;/code&gt; / &lt;code&gt;value&lt;/code&gt; / &lt;code&gt;values&lt;/code&gt; supported&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_claims_parameter.ps1&lt;/code&gt; — 14 assertions, negatives included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OIDC Discovery 1.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; + JWKS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_discovery_jwks.ps1&lt;/code&gt; + manual RS256 verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OIDC Dynamic Client Registration 1.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RFC 7591 extensions for OIDC client_metadata&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_dcr_lifecycle.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RP-Initiated Logout 1.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/connect/logout&lt;/code&gt; + &lt;code&gt;post_logout_redirect_uri&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_logout_endsession.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Back-Channel Logout 1.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;backchannel_logout_uri&lt;/code&gt; + JWT logout token + pull-based revoked-SID feed for multi-replica RPs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_backchannel_logout.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Validated by the official OpenID Foundation conformance suite — not just our own tests
&lt;/h3&gt;

&lt;p&gt;A catalogue is a claim. The proof is running &lt;strong&gt;the very conformance suite the OpenID Foundation uses to certify providers&lt;/strong&gt; — not our tests, not "works on my machine," the official suite against a live server. Most .NET OAuth/OIDC implementations have never seen it; we drive redb.Identity through it as a real OP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic OP — 35 modules, 0 failures:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Modules&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PASSED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;REVIEW&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;the suite wants a human-uploaded screenshot (login form, error page) — counted as passing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WARNING&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;two extra claims in the id_token — a deliberate extension of ours, discussed below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SKIPPED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;request objects (RFC 9101) — we honestly don't advertise them, so the suite skips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FAILED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;Config OP&lt;/strong&gt; profile is clean too (native HTTPS, no reverse proxy).&lt;/p&gt;

&lt;p&gt;The suite is unforgiving: it checks exactly the RFC requirements hand-rolled servers trip on. And it was no rubber stamp — it found &lt;strong&gt;real defects&lt;/strong&gt;. Here they are, unvarnished:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authorization errors are delivered &lt;strong&gt;only&lt;/strong&gt; to the client's &lt;em&gt;registered&lt;/em&gt; &lt;code&gt;redirect_uri&lt;/code&gt; (RFC 6749 §4.1.2.1) — an error open-redirect, closed, not "redirect to whatever was sent";&lt;/li&gt;
&lt;li&gt;a reused authorization code returns &lt;code&gt;400 invalid_grant&lt;/code&gt;, not &lt;code&gt;401 invalid_token&lt;/code&gt; (§5.2);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Cache-Control: no-store&lt;/code&gt; on token / introspection / revocation (§5.1);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email_verified&lt;/code&gt; / &lt;code&gt;phone_number_verified&lt;/code&gt; are JSON &lt;strong&gt;booleans&lt;/strong&gt;, not strings (OIDC §5.1);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prompt=login&lt;/code&gt; / &lt;code&gt;max_age&lt;/code&gt; route to &lt;code&gt;/login&lt;/code&gt; and complete on re-login; the re-auth marker is bound to the &lt;strong&gt;session id&lt;/strong&gt; — no redirect loop, no same-second bypass;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII was leaking into the id_token.&lt;/strong&gt; Claims from the &lt;code&gt;profile&lt;/code&gt; / &lt;code&gt;email&lt;/code&gt; / &lt;code&gt;phone&lt;/code&gt; / &lt;code&gt;address&lt;/code&gt; scopes were baked straight into the id_token, when in the code flow they belong in UserInfo (OIDC §5.4). An id_token gets forwarded to third parties and logged as proof of the sign-in event, so the user's phone number travelled a great deal further than the RP ever intended. It doesn't any more;&lt;/li&gt;
&lt;li&gt;UserInfo, conversely, was &lt;strong&gt;returning too much&lt;/strong&gt; — the token's own plumbing: OpenIddict's &lt;code&gt;oi_*&lt;/code&gt; internals, &lt;code&gt;jti&lt;/code&gt; / &lt;code&gt;exp&lt;/code&gt; / &lt;code&gt;iat&lt;/code&gt; / &lt;code&gt;at_hash&lt;/code&gt;. Those describe the &lt;strong&gt;token&lt;/strong&gt;, not the user; UserInfo (§5.3) must return the user's claims and nothing else;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/connect/userinfo&lt;/code&gt; didn't accept the access token in the POST body (RFC 6750 §2.2);&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;profile&lt;/code&gt; claim set was incomplete — the suite diffs UserInfo against exactly the §5.1 list and flags every claim you omit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The suite also raised a &lt;code&gt;WARNING&lt;/code&gt; on &lt;code&gt;oidcc-claims-essential&lt;/code&gt;: "name not found in userinfo". We dug in: the test asks for &lt;code&gt;name&lt;/code&gt; through the &lt;strong&gt;&lt;code&gt;claims&lt;/code&gt; parameter&lt;/strong&gt; (§5.5) — which we had not implemented at all, and our own discovery document admitted it (&lt;code&gt;"claims_parameter_supported": false&lt;/code&gt;). So we built it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Two warnings we kept on purpose
&lt;/h4&gt;

&lt;p&gt;The very first module, &lt;code&gt;oidcc-server&lt;/code&gt;, finishes with two warnings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARNING  EnsureIdTokenDoesNotContainNonRequestedClaims
         id_token contains non-requested claim 'oi_tkn_id'
WARNING  EnsureIdTokenDoesNotContainNonRequestedClaims
         id_token contains non-requested claim 'redb:user_id'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're not going to bury those. Here's why they're there and why they stay.&lt;/p&gt;

&lt;p&gt;First, the important bit: these are &lt;strong&gt;warnings, not failures&lt;/strong&gt;. OIDC Core does not forbid additional claims in an id_token. The suite warns because an extra claim &lt;em&gt;can&lt;/em&gt; mean user data is leaking — and its own message concedes the alternative: &lt;em&gt;"…or that it implements an extension the conformance suite is not aware of."&lt;/em&gt; That's our case.&lt;/p&gt;

&lt;p&gt;Neither claim is &lt;strong&gt;data about the user&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;oi_tkn_id&lt;/code&gt;&lt;/strong&gt; — the id of the token's entry in the store. It is what makes the id_token &lt;strong&gt;revocable&lt;/strong&gt;, and what drives back-channel logout. Removing it means giving up id_token revocation. That trades a real capability for a clean warning list, and we declined the trade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;redb:user_id&lt;/code&gt;&lt;/strong&gt; — our own private claim, namespaced per the RFC 7519 §4.3 convention. The public &lt;code&gt;sub&lt;/code&gt; is a GUID (stable, correct across instances); the hot key in the relational &lt;code&gt;_users&lt;/code&gt; table is a bigint. This claim lets a client decode the id_token and join the user to &lt;strong&gt;its own&lt;/strong&gt; table by our internal id, without a round-trip back to us.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here's the part that proves this isn't a rationalisation. &lt;strong&gt;In that same run we deleted a third such claim from the id_token.&lt;/strong&gt; OpenIddict was also stamping &lt;code&gt;oi_au_id&lt;/code&gt; in there — its internal link to the authorization entry. That one has no business in a token handed to a client: it means nothing to them, and there was no defending it. Gone (kept on the access_token, where introspection needs it).&lt;/p&gt;

&lt;p&gt;So we didn't wave the warnings away. We went through each one and kept exactly the two we can answer for.&lt;/p&gt;

&lt;p&gt;That's the line between "wrote OAuth over a weekend" and a server that clears the same checks industrial IdPs do. Full per-module breakdown and local setup: &lt;code&gt;OPENID_CERTIFICATION.md&lt;/code&gt; in the repo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To be scrupulous: we do &lt;strong&gt;not&lt;/strong&gt; carry the &lt;strong&gt;OpenID Certified™&lt;/strong&gt; mark. That's a trademark, granted by the OIDF through a formal submission. What we claim is exactly what's true — the server is run against the official OIDF conformance suite, and the results are in the repo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  JOSE / JWT
&lt;/h3&gt;

&lt;p&gt;RFC 7515 (JWS), 7517 (JWKS), 7518 (JWA), 7519 (JWT), 7638 (JWK thumbprint, used as the DPoP &lt;code&gt;jkt&lt;/code&gt;), 7800 (&lt;code&gt;cnf&lt;/code&gt; / proof-of-possession), 8176 (&lt;code&gt;amr&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  SCIM 2.0
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RFC&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Demo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7643&lt;/strong&gt; SCIM Schema&lt;/td&gt;
&lt;td&gt;User, Group, Meta&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_scim.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7643 §4.3&lt;/strong&gt; Enterprise User extension&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;department&lt;/code&gt;, &lt;code&gt;manager&lt;/code&gt;, &lt;code&gt;employeeNumber&lt;/code&gt;, &lt;code&gt;costCenter&lt;/code&gt;, &lt;code&gt;organization&lt;/code&gt;, &lt;code&gt;division&lt;/code&gt;. &lt;code&gt;manager&lt;/code&gt; is a complex attribute; &lt;code&gt;$ref&lt;/code&gt; and &lt;code&gt;displayName&lt;/code&gt; are &lt;strong&gt;derived, not stored&lt;/strong&gt; (a stored copy would rot the moment the manager is renamed)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_scim_enterprise.ps1&lt;/code&gt; — 24 assertions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 7644&lt;/strong&gt; SCIM Protocol&lt;/td&gt;
&lt;td&gt;CRUD + ETag concurrency (RFC 7232 → 412) + discovery. &lt;strong&gt;Filter is single-expression&lt;/strong&gt;: &lt;code&gt;userName&lt;/code&gt; / &lt;code&gt;externalId&lt;/code&gt; / &lt;code&gt;displayName&lt;/code&gt; / &lt;code&gt;emails.value&lt;/code&gt;, no &lt;code&gt;and&lt;/code&gt;/&lt;code&gt;or&lt;/code&gt;. &lt;strong&gt;Bulk is opt-in&lt;/strong&gt; (&lt;code&gt;Features.EnableScimBulk&lt;/code&gt;, off by default — and the ServiceProviderConfig says so honestly)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_scim.ps1&lt;/code&gt; (CRUD) + &lt;code&gt;demo_scim_bulk.ps1&lt;/code&gt; (bulk) + &lt;code&gt;demo_scim_etag.ps1&lt;/code&gt; (concurrency)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Enterprise extension is what corporate provisioning sends on its &lt;strong&gt;very first request&lt;/strong&gt;: Okta, Entra ID and Workday all push &lt;code&gt;department&lt;/code&gt; and &lt;code&gt;manager&lt;/code&gt; on the initial sync. A provider that advertises only the core schema makes them drop that data on the floor.&lt;/p&gt;

&lt;p&gt;The SCIM discovery endpoints (&lt;code&gt;/scim/v2/ServiceProviderConfig&lt;/code&gt;, &lt;code&gt;ResourceTypes&lt;/code&gt;, &lt;code&gt;Schemas&lt;/code&gt;) are served &lt;strong&gt;unauthenticated and unconditional&lt;/strong&gt;, so an RP can probe them &lt;em&gt;before&lt;/em&gt; SCIM provisioning is switched on.&lt;/p&gt;

&lt;h3&gt;
  
  
  MFA / WebAuthn / OTP
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Standard&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Demo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;RFC 6238&lt;/strong&gt; TOTP&lt;/td&gt;
&lt;td&gt;time-based OTP, atomic + replay-protected per §5.2 (160-bit secret per RFC 4226 §4)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;demo_mfa_totp.ps1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;W3C WebAuthn Level 2 / FIDO2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;passkey enrollment + assertion, &lt;code&gt;attestation=none/direct/indirect/enterprise&lt;/code&gt;, &lt;code&gt;userVerification&lt;/code&gt; variants&lt;/td&gt;
&lt;td&gt;API ready, demo waits on a frontend companion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NIST SP 800-63B / OWASP ASVS 4.0.3 §2.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;password policy (min 12, upper+lower+digit, history)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;demo_password_change_negatives.ps1&lt;/code&gt; 13/13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Argon2id&lt;/strong&gt; (OWASP 2023 primary)&lt;/td&gt;
&lt;td&gt;64 MiB / 3 iterations / parallelism 4&lt;/td&gt;
&lt;td&gt;enforced, upgrade-on-login for legacy BCrypt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SMS/Email OTP and recovery codes (one-shot, consumed in the same transaction that creates the session) — &lt;code&gt;demo_mfa_recovery_codes.ps1&lt;/code&gt;. Disable / replace a method — &lt;code&gt;demo_mfa_disable_replace.ps1&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Federation, self-service, admin
&lt;/h3&gt;

&lt;p&gt;External OIDC IdPs (Google, Microsoft, Keycloak) plus a dedicated &lt;strong&gt;GitHub OAuth2&lt;/strong&gt; path (no discovery, no &lt;code&gt;id_token&lt;/code&gt; — profile and emails pulled from the REST &lt;code&gt;/user&lt;/code&gt; and &lt;code&gt;/user/emails&lt;/code&gt;, endpoints configurable for GitHub Enterprise / Gitea). Provision-on-first-login, link-on-replay (same &lt;code&gt;sub&lt;/code&gt; → same user, no duplicate), self-service link/unlink via &lt;code&gt;/me/federated-identities/*&lt;/code&gt;, email-conflict resolution that refuses a silent takeover. Demos: &lt;code&gt;demo_federation.ps1&lt;/code&gt;, &lt;code&gt;demo_federation_e2e.ps1&lt;/code&gt; (full round-trip against &lt;a href="https://github.com/navikt/mock-oauth2-server" rel="noopener noreferrer"&gt;navikt/mock-oauth2-server&lt;/a&gt;), &lt;code&gt;demo_federation_github.ps1&lt;/code&gt;, &lt;code&gt;demo_federation_link_unlink.ps1&lt;/code&gt;. LDAP / Active Directory is a separate &lt;code&gt;redb.Identity.Ldap&lt;/code&gt; package with bind-on-login and UserAccountControl parsing.&lt;/p&gt;

&lt;p&gt;Self-service and admin round it out: registration + email verify, profile and email change with dual-confirm, password change (13 negatives) and forgot-password round-trip, sessions list/revoke (own and admin, with dry-run), self-service account delete with cascade, OIDC client CRUD, a granular scope gate (&lt;code&gt;identity:audit.read&lt;/code&gt; is distinct from &lt;code&gt;identity:read&lt;/code&gt; is distinct from &lt;code&gt;identity:users.manage&lt;/code&gt;), group hierarchy with per-member role labels, and the emergency-admin bootstrap endpoint. Each has its own demo.&lt;/p&gt;

&lt;p&gt;Two that deserve a shout because they're rare in .NET: &lt;strong&gt;DPoP (RFC 9449)&lt;/strong&gt; and &lt;strong&gt;private_key_jwt (7521/7523)&lt;/strong&gt; — both implemented, both pinned by demo probes, not roadmap items.&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%2F9omtjis8kx7ayrq6wdkl.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%2F9omtjis8kx7ayrq6wdkl.png" alt="redb.Identity" width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Production-grade features — the "later" list, done now
&lt;/h2&gt;

&lt;p&gt;The unglamorous things that decide whether an identity server survives production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live JWKS rotation — no process restart
&lt;/h3&gt;

&lt;p&gt;The classic problem: you shipped an OIDC server, 60 days pass, you need to rotate the signing key (RP JWKS caches live up to a week, and then there are incidents). Most implementations — vanilla OpenIddict included — need a &lt;strong&gt;process restart&lt;/strong&gt; to rotate, because &lt;code&gt;OpenIddictServerOptions.SigningCredentials&lt;/code&gt; is built once via &lt;code&gt;IPostConfigureOptions&amp;lt;&amp;gt;&lt;/code&gt; and cached.&lt;/p&gt;

&lt;p&gt;Here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An admin hits &lt;code&gt;POST /signing-keys/rotate&lt;/code&gt; (requires scope &lt;code&gt;identity:applications.manage&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PropsSigningKeyStore.RotateAsync&lt;/code&gt; persists the new key and marks the old one demoted — but it stays in the JWKS until retire, a grace window for in-flight tokens.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IOptionsMonitorCache&amp;lt;OpenIddictServerOptions&amp;gt;&lt;/code&gt; and &lt;code&gt;…ValidationOptions&lt;/code&gt; are invalidated immediately.&lt;/li&gt;
&lt;li&gt;The next OIDC handler re-evaluates the whole Configure → PostConfigure chain, and &lt;code&gt;PropsSigningKeyStoreOpenIddictPostConfigure&lt;/code&gt; rebuilds &lt;code&gt;SigningCredentials&lt;/code&gt; from the current store snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New tokens are signed under the just-rotated kid right away&lt;/strong&gt;; in-flight tokens validate under the old key until it's retired.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;demo_jwks_rotation.ps1&lt;/code&gt; asserts the full grace-window flow: after rotate the JWKS serves K1 + K2 (old-in-grace + new-active); new id_tokens carry the just-rotated kid; after &lt;code&gt;DELETE /signing-keys/{K1}&lt;/code&gt; the JWKS holds only K2 and old tokens stop validating; K1 stays in the admin audit list with &lt;code&gt;inJwks=false&lt;/code&gt; for the compliance trail. All without a restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cluster-safe primitives — built in
&lt;/h3&gt;

&lt;p&gt;It's an identity server; it'll live in a cluster. Baked in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimistic concurrency&lt;/strong&gt; on every &lt;code&gt;RedbObject&amp;lt;T&amp;gt;&lt;/code&gt; via a &lt;code&gt;_hash&lt;/code&gt; field. &lt;code&gt;UpdateAsync&lt;/code&gt; takes a row lock, checks the hash, and throws &lt;code&gt;OpenIddictExceptions.ConcurrencyException&lt;/code&gt; on a mismatch. A test drives exactly that with two concurrent writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cluster-wide lock for schema init.&lt;/strong&gt; When several workers come up at once, &lt;code&gt;IdentitySchemaInitListener&lt;/code&gt; takes &lt;code&gt;LockForUpdate&lt;/code&gt; on a specific row — "leader under a cluster lock" — initializes the schemes, and releases. Followers see &lt;code&gt;another node holds the lock, proceeding idempotently&lt;/code&gt; and wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic claim for background tasks.&lt;/strong&gt; Trash purge and orphaned-task recovery go through &lt;code&gt;TryClaimOrphanedTaskAsync&lt;/code&gt; — an atomic UPDATE with a WHERE condition. Whichever worker flips &lt;code&gt;pending → running&lt;/code&gt; first owns the work. No distributed locking needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-request cache invalidation&lt;/strong&gt; via the DI scope. &lt;code&gt;RedbApplicationStore._clientIdCache&lt;/code&gt; lives for exactly one HTTP request and is invalidated on CRUD — no stale data across requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Background deletion — the DB as a queue
&lt;/h3&gt;

&lt;p&gt;Deleting an OIDC application with thousands of accumulated tokens and authorizations is an expensive cascade. Do it on the request thread and the user times out; do it through an in-memory channel and you lose it on a worker crash. Here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;SoftDeleteAsync&lt;/code&gt; synchronously re-parents the object under a trash scheme (&lt;code&gt;scheme_id = -10&lt;/code&gt;). It &lt;strong&gt;instantly&lt;/strong&gt; disappears from every &lt;code&gt;Query&amp;lt;&amp;gt;()&lt;/code&gt;. The RP sees 204, the operation "succeeded."&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;BackgroundDeletionService&lt;/code&gt; polls the DB for pending trash containers every 5 seconds. Cluster-safe (&lt;code&gt;TryClaimOrphanedTaskAsync&lt;/code&gt;); on a worker crash the next poll cycle picks it up.&lt;/li&gt;
&lt;li&gt;The purge runs in batches of 10 with a &lt;code&gt;Task.Delay(50ms)&lt;/code&gt; between them, so live traffic can interleave.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;No in-memory state&lt;/strong&gt; — a crash mid-operation loses nothing; the DB stays the source of truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  DataProtection key ring on the storage layer
&lt;/h3&gt;

&lt;p&gt;The cookie session (SameSite=Strict), federation state, MFA setup tokens, the recovery-code pepper — all signed or encrypted via ASP.NET Core DataProtection. By default DataProtection keeps keys on the filesystem or in Azure Storage, which isn't cluster-safe out of the box and needs its own operational care. Here the key ring lives &lt;strong&gt;in the same REDB store&lt;/strong&gt; via &lt;code&gt;RedbXmlRepository&lt;/code&gt;: back up Postgres and the ring comes with it; every cluster node sees the same keys (rotation streams through REDB notifications too); the keys are &lt;strong&gt;encrypted at rest&lt;/strong&gt; by a configurable master key (AES-GCM, certificate-based, or a custom KMS factory).&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate limiting with a distributed backend
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/connect/token&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt;, &lt;code&gt;/mfa/verify&lt;/code&gt;, &lt;code&gt;/mfa/recovery&lt;/code&gt; are all throttled — per-IP and per-&lt;code&gt;(IP+username)&lt;/code&gt; counters. The backend is an option: &lt;code&gt;memory&lt;/code&gt; (single-node in-memory bucket) or &lt;code&gt;redis&lt;/code&gt; (cluster-wide via StackExchange.Redis, one Redis for rate limits + the federation-state nonce store). 429 + Retry-After is exactly to spec; &lt;code&gt;demo_throttle_rfc6585.ps1&lt;/code&gt; runs a parallel burst, recovery after the window, and isolation between keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Password hashing — Argon2id and BCrypt side by side, with upgrade-on-login and a timing-attack fix
&lt;/h3&gt;

&lt;p&gt;The details that separate a production hashing pipeline from a tutorial one. Two algorithms run in parallel, no deployment-level either/or.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Argon2id — the primary for new deployments.&lt;/strong&gt; Defaults straight from the &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Password Storage Cheat Sheet 2023&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;new&lt;/span&gt; &lt;span class="nf"&gt;Argon2idPasswordHasher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;memoryKib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 64 MiB&lt;/span&gt;
    &lt;span class="n"&gt;iterations&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;parallelism&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="n"&gt;saltBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hashBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A memory-hard function: 64 MiB per verify makes GPU/ASIC attacks uneconomical compared to BCrypt (compute-hard but memory-cheap, so a mass FPGA attack works). Argon2id is the hybrid that won the &lt;a href="https://www.password-hashing.net/" rel="noopener noreferrer"&gt;Password Hashing Competition&lt;/a&gt; in 2015.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BCrypt — kept for legacy and compliance-driven deployments.&lt;/strong&gt; It's been in production since 1999; an auditor sees a familiar algorithm with known failure modes, and the PCI-approved libraries allow it outright. And there's the migration story: if you're moving off IdentityServer / ASP.NET Identity, your DB already holds BCrypt hashes — we read them natively, no forced password reset on 100k users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upgrade-on-login — a transparent migration.&lt;/strong&gt; Every successful login checks whether the stored hash matches the current OWASP parameters. If not, a fire-and-forget rehash into the fresh format:&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;// LoginService.cs (simplified)&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;_passwordHasher&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;NeedsRehash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_passwordHasher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coreUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&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;Task&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="k"&gt;async&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;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rehashScope&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="nf"&gt;CreateScope&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;rehashRedb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rehashScope&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;freshUser&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;rehashRedb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUserByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&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;rehashRedb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPasswordAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;freshUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capturedPassword&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;Over N logins the whole base drifts onto Argon2id — no forced reset, no migration window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constant-time fake-verify — against user enumeration by timing.&lt;/strong&gt; &lt;code&gt;ValidateUserAsync&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; for three failure modes: user not found, user disabled, wrong password. Without mitigation (1) and (2) return in 5–10ms (the DB lookup misses immediately) while (3) takes ~250ms (BCrypt actually runs), and an attacker can tell "does this email exist" from the response time — that's CWE-204. A precomputed &lt;code&gt;FakeBcryptHash&lt;/code&gt; (workFactor 12) runs on every negative path, so the wall-clock cost is identical across all three:&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;// LoginService.AuthenticateLocal — fake-verify to keep timing consistent&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;coreUser&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;try&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;BCrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BCrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&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;FakeBcryptHash&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="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;_securityLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Login denied: user '{Username}' — invalid credentials or not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&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;LoginResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid credentials."&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;Password policy — NIST SP 800-63B / OWASP ASVS 4.0.3 §2.1&lt;/strong&gt;, enforced before hashing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Configurable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MinLength&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;RequireDigit&lt;/code&gt; / &lt;code&gt;RequireUppercase&lt;/code&gt; / &lt;code&gt;RequireLowercase&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RequireSpecial&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;HistoryCount&lt;/code&gt; (can't reuse the last N)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MaxAge&lt;/code&gt; (force rotation)&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;BreachCheckEnabled&lt;/code&gt; (haveibeenpwned probe)&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;HistoryCount&lt;/code&gt; lives in &lt;code&gt;PropsPasswordHistoryStore&lt;/code&gt; — SHA-256 hashed, peppered prior-password digests in REDB, which closes the "change the password, come back an hour later to the old one" gap. &lt;code&gt;demo_password_change_negatives.ps1&lt;/code&gt; runs all 13 negative paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  Audit — "anywhere," proven by integration tests
&lt;/h2&gt;

&lt;p&gt;Audit is first-class, not a bolt-on. &lt;strong&gt;116 typed events across 9 categories&lt;/strong&gt;, single source of truth in &lt;code&gt;IdentityAuditEventIds&lt;/code&gt;. Each event lands in a relational &lt;code&gt;identity_audit_log&lt;/code&gt; table (&lt;code&gt;user_id BIGINT&lt;/code&gt;, indexed — a direct integer seek, not a scan) and, if configured, multicasts to external sinks. Plaintext secrets are never written — a rotation logs a &lt;code&gt;ClientSecretRotated&lt;/code&gt; marker only.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;authentication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UserLoggedIn&lt;/code&gt;, &lt;code&gt;LoginFailed&lt;/code&gt;, &lt;code&gt;PasswordChanged&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;authorization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TokenIssued&lt;/code&gt;/&lt;code&gt;Revoked&lt;/code&gt;/&lt;code&gt;Introspected&lt;/code&gt;, &lt;code&gt;ConsentGranted&lt;/code&gt;, &lt;code&gt;DpopReplayDetected&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;admin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Client*&lt;/code&gt;, &lt;code&gt;Scope*&lt;/code&gt;, &lt;code&gt;User*&lt;/code&gt;, &lt;code&gt;Group*&lt;/code&gt;, &lt;code&gt;ClaimMapper*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;federation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;FederatedUserLoggedIn&lt;/code&gt;, &lt;code&gt;FederationStateValidationFailed&lt;/code&gt;, &lt;code&gt;FederatedEmailConflict&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mfa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MfaEnrolled&lt;/code&gt;, &lt;code&gt;MfaVerifyFailed&lt;/code&gt;, &lt;code&gt;MfaWebAuthnSignCounterAnomaly&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ScimUserCreated&lt;/code&gt;/&lt;code&gt;Replaced&lt;/code&gt;/&lt;code&gt;Patched&lt;/code&gt;, &lt;code&gt;ScimBulkProcessed&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SessionRevoked&lt;/code&gt;, &lt;code&gt;SidRevoked&lt;/code&gt;, &lt;code&gt;TokensRevokedByUser&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;First candidates for a SIEM: &lt;code&gt;LoginFailed&lt;/code&gt;, &lt;code&gt;MfaVerifyFailed&lt;/code&gt;, &lt;code&gt;MfaWebAuthnSignCounterAnomaly&lt;/code&gt;, &lt;code&gt;DpopReplayDetected&lt;/code&gt;, &lt;code&gt;FederationStateValidationFailed&lt;/code&gt;, &lt;code&gt;ClientSecretRotated&lt;/code&gt;, &lt;code&gt;AllSessionsRevoked&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mechanism: a WireTap on direct-vm
&lt;/h3&gt;

&lt;p&gt;Every mutating action (login, token issue, scope grant, MFA enroll, federation link, user delete, …) goes through &lt;code&gt;EventDispatchProcessor&lt;/code&gt;, which writes a typed &lt;code&gt;IdentityEvent&lt;/code&gt; into &lt;code&gt;exchange.Out.Body&lt;/code&gt; + headers. From there it's a standard redb.Route WireTap:&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;WithRedbTx&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;IdentityEndpoints&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;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;WireTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IdentityEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← a copy branches off to the audit pipeline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;identity-events&lt;/code&gt; route fans out to any number of sinks at once — &lt;strong&gt;without touching identity code&lt;/strong&gt;, it's route config.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the tests actually prove
&lt;/h3&gt;

&lt;p&gt;"Anywhere" isn't marketing. &lt;strong&gt;Nine different audit targets&lt;/strong&gt; each have their own integration test in &lt;code&gt;redb.Identity.Tests/Audit/&lt;/code&gt;, run against real containerized brokers in the dev compose stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sink&lt;/th&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;What it checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;SQL&lt;/strong&gt; (Postgres / MS SQL table)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditSqlIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;event lands with the right columns, parameter binding via the redb.Route SQL connector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;PROPS store&lt;/strong&gt; (typed storage in the same REDB)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditEavPersistenceIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IdentityEvent&lt;/code&gt; saved as &lt;code&gt;RedbObject&amp;lt;AuditEventProps&amp;gt;&lt;/code&gt; next to identity data — queryable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditKafkaIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;publish to a topic, partition key from user_id / event_type, acks=all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditRabbitMqIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;publish to an exchange, routing key from event-type, durable queue with manual ack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IBM MQ / WMQ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditIbmMqIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;publish to an MQQueue, MQMD with correlation-id, transactional context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AMQP 1.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditAmqpIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;publish to a standard AMQP node, properties + application-properties + body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MQTT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditMqttIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;publish to a topic with QoS 1+, retained flag (audit stream for IoT gateways)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Elasticsearch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditElasticsearchIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bulk indexing into &lt;code&gt;identity-events-*&lt;/code&gt;, ready for Kibana&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AuditRedisIntegrationTests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;publish to pub/sub (live firehose) + optional append to a Stream (replayable history)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus &lt;code&gt;AuditEventSinkProcessorTests&lt;/code&gt; (unit tests on the fan-out processor) and &lt;code&gt;AuditCompletenessFullCycleTests&lt;/code&gt; in &lt;code&gt;FullStack/&lt;/code&gt; — a full-cycle run that performs N mutating actions over HTTP and then checks that every one landed in the configured sinks. So "audit goes anywhere" means &lt;strong&gt;CI runs 8 real sinks on every commit&lt;/strong&gt; (Kafka, RabbitMQ, IBM MQ, AMQP, MQTT, Elasticsearch, Redis, SQL) and verifies events serialize and arrive. Not an architecture slide — a green build against real infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-world fan-out
&lt;/h3&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;IdentityEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Marshal&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="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres://?table=identity_audit"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                  &lt;span class="c1"&gt;// durable retention&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://broker:9092/identity-events?acks=all"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;// real-time SIEM stream&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;"elasticsearch://logs/identity-events?bulk=true"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;    &lt;span class="c1"&gt;// Kibana ops dashboard&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;"wmq://QM.PROD?queue=COMPLIANCE.IDENTITY.AUDIT"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;// a bank's compliance core&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;"file:///var/log/identity/audit-${date:yyyy-MM-dd}.jsonl?append=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// locked-down fallback&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One identity server, one event source (&lt;code&gt;direct-vm://identity-events&lt;/code&gt;), five parallel sinks of different natures. No identity server on the market does this out of the box — it's either "pick one sink in config" or "a webhook to your HTTP endpoint, do the fan-out yourself." This is transport-agnosticism at the event level: IBM MQ for banks that can't leave WMQ on compliance grounds, RabbitMQ for general-purpose, Kafka for streaming analytics — Identity uses the same transport set as any other business route.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability
&lt;/h3&gt;

&lt;p&gt;Metrics ride the standard OpenTelemetry meter &lt;code&gt;RedbIdentity&lt;/code&gt; — plug it into any OTel pipeline:&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;builder&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;AddOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMetrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&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;AddMeter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RedbIdentity"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Login/failure counters (tagged &lt;code&gt;reason&lt;/code&gt;), MFA verifications (&lt;code&gt;method&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt;), tokens issued (&lt;code&gt;grant_type&lt;/code&gt;, &lt;code&gt;token_type&lt;/code&gt;), token errors, rate-limit rejections, and a password-verify duration histogram that catches CPU regressions on hashing. Plus a dedicated &lt;strong&gt;security log channel&lt;/strong&gt; (&lt;code&gt;RedbIdentity.Security&lt;/code&gt;) so a SIEM can subscribe to audit-grade events without sifting routine logs, and module health probes (&lt;code&gt;db&lt;/code&gt;, &lt;code&gt;signing-keys&lt;/code&gt;, &lt;code&gt;data-protection&lt;/code&gt;) under Tsak's aggregated &lt;code&gt;/api/health/{startup,live,ready}&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Logical-level backup via redb.Export — no &lt;code&gt;pg_dump&lt;/code&gt;, no vendor lock
&lt;/h2&gt;

&lt;p&gt;The section that usually gets left for later and then suddenly becomes a problem. Most identity servers have one of two backup stories: &lt;code&gt;pg_dump&lt;/code&gt;/&lt;code&gt;mssqldump&lt;/code&gt; (a binary snapshot — you can't restore a subset, it's pinned to the DB version, and Postgres → MSSQL migration is off the table) or an application-level export through an admin API (usually shallow, no FK order, doesn't cover custom schemes).&lt;/p&gt;

&lt;p&gt;There's a separate package — &lt;strong&gt;&lt;code&gt;redb.Export&lt;/code&gt;&lt;/strong&gt; — that does a logical-level dump into a portable &lt;code&gt;.redb&lt;/code&gt; format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSONL stream&lt;/strong&gt; (newline-delimited JSON), optionally zipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreign-key-safe order&lt;/strong&gt; — types → roles → users → user_roles → lists → list_items → schemes → structures → objects → permissions → values. Single-pass restore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheme-subset filter&lt;/strong&gt; — &lt;code&gt;--schemes UserProps,ApplicationProps&lt;/code&gt;. You can restore a subset too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-type counts&lt;/strong&gt; — the header knows exactly how many rows of each type were exported; the footer asserts it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-provider&lt;/strong&gt; — an &lt;code&gt;IDataProvider&lt;/code&gt; for Postgres / MSSQL / SQLite. One &lt;code&gt;.redb&lt;/code&gt; file is portable across engines; dialect differences are encapsulated in the provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dryRun&lt;/strong&gt; — count what &lt;em&gt;would&lt;/em&gt; be exported without writing to disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A production example — a daily cron backup through redb.Route
&lt;/h3&gt;

&lt;p&gt;From a real project. &lt;code&gt;TsumBackupRouteBuilder.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;class&lt;/span&gt; &lt;span class="nc"&gt;TsumBackupRouteBuilder&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RouteBuilder&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&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;backupConfig&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;GetProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDictionary&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;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Backup"&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;directory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;backupConfig&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;"Directory"&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;dir&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;dir&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;"backups"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"backups"&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;retentionDays&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;backupConfig&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;"RetentionDays"&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;ret&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;int&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;ret&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="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;rd&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&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;SetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_backup.directory"&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="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"_backup.retentionDays"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retentionDays&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;"cron://tsum-backup?schedule=0 0 3 * * ?"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// ← Quartz cron, daily at 03:00&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-backup-cron"&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;RunBackupAsync&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// ← a named-scope IRedbService is injected&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;RunBackupAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IRedbService&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;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="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pgConn&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;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&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;directory&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;GetProperty&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;"_backup.directory"&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;retentionDays&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;GetProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"_backup.retentionDays"&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;directory&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;timestamp&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="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"yyyy-MM-dd_HHmmss"&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;filePath&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;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"tsum_backup_&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="s"&gt;.redb"&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;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProviderFactory&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="s"&gt;"postgres"&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;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pgConn&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;exportService&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;ExportService&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="n"&gt;verbose&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="n"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&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;exportService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExportAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schemeIds&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;compress&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;dryRun&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="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="n"&gt;provider&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;RotateBackups&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="n"&gt;retentionDays&lt;/span&gt;&lt;span class="p"&gt;);&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;void&lt;/span&gt; &lt;span class="nf"&gt;RotateBackups&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;directory&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;retentionDays&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;cutoff&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="nf"&gt;AddDays&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="n"&gt;retentionDays&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;file&lt;/span&gt; &lt;span class="k"&gt;in&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;GetFiles&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="s"&gt;"tsum_backup_*.redb"&lt;/span&gt;&lt;span class="p"&gt;)&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;f&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="nf"&gt;FileInfo&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="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;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;CreationTimeUtc&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;))&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;Delete&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;No separate backup service, no Bash cron job, no separate deployment: one &lt;code&gt;From("cron://...")&lt;/code&gt; (Quartz is already built into the route system), &lt;code&gt;.ProcessWithRedb(...)&lt;/code&gt; injects a named-scope &lt;code&gt;IRedbService&lt;/code&gt;, &lt;code&gt;ExportService.ExportAsync(..., compress: true, ...)&lt;/code&gt; turns on compression by argument, and &lt;code&gt;RotateBackups&lt;/code&gt; is a plain date-based retention sweep. The file drops into the project's domain with a single &lt;code&gt;context.AddRoutes(new TsumBackupRouteBuilder())&lt;/code&gt;. Identity gets &lt;strong&gt;the same backup for free&lt;/strong&gt; — because Identity's data lives in &lt;strong&gt;the same&lt;/strong&gt; REDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Restore and cross-provider migration
&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;importService&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;ImportService&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="n"&gt;verbose&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="n"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&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;importService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ImportAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tsum_backup_2026-06-20_030000.redb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schemeIds&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="c1"&gt;// null = whole file; a subset also works&lt;/span&gt;
    &lt;span class="n"&gt;truncateBefore&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;// or true for a clean restore&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;One of the killer moves is migrating the whole identity stack between engines &lt;strong&gt;without touching code&lt;/strong&gt;, in three CLI commands:&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;# 1. Export from the old DB&lt;/span&gt;
redb &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; postgres &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Host=src;Database=redb;..."&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; data.redb &lt;span class="nt"&gt;--compress&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="c"&gt;# 2. Bootstrap the schema on the new DB&lt;/span&gt;
redb init &lt;span class="nt"&gt;-p&lt;/span&gt; mssql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Server=dst;Database=redb;..."&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="c"&gt;# 3. Import&lt;/span&gt;
redb import &lt;span class="nt"&gt;-p&lt;/span&gt; mssql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Server=dst;Database=redb;..."&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; data.redb &lt;span class="nt"&gt;--clean&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.redb&lt;/code&gt; format is typed and cross-provider; dialect differences are encapsulated in &lt;code&gt;IDataProvider&lt;/code&gt;. After the migration Identity sees the exact same data — the same &lt;code&gt;OpenIddictApplication&lt;/code&gt; ids, the same user &lt;code&gt;sub&lt;/code&gt;s, the same signing keys. That's a level of portability IdentityServer (bound to its EF provider and migrations), Keycloak (Java SPI ↔ Postgres/MariaDB), and Auth0 (no on-prem at all) don't offer.&lt;/p&gt;

&lt;h3&gt;
  
  
  redb.CLI — a global .NET tool for the BAU operations
&lt;/h3&gt;

&lt;p&gt;The same pipeline is on the command line via &lt;code&gt;redb.CLI&lt;/code&gt; (on nuget.org):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; redb.CLI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four commands cover the identity operational cycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;redb init&lt;/code&gt;&lt;/strong&gt; — bootstrap the REDB tables/sequences/functions/views in a clean DB (&lt;code&gt;-p postgres|mssql|sqlite&lt;/code&gt;). The one-time analog of "migrations init"; after that the schema lives through the scheme registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;redb schema&lt;/code&gt;&lt;/strong&gt; — emit the SQL script for DBA review (&lt;code&gt;redb schema -p postgres -o redb_schema.sql&lt;/code&gt;, or pipe into &lt;code&gt;psql&lt;/code&gt;). Handy for change management (a PR flow), a CI/CD DDL artifact, or a compliance review without standing up a DB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;redb export&lt;/code&gt;&lt;/strong&gt; — backup to &lt;code&gt;.redb&lt;/code&gt; (&lt;code&gt;--compress&lt;/code&gt;, &lt;code&gt;--schemes 100,200,300&lt;/code&gt;, &lt;code&gt;--dry-run&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;redb import&lt;/code&gt;&lt;/strong&gt; — restore from &lt;code&gt;.redb&lt;/code&gt; (&lt;code&gt;--clean&lt;/code&gt; to truncate first, &lt;code&gt;--dry-run&lt;/code&gt; to check compatibility).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For identity that gives you: compliance retention (encrypt the &lt;code&gt;.redb&lt;/code&gt; at the FS/KMS level, retention for N years), DR testing (restore into staging for a regular drill without pg_restore choreography), a forensic snapshot at incident time (one cron trigger over &lt;code&gt;direct-vm://&lt;/code&gt; and you have it), a scheme-subset export for a regulator, and cross-version migration. The kind of thing cloud IDPs charge for as an Enterprise add-on; here it's first-class.&lt;/p&gt;




&lt;h2&gt;
  
  
  Host-agnostic — Tsak is an option, not a dependency
&lt;/h2&gt;

&lt;p&gt;A common question: "you keep mentioning hot-reload, a cluster, a dashboard — is all of that mandatory?" &lt;strong&gt;No.&lt;/strong&gt; The identity server is a set of redb.Route routes; what hosts them is up to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1 — a plain .NET worker.&lt;/strong&gt; Your own &lt;code&gt;Program.cs&lt;/code&gt; on &lt;code&gt;Host.CreateApplicationBuilder&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;builder&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="nf"&gt;CreateApplicationBuilder&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="n"&gt;builder&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;AddRedbCore&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;Configuration&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;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;builder&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;AddRedbIdentity&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;Configuration&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;host&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="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="n"&gt;host&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;GetRequiredService&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;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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IdentityCoreRouteBuilder&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="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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpFacadeRouteBuilder&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="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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;host&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dotnet run&lt;/code&gt; and the OIDC server is up. No runtime container, no dashboard, no &lt;code&gt;.tpkg&lt;/code&gt;. Deploy it as a plain Worker Service / systemd unit / Windows service / Docker container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2 — inside an ASP.NET Core monolith.&lt;/strong&gt; Identity embeds in the same process and reuses the host's &lt;code&gt;IServiceProvider&lt;/code&gt; (DataProtection key ring, &lt;code&gt;IConfiguration&lt;/code&gt;, &lt;code&gt;ILogger&lt;/code&gt;) — no second initialization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3 — a service-mesh sidecar.&lt;/strong&gt; Identity as an in-process compliance capsule inside each microservice; service A mints tenant-local tokens over &lt;code&gt;direct-vm://identity-token&lt;/code&gt; with no network hop. No Tsak — &lt;code&gt;services.AddRedbIdentity(...)&lt;/code&gt; is enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 4 — &lt;a href="https://github.com/redbase-app/redb-tsak" rel="noopener noreferrer"&gt;redb.Tsak&lt;/a&gt; for an enterprise control plane.&lt;/strong&gt; When Identity lives in Tsak you get a management layer you'd otherwise hand-write:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What Tsak adds&lt;/th&gt;
&lt;th&gt;For Identity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Hot-reload&lt;/strong&gt; of one &lt;code&gt;.tpkg&lt;/code&gt; without dropping in-flight messages in the other contexts&lt;/td&gt;
&lt;td&gt;Update &lt;code&gt;redb.Identity.Core.Module.tpkg&lt;/code&gt; (a new redirect_uri check) — Kafka consumers / the audit pipeline / federation callbacks keep running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Multiple named contexts&lt;/strong&gt; in one process&lt;/td&gt;
&lt;td&gt;Identity next to your &lt;code&gt;orders&lt;/code&gt; / &lt;code&gt;payments&lt;/code&gt; / &lt;code&gt;analytics&lt;/code&gt; — each isolated via its own &lt;code&gt;AssemblyLoadContext&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Blazor Server dashboard&lt;/strong&gt; + REST API&lt;/td&gt;
&lt;td&gt;An operator sees every identity route, per-route metrics, ring-buffer logs, and can stop one route (say &lt;code&gt;/connect/register&lt;/code&gt; under attack) without touching the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;A CLI&lt;/strong&gt; (&lt;code&gt;tsak route stop&lt;/code&gt;, &lt;code&gt;tsak context stop orders&lt;/code&gt;, …)&lt;/td&gt;
&lt;td&gt;CI/CD without a custom kubectl wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;REST endpoints&lt;/strong&gt; (&lt;code&gt;/api/contexts&lt;/code&gt;, &lt;code&gt;/api/modules&lt;/code&gt;, &lt;code&gt;/api/routes&lt;/code&gt;, &lt;code&gt;/api/cluster&lt;/code&gt;, &lt;code&gt;/api/logs&lt;/code&gt;, &lt;code&gt;/api/watchdog&lt;/code&gt;, &lt;code&gt;/api/scheduler&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Your own ops portal via the typed &lt;code&gt;ITsakApiClient&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Cluster mode with leader election&lt;/strong&gt; + context redistribution&lt;/td&gt;
&lt;td&gt;A replica dies — Tsak redirects its contexts to healthy nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;A Quartz &lt;code&gt;IScheduler&lt;/code&gt;&lt;/strong&gt; in every context&lt;/td&gt;
&lt;td&gt;Token cleanup, session expiry, JWKS auto-rotation, audit retention on a standard cron&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;A watchdog&lt;/strong&gt; that spots hung routes&lt;/td&gt;
&lt;td&gt;A federation callback stuck at 30s gets restarted — no manual ops page at 3 a.m.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Prometheus &lt;code&gt;/metrics&lt;/code&gt;&lt;/strong&gt; + &lt;strong&gt;OTLP/Jaeger traces&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Per-token-issue p50/p95/p99 + per-route error rate in Grafana, traces in Jaeger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;API key + HMAC-SHA256 + roles + expiry + revocation&lt;/strong&gt; for the admin API&lt;/td&gt;
&lt;td&gt;Identity admin endpoints (e.g. &lt;code&gt;/signing-keys/rotate&lt;/code&gt;) sit behind the same auth layer as route management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Five-layer config hot-reload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Change &lt;code&gt;Identity.RateLimit.PerIpPerMinute&lt;/code&gt; in &lt;code&gt;context.json&lt;/code&gt; — picked up, no restart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tsak earns its keep when you have dozens of independent pipelines and Identity is one of them; when an ops team wants a browser dashboard over &lt;code&gt;kubectl exec&lt;/code&gt;; when Identity runs on 3+ nodes and you want automatic redistribution; when you need hot-reload without downtime windows; or when you want to drop in your own extensions (&lt;code&gt;IModuleProvider&lt;/code&gt;, &lt;code&gt;IRouteLifecycleListener&lt;/code&gt;, a watchdog strategy — plain interfaces in &lt;code&gt;redb.Tsak.Core&lt;/code&gt;). It's overkill when it's one server you can restart, an ASP.NET monolith with Identity inside, or a single sidecar microservice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The identity stack doesn't know Tsak exists&lt;/strong&gt; — there's not one &lt;code&gt;using redb.Tsak&lt;/code&gt; in &lt;code&gt;redb.Identity.Core&lt;/code&gt;. The deployment spectrum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌── Option A ── A 30-line Program.cs (junior-friendly) ─────────────────────┐
│   Worker Service + AddRedbIdentity + AddRoutes. Deploy via systemd / Docker.│
├── Option B ── An ASP.NET Core monolith with Identity inside ──────────────┤
│   One process, Identity next to your controllers.                          │
├── Option C ── A sidecar in each microservice ─────────────────────────────┤
│   In-process embedding — direct-vm token issue, no network hop.            │
├── Option D ── Tsak with a single identity module ─────────────────────────┤
│   Hot-reload, dashboard, REST/CLI. No cluster mode, no dozens of modules.  │
├── Option E ── Tsak full enterprise: Identity + your 20 other modules ─────┤
│   Cluster + leader election + Prometheus + Jaeger + Grafana + k8s.         │
│   Your own custom Tsak extensions (IModuleProvider, watchdog, ...).        │
└────────────────────────────────────────────────────────────────────────────┘
                  ⬇️ Same identity server at every level ⬇️
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Extending alongside Identity — your own scheme, your own routes
&lt;/h2&gt;

&lt;p&gt;The identity server isn't a closed box — it lives in the same REDB and the same &lt;code&gt;IRouteContext&lt;/code&gt; as any business route of yours. So you declare a typed props class, register the scheme next to the identity schemes (no ALTER TABLE), get full CRUD + LINQ through the same &lt;code&gt;IRedbService&lt;/code&gt;, and WireTap your routes onto &lt;code&gt;identity-events&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;// Your module next to Identity — no permission to ask for&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;OrgEnrollmentProps&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;long&lt;/span&gt; &lt;span class="n"&gt;UserId&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;// FK to an identity user via _users._id&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;OrganisationCode&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="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;EnrolledAt&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="n"&gt;Role&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="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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;Tags&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="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddRedbScheme&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrgEnrollmentProps&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;enrollments&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;OrgEnrollmentProps&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;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;OrganisationCode&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="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;o&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;userId&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;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IdentityEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;)&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;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;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;"event-type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"UserCreated"&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;,&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="cm"&gt;/* your onboarding flow */&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://org-enrollment-pipeline"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No extension points, no SPI, no plugin manifests, no rebuild of the identity server — it's ordinary C# in an ordinary assembly that &lt;strong&gt;co-exists&lt;/strong&gt; with the identity stack in one process and one database. Drop it into Tsak as its own &lt;code&gt;.tpkg&lt;/code&gt; (deploys independently) or compile it into the same worker.&lt;/p&gt;

&lt;h3&gt;
  
  
  An addon calls the identity server from the inside over direct-vm — no HTTP, one transaction
&lt;/h3&gt;

&lt;p&gt;This is &lt;strong&gt;stronger&lt;/strong&gt; than a WireTap. The identity routes are published as &lt;code&gt;direct-vm://identity-token&lt;/code&gt;, &lt;code&gt;direct-vm://identity-authorize&lt;/code&gt;, &lt;code&gt;direct-vm://identity-users-delete&lt;/code&gt;, … — all the standard endpoint URIs (&lt;code&gt;IdentityEndpoints.Token&lt;/code&gt;, &lt;code&gt;.Authorize&lt;/code&gt;, …, the full list in &lt;a href="https://github.com/redbase-app/redb-identity/blob/main/src/redb.Identity.Contracts/Routes/IdentityEndpoints.cs" rel="noopener noreferrer"&gt;&lt;code&gt;IdentityEndpoints.cs&lt;/code&gt;&lt;/a&gt;). So an addon &lt;strong&gt;calls Identity like its own library — via message passing, but inside one process&lt;/strong&gt; with no network hop.&lt;/p&gt;

&lt;p&gt;Example: the addon enrolls a user in an organization &lt;strong&gt;and mints their onboarding token&lt;/strong&gt; through the identity server, all in one transaction:&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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrgEnrollmentRoutes&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RouteBuilder&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;WithRedbTx&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-vm://org-enrollment-create-with-token"&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;e&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;// 1. Create the enrollment record in OUR own scheme&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;enrollment&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;OrgEnrollmentProps&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="n"&gt;OrgEnrollmentProps&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;UserId&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;GetHeader&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="s"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;OrganisationCode&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="n"&gt;EnrolledAt&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;UtcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"member"&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;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;enrollment&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="c1"&gt;// 2. ↓↓↓ CALL THE IDENTITY SERVER over direct-vm — NO HTTP ↓↓↓&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;,&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="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;"grant_type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"client_credentials"&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;"client_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"org-onboarding-bot"&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;"scope"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"openid org:onboarding"&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;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;IdentityEndpoints&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="c1"&gt;// ← "direct-vm://identity-token" — Identity in the same process&lt;/span&gt;
            &lt;span class="c1"&gt;// ↑↑↑ access_token is already in e.Out.Body, no HTTP round-trip happened ↑↑↑&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="k"&gt;async&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;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;tokenResponse&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;Out&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;as&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="kt"&gt;object&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;accessToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="s"&gt;"access_token"&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_emailSender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendOnboardingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* userId */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accessToken&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="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;What happened: one route does the enrollment in its own scheme + a token issue in the identity stack + an email notification, in sequence, in a single exchange chain; &lt;code&gt;WithRedbTx(...)&lt;/code&gt; wraps &lt;strong&gt;all of it&lt;/strong&gt; — both the save into &lt;code&gt;OrgEnrollmentProps&lt;/code&gt; and Identity's token issue (which also writes to &lt;code&gt;_objects&lt;/code&gt; via &lt;code&gt;RedbTokenStore&lt;/code&gt;) — one redb tx, one atomic commit; no &lt;code&gt;HttpClient&lt;/code&gt;, no JSON serialization, no TLS handshake; and the same correlation id flows through, so Identity logs the token issue under the same trace id as the enrollment. Any direct-vm endpoint is available to the addon as an in-process call — not "SDK wrappers," the endpoints themselves.&lt;/p&gt;

&lt;p&gt;For contrast: to mint a token from Java next to Keycloak you have to write a wrapper (HttpClient → form-data → HTTP → JSON back → retry/circuit-breaker → SDK classes → keep the SDK in sync with the Keycloak version). Every wrapper is a failure point, a serialization point, &lt;strong&gt;a separate-transaction point&lt;/strong&gt; (atomic semantics between "minted a token" and "wrote to my DB" are impossible), a version-drift point, and a separate-test point. Here it's one line, &lt;code&gt;.To(IdentityEndpoints.Token)&lt;/code&gt;, and an atomic commit via &lt;code&gt;WithRedbTx&lt;/code&gt;. That gap is exactly why people write their own identity servers in the first place: the boxed ones won't let you use identity as a library next to your business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your own facade on your own protocol
&lt;/h3&gt;

&lt;p&gt;The HTTP facade is just one connector over the direct-vm core. Nothing stops you from putting &lt;strong&gt;your&lt;/strong&gt; facade in front of a corporate/proprietary protocol. The core doesn't care — it receives messages over &lt;code&gt;direct-vm://&lt;/code&gt;, and how they got there is your business:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Corporate binary RPC&lt;/strong&gt; (custom framing over TCP, cert pinning)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From("custom-rpc://0.0.0.0:9999")&lt;/code&gt; → parse the frame → &lt;code&gt;To(IdentityEndpoints.Token)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A gRPC facade&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From("grpc://.../oidc.IdentityService/IssueToken")&lt;/code&gt; → map to headers → &lt;code&gt;To(IdentityEndpoints.Token)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;MQTT for IoT&lt;/strong&gt; (devices that can't do HTTP/2)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From("mqtt://broker:1883/device/+/token-request")&lt;/code&gt; → forward into the device_code grant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;An internal enterprise bus&lt;/strong&gt; (WMQ as the only transport in a bank's perimeter)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From("wmq://QM.PROD?queue=IDENTITY.TOKEN.REQ")&lt;/code&gt; → forward → reply on &lt;code&gt;IDENTITY.TOKEN.RESP&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;A crypto wrapper&lt;/strong&gt; (token requests in a custom envelope)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.Process(DecryptEnvelope)&lt;/code&gt; → &lt;code&gt;To(IdentityEndpoints.Token)&lt;/code&gt; → &lt;code&gt;.Process(EncryptEnvelope)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Any redb.Route connector can front the identity server. Want tokens minted from a RabbitMQ queue? One RouteBuilder. A Slack bot as the identity admin API? One RouteBuilder.&lt;/p&gt;

&lt;h4&gt;
  
  
  A separate control facade for admin endpoints on a public perimeter
&lt;/h4&gt;

&lt;p&gt;A scenario people rarely spell out: the identity server has to sit on a public IP (cloud edge, partner API, multi-region). You can't blanket it in a VPN/mTLS — real RP clients come in over HTTPS from browsers and mobile apps, an external federation IdP calls back to your &lt;code&gt;/connect/federation/callback&lt;/code&gt;, and back-channel logout needs a publicly resolvable URL. So the standard OIDC endpoints (&lt;code&gt;/connect/*&lt;/code&gt;) &lt;strong&gt;have to&lt;/strong&gt; be public.&lt;/p&gt;

&lt;p&gt;But the &lt;strong&gt;critical admin endpoints&lt;/strong&gt; — &lt;code&gt;/signing-keys/rotate&lt;/code&gt;, &lt;code&gt;/users/{id}/force-revoke&lt;/code&gt;, &lt;code&gt;/internal/bootstrap-admin&lt;/code&gt;, application mutations — &lt;strong&gt;don't&lt;/strong&gt; have to be HTTP. You can expose them through a separate facade over your own proprietary TCP framing with custom magic bytes, a protocol version, non-standard markers — on a separate port, on a separate network:&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 control facade over a proprietary protocol — separate port, separate network&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;"custom-tcp://0.0.0.0:7891"&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;"identity-admin-proprietary-facade"&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;ValidateMagicBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// ← a client that doesn't know the format never gets in&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;ParseCustomFraming&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// ← binary format with CRC + version + opcode&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;VerifyProprietaryAuthHmac&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="c1"&gt;// ← your own HMAC scheme, not an RFC&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;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"opcode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0x42&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;IdentityEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SigningKeyRotate&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;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"opcode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0x43&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;IdentityEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserForceRevoke&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;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"opcode"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0x44&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;IdentityEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BootstrapAdmin&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;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SerializeCustomResponse&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;This is not a substitute for crypto&lt;/strong&gt; — auth + HMAC + a role check are on every opcode. It's an extra layer of defense-in-depth that works not on crypto but on &lt;strong&gt;reduced attack surface and information asymmetry&lt;/strong&gt;: off-the-shelf toolkits (Burp, sqlmap, msf) and botnet scanners don't understand custom framing and don't scan custom TCP protocols at all; an attacker has to reverse-engineer the format before they can even form a syntactically valid request. Every day of that delay is a day for your security team to detect and respond. It's a pattern banks, defense, and the public sector have long used (their own RDBMS protocols, their own serialization on service-to-service channels); it used to require rewriting the stack around a proprietary protocol, and now it's one RouteBuilder in front of a standard OIDC core. Standard RPs keep talking &lt;code&gt;/connect/*&lt;/code&gt; over HTTPS — nothing changes for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extension points — everywhere, not just Identity
&lt;/h3&gt;

&lt;p&gt;The freedom to extend is a property of the whole ecosystem:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Extension points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;REDB storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IRedbObjectStorageProvider&lt;/code&gt; (a different backend under &lt;code&gt;_objects&lt;/code&gt;), &lt;code&gt;ISqlDialect&lt;/code&gt;, &lt;code&gt;IPasswordHasher&lt;/code&gt;, &lt;code&gt;IPropsSaveStrategy&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;redb.Route&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;your own connector (&lt;code&gt;From("myprotocol://...")&lt;/code&gt;), &lt;code&gt;IProcessor&lt;/code&gt;, a custom EIP, &lt;code&gt;IDataFormatRegistry&lt;/code&gt;, &lt;code&gt;IExpression&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;redb.Identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IExternalUserProvider&lt;/code&gt;, &lt;code&gt;IPasswordHasher&lt;/code&gt;, &lt;code&gt;IPasswordBreachChecker&lt;/code&gt;, &lt;code&gt;IRateLimitStore&lt;/code&gt;, &lt;code&gt;IEmailNotificationChannel&lt;/code&gt;, &lt;code&gt;IBackgroundDeletionService&lt;/code&gt;, &lt;code&gt;IClaimMapper&lt;/code&gt; — plus any custom RouteBuilder as a peer route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;redb.Tsak&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IModuleProvider&lt;/code&gt;, &lt;code&gt;IRouteLifecycleListener&lt;/code&gt;, &lt;code&gt;IWatchdogStrategy&lt;/code&gt;, &lt;code&gt;ITsakAuthProvider&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;redb.Route.Controllers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IControllerDispatcher&lt;/code&gt;, &lt;code&gt;IActionResultMapper&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same shape everywhere: a public interface with a clear contract + DI registration + your implementation. Register yours and Identity picks it up without noticing the swap. No SPI manifests, no vendor lock-in moments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Combinatorics with the LLM connector
&lt;/h3&gt;

&lt;p&gt;Put an LLM connector next to Identity (&lt;a href="https://habr.com/ru/articles/1046237/" rel="noopener noreferrer"&gt;&lt;code&gt;redb.Route.Llm&lt;/code&gt;&lt;/a&gt; — Anthropic / OpenAI / Azure OpenAI), a Kafka stream of identity events, and your own domain schemes. Passwords stay Argon2id-hashed in the DB the whole time — the LLM never sees plaintext (the prompt gets an &lt;code&gt;IdentityEvent&lt;/code&gt;: &lt;code&gt;event-type&lt;/code&gt;, &lt;code&gt;user-id&lt;/code&gt;, &lt;code&gt;client-id&lt;/code&gt;, &lt;code&gt;ip-address&lt;/code&gt;, timestamps; sensitive material is filtered in &lt;code&gt;EventDispatchProcessor&lt;/code&gt; before the WireTap). What you get is another &lt;code&gt;.To()&lt;/code&gt; / &lt;code&gt;.Filter()&lt;/code&gt; / &lt;code&gt;.Process()&lt;/code&gt; in your RouteBuilder, not a separate product behind an extra license:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Pipeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;AI anomaly detector&lt;/strong&gt; on login patterns&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From(IdentityEvents).Filter(type=="LoginSuccess").To("llm://anthropic?prompt=...")&lt;/code&gt; → risk score → &lt;code&gt;direct-vm://identity-sessions-revoke&lt;/code&gt; past a threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Real-time fraud scoring&lt;/strong&gt; on token issue&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From(IdentityEndpoints.Token).WireTap(To("llm://..."))&lt;/code&gt; → score &amp;gt; threshold → revoke + flag-for-review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Conversational admin&lt;/strong&gt; via Telegram/Slack&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From("telegram://admin-bot")&lt;/code&gt; → an LLM agent with &lt;code&gt;.AsLlmTool()&lt;/code&gt; wrappers over admin endpoints → "revoke all sessions for &lt;a href="mailto:foo@acme.com"&gt;foo@acme.com&lt;/a&gt;" in one message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LLM classification of audit events&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From(IdentityEvents).To("llm://claude?prompt=Classify severity...")&lt;/code&gt; → &lt;code&gt;To("kafka://security-incidents-prioritised")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Auto compliance reports&lt;/strong&gt; per quarter&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;From("cron://quarterly-report")&lt;/code&gt; → &lt;code&gt;To("postgres://identity_audit?query=...")&lt;/code&gt; → LLM aggregation → &lt;code&gt;.To("email://compliance@org.com")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;LLM validation of a redirect_uri&lt;/strong&gt; at DCR&lt;/td&gt;
&lt;td&gt;a new OIDC client → &lt;code&gt;.To("llm://...?prompt=Is this redirect_uri benign?")&lt;/code&gt; → human-in-the-loop for the suspicious ones&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You already have Identity with direct-vm endpoints, the LLM connector with &lt;code&gt;.AsLlmTool()&lt;/code&gt;, Kafka/RabbitMQ for streaming, Tsak for deployment, and your own RouteBuilders — wiring them into pipelines is a couple of hours, not a quarter-long epic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enterprise scale — thousands of microservices, hundreds of thousands of users
&lt;/h2&gt;

&lt;p&gt;Those features (a transport-agnostic core, schemes as data, cluster-safe primitives, live JWKS refresh) exist because a modern enterprise looks like this: thousands of microservices talking &lt;code&gt;client_credentials&lt;/code&gt; and token-exchange delegation chains; hundreds of thousands of live sessions (mobile + web + thick clients) with concurrent federation round-trips; several datacenters running identity replicas behind an LB; and regulatory rules that forbid identity data from leaving the perimeter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service-to-service auth — without the round-trip
&lt;/h3&gt;

&lt;p&gt;The classic shape: &lt;code&gt;service-A → POST /connect/token → identity (HTTP)&lt;/code&gt;, then &lt;code&gt;service-B → POST /connect/introspect → identity (HTTP)&lt;/code&gt;. That's two network hops to Identity on every internal call — at 10K internal RPS the identity server becomes a bottleneck, a single point of failure, and p99 tax. Here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Embedding mode.&lt;/strong&gt; &lt;code&gt;redb.Identity.Core&lt;/code&gt; links into each microservice that has to mint/validate tokens; &lt;code&gt;client_credentials&lt;/code&gt; issue goes over &lt;code&gt;direct-vm://identity-token&lt;/code&gt; — zero network hop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT validation without introspect.&lt;/strong&gt; Resource servers validate the bearer locally via &lt;code&gt;redb.Identity.Resource.Dpop&lt;/code&gt; (or the vanilla &lt;code&gt;JwtSecurityTokenHandler&lt;/code&gt; for non-DPoP cases). The JWKS is fetched through &lt;code&gt;IConfigurationManager&amp;lt;OpenIdConnectConfiguration&amp;gt;&lt;/code&gt; with a long TTL — a network call once an hour, not per bearer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token exchange (RFC 8693) for delegation chains.&lt;/strong&gt; Service A takes the user's token and calls service B on their behalf; service B sees the &lt;code&gt;act&lt;/code&gt; claim chain (RFC 8693 §4.1) with the delegation history. &lt;code&gt;demo_token_exchange.ps1&lt;/code&gt; covers it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cluster stability — for many-thousand sessions
&lt;/h3&gt;

&lt;p&gt;Identity is built for N replicas where leadership rotates with no external coordination:&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;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stateless replicas&lt;/td&gt;
&lt;td&gt;all handlers are Scoped, no static state — any replica serves any request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DataProtection key ring across replicas&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RedbXmlRepository&lt;/code&gt; — keys in redb; each replica refreshes its snapshot every &lt;code&gt;XmlRepositoryRefreshInterval&lt;/code&gt; (60s). &lt;strong&gt;Not&lt;/strong&gt; cluster-gated — every node must catch keys others rotated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth signing keys across replicas&lt;/td&gt;
&lt;td&gt;redb-backed RSA 2048 PEMs, DataProtection-encrypted, bootstrapped under a distributed lock (one replica mints the first key)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Federation nonce / rate-limit counters&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;Redis&lt;/code&gt; backend, cluster-wide (RFC 6749 §10.12 / RFC 6585 §4)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema init&lt;/td&gt;
&lt;td&gt;a cluster-wide lock via &lt;code&gt;LockForUpdate&lt;/code&gt; — leader initializes, followers wait&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background trash purge&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TryClaimOrphanedTaskAsync&lt;/code&gt; — any replica picks up an orphan after another crashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signing key rotation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IOptionsMonitorCache.TryRemove&lt;/code&gt; invalidates the cache on every replica&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Back-channel logout across replicas&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/revoked-sids/add&lt;/code&gt; writes the revocation; &lt;code&gt;/revoked-sids/since?cursor=&lt;/code&gt; lets each RP replica pull deltas. Push-and-poll survives lost nodes and network splits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No ZooKeeper, no separate store for keys — it's all redb objects. In a three-node cluster around ~5K /token RPS the stack runs at ~50–80ms p99 (the Pro path with PVT indexes on &lt;code&gt;value_string&lt;/code&gt; for the client_id lookup).&lt;/p&gt;

&lt;h3&gt;
  
  
  Data sovereignty — identity stays inside the perimeter
&lt;/h3&gt;

&lt;p&gt;Self-hosted means the identity data physically lives on your hardware. No callback to a third-party processor, no shipping emails/phones to a cloud vendor. That matters for banks (KYC on a regulated network), health/medical (HIPAA / GDPR Article 9), the public sector (data residency), and B2B SaaS on-prem (the customer deploys into their own VPC). &lt;code&gt;redb.Identity&lt;/code&gt; &lt;strong&gt;doesn't phone home&lt;/strong&gt; — no telemetry, no pings, no update checks. One Postgres + one .NET worker + your network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-tenant through schemes
&lt;/h3&gt;

&lt;p&gt;Classic OIDC multi-tenancy — &lt;code&gt;/{tenant}/connect/token&lt;/code&gt;, separate DB schemas, separate key rings — needs tenant routing in every handler, tenant-aware migrations, and per-tenant key rotation. Here isolation is possible at the REDB scheme level: the split is physical, through &lt;code&gt;_id_scheme&lt;/code&gt;, and a query from tenant A can't touch tenant B's data even through a bad filter. Today it's a &lt;strong&gt;capability&lt;/strong&gt;, not a packaged feature: production deployments usually start single-tenant and layer multi-tenancy on via a &lt;code&gt;tenantId&lt;/code&gt; claim — we'll prioritize it on inbound demand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Live demos: 61 probes against a real server
&lt;/h2&gt;

&lt;p&gt;Beyond the xUnit suite, &lt;code&gt;demos/&lt;/code&gt; holds &lt;strong&gt;61 self-contained PowerShell probes&lt;/strong&gt; that drive a &lt;strong&gt;live&lt;/strong&gt; server over its real HTTP surface — one &lt;code&gt;demo_*.ps1&lt;/code&gt; per protocol contract. Each sets up its own client via DCR, runs the flow end-to-end, and asserts the wire-level result (status codes, headers, token/JWKS shape, replay rejection). Executable RFC docs and a black-box regression net in one.&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="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Identity/demos&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\run_all.ps1&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="c"&gt;# every demo_*.ps1 in canonical order&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\run_all.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mfa&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c"&gt;# only the MFA ones&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\run_all.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-StopOnFail&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c"&gt;# bail on the first failure&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;run_all.ps1&lt;/code&gt; runs each probe in its own &lt;code&gt;pwsh&lt;/code&gt; child process (a hard failure in one can't take the rest down), streams output, captures a transcript to &lt;code&gt;demos/_logs/&lt;/code&gt;, and prints a pass/fail table. The map by category:&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%2Fhfeo7ep8ng8fosv4inn3.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%2Fhfeo7ep8ng8fosv4inn3.png" alt="redb.Identity" width="731" height="1224"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Grant types        demo_client_credentials / demo_authcode_pkce / demo_refresh_rotation
                   demo_device_code (+_ci) / demo_password_ropc / demo_token_exchange
Endpoints          demo_discovery_jwks / demo_discovery_shape / demo_introspect_revoke
                   demo_userinfo / demo_jwks_rotation
Authorize surface  demo_auth_extras (RFC 9207 iss, form_post) / demo_prompt_max_age
                   demo_acr_values / demo_claim_probes
PAR / DPoP         demo_par / demo_par_per_client / demo_dpop
Logout             demo_logout_endsession / demo_backchannel_logout
DCR                demo_dcr_lifecycle / demo_private_key_jwt
SCIM 2.0           demo_scim / demo_scim_bulk / demo_scim_etag
MFA                demo_mfa_totp / demo_mfa_recovery_codes / demo_mfa_disable_replace
                   demo_password_change_negatives
Self-service       demo_account_register_verify / demo_me_profile / demo_me_email_change
                   demo_me_sessions / demo_me_delete / demo_password_reset
Federation         demo_federation / demo_federation_e2e / demo_federation_github
                   demo_federation_link_unlink
Admin              demo_admin_scopes / demo_groups_roles_claims / demo_sessions_admin
                   demo_throttle_rfc6585 / demo_jwt
&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%2Fzmz4xeeet23vfu257lts.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%2Fzmz4xeeet23vfu257lts.png" alt="redb.Tsak" width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The only probe that needs a human is device-code consent — there's a non-interactive &lt;code&gt;demo_device_code_ci.ps1&lt;/code&gt; for CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reference BFF + Blazor admin, included
&lt;/h2&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%2Fv7ohzxfoaafhuxx39bka.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%2Fv7ohzxfoaafhuxx39bka.png" alt="redb.Identity web BFF" width="800" height="920"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;redb.Identity.Web&lt;/code&gt; is a reference Blazor Server app that demonstrates the HARDLINE BFF pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;references &lt;strong&gt;only&lt;/strong&gt; &lt;code&gt;redb.Identity.Contracts&lt;/code&gt; + &lt;code&gt;redb.Identity.Client&lt;/code&gt; — no &lt;code&gt;Core&lt;/code&gt;, no &lt;code&gt;Http&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;every Identity call goes through the typed &lt;code&gt;IIdentityClient&lt;/code&gt; — &lt;strong&gt;never&lt;/strong&gt; a raw &lt;code&gt;HttpClient&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;OIDC Authorization Code + PKCE, &lt;code&gt;SaveTokens=true&lt;/code&gt;, back-channel-logout sink + poll fallback for multi-replica RPs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pages cover both self-service (&lt;code&gt;/Me/*&lt;/code&gt;: profile, password, MFA/TOTP, WebAuthn credentials, sessions, federated links, consents) and admin (&lt;code&gt;/Admin/*&lt;/code&gt;: users, groups-as-tree, applications, scopes, claim mappers, roles, federation providers, sessions, tokens, audit with a JsonViewer, SCIM browser, settings). It ships as &lt;strong&gt;source&lt;/strong&gt; in the repo, not a package — clone it and run it, or lift it as a starting point.&lt;/p&gt;

&lt;p&gt;And a typed SDK for anyone calling Identity from the outside:&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;AddIdentityClient&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;BaseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://identity.local"&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;AccessTokenProvider&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;ClientCredentialsAccessTokenProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"my-svc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"IdentitySvcSecret"&lt;/span&gt;&lt;span class="p"&gt;]!,&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"identity:manage"&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IIdentityClient&lt;/span&gt; &lt;span class="n"&gt;identity&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="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;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;page&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;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ListAsync&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;UsersListRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Limit&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="n"&gt;ct&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;u&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;PersistAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&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;// back-channel revocations — pull deltas since the last cursor&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;since&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;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RevokedSids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSinceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cursor&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;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;e&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Apply&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;_cursor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NextCursor&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;Every HTTP error is normalized to RFC 7807 &lt;code&gt;ProblemDetails&lt;/code&gt; and surfaces as &lt;code&gt;ApiException&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A little honest comparison
&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;ASP.NET-bound IS (Duende, OpenIddict samples)&lt;/th&gt;
&lt;th&gt;redb.Identity&lt;/th&gt;
&lt;th&gt;Standalone IAM (Keycloak / Auth0)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Call &lt;code&gt;token&lt;/code&gt; from another in-process module&lt;/td&gt;
&lt;td&gt;Loopback HTTP&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;To("direct-vm://identity-token")&lt;/code&gt;&lt;/strong&gt; — same exchange, zero-copy&lt;/td&gt;
&lt;td&gt;Network hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add a transport (gRPC, MQ, SignalR)&lt;/td&gt;
&lt;td&gt;Rewrite endpoints as gRPC services / consumers&lt;/td&gt;
&lt;td&gt;Drop a facade &lt;code&gt;.tpkg&lt;/code&gt;, point it at &lt;code&gt;direct-vm://&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Vendor adapter (if it exists)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace HTTP with RabbitMQ entirely&lt;/td&gt;
&lt;td&gt;Major refactor&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Drop the HTTP facade. Keep Core.&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Custom EF schema + migrations&lt;/td&gt;
&lt;td&gt;redb &lt;code&gt;RedbObject&amp;lt;TProps&amp;gt;&lt;/code&gt; — code-first, no migrations&lt;/td&gt;
&lt;td&gt;Vendor schema, vendor migrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom claims&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;jsonb&lt;/code&gt; blob + your own indexes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dictionary&lt;/code&gt; on props — natively queryable&lt;/td&gt;
&lt;td&gt;Vendor attribute model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-replica key sharing&lt;/td&gt;
&lt;td&gt;DIY DataProtection + JWKS&lt;/td&gt;
&lt;td&gt;Built-in: redb key-ring + signing key store&lt;/td&gt;
&lt;td&gt;Built-in (vendor-specific)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embed in your worker as a library&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes — one or two &lt;code&gt;.tpkg&lt;/code&gt;, or call Core directly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB providers&lt;/td&gt;
&lt;td&gt;One per build&lt;/td&gt;
&lt;td&gt;PG / MSSQL / SQLite from one codebase&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An addon as a peer in the same DB/transaction&lt;/td&gt;
&lt;td&gt;Controllers alongside, but storage is separate (EF)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Own scheme + direct-vm calls + one redb tx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only from outside, over REST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Mixed (Duende: commercial)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Apache 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mixed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two honest caveats so this doesn't read as a hit piece. Keycloak/Auth0 are finished products with admin UX, multi-tenancy, and an ecosystem we don't reproduce one-for-one; if you want "install the box and stop thinking," they're still a valid pick. Duende IdentityServer is the most mature .NET stack out there with a huge community — the argument isn't "it's bad," it's "it's architecturally married to the HTTP pipeline, and that isn't always what you want." On social providers out of the box (Sign in with Apple, LinkedIn, X) Auth0/Okta are ahead — we ship generic OIDC + GitHub OAuth2, and the rest you write as federation providers (there's a mock provider in &lt;code&gt;demo_federation_e2e.ps1&lt;/code&gt; showing how). That's an honest limit, not a blocker. redb.Identity wins precisely where identity should be a &lt;strong&gt;built-in part&lt;/strong&gt; of your .NET system rather than a service next door, and where you'd rather not drag along migrations and a second storage model.&lt;/p&gt;




&lt;h2&gt;
  
  
  What isn't done yet (and that's fine)
&lt;/h2&gt;

&lt;p&gt;This list is honest because it's checkable: everything absent from it, our own &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; will confirm or refute in a single &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not implemented:&lt;/strong&gt; RFC 8705 (mTLS + cert-bound tokens — &lt;code&gt;tls_client_certificate_bound_access_tokens: false&lt;/code&gt;), 9101 (JAR — request objects aren't consumed; &lt;code&gt;request=&lt;/code&gt; gets &lt;code&gt;request_not_supported&lt;/code&gt;), 8707 (Resource Indicators), 9396 (RAR), 9470 (step-up), FAPI 2.0, CIBA, OIDC Federation 1.0. From OIDC Core: no pairwise &lt;code&gt;sub&lt;/code&gt; (public only) and, per OAuth 2.1, no implicit and no hybrid flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two things we deliberately chose &lt;em&gt;not&lt;/em&gt; to build — and the reasoning matters more than the checkbox:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Front-Channel Logout 1.0.&lt;/strong&gt; It signs RPs out through an iframe to each client, which means it rides on &lt;strong&gt;third-party cookies&lt;/strong&gt;. Safari's ITP blocks them; Chrome is burying them. The mechanism is broken by design in a modern browser, and anyone ticking that box owes you a footnote saying "works sometimes." We ship &lt;strong&gt;Back-Channel Logout&lt;/strong&gt; instead — server to server, signed logout token, plus a pull feed of revoked &lt;code&gt;sid&lt;/code&gt;s for multi-replica RPs. It is strictly better and doesn't depend on cookies at all. We'd rather have the mechanism that works than the checkbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HOTP (RFC 4226) as a standalone method.&lt;/strong&gt; Counter-based OTP is effectively unused in 2026; the world runs on TOTP. RFC 4226 lives on as the &lt;em&gt;foundation&lt;/em&gt; of TOTP — and in that role we do implement it (160-bit secret per §4). Building a separate counter with a resync window purely to add a row to a table is work for the table, not for the user.&lt;/p&gt;

&lt;p&gt;Planned facades (same &lt;code&gt;.tpkg&lt;/code&gt; pattern, no Core changes): &lt;code&gt;redb.Identity.Grpc&lt;/code&gt;, &lt;code&gt;.Rmq&lt;/code&gt;/&lt;code&gt;.Amqp&lt;/code&gt;/&lt;code&gt;.IbmMq&lt;/code&gt;, &lt;code&gt;.SignalR&lt;/code&gt;, &lt;code&gt;.Kafka&lt;/code&gt; (event-only sink). PRs welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick start
&lt;/h2&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%2F77pbp1lfxu8psw5yq2ff.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%2F77pbp1lfxu8psw5yq2ff.png" alt="redb.Identity github" width="800" height="854"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fastest path — Docker (SQLite + HTTPS, out of the box).&lt;/strong&gt; The image is a working OpenID provider with zero configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5002:5002 ghcr.io/redbase-app/redb-identity-backend:latest
curl &lt;span class="nt"&gt;-k&lt;/span&gt; https://localhost:5002/.well-known/openid-configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It bundles a self-signed dev cert (&lt;code&gt;CN=localhost&lt;/code&gt;) — call it with &lt;code&gt;curl -k&lt;/code&gt; for a local trial. Variants: &lt;code&gt;backend&lt;/code&gt; (OIDC only), &lt;code&gt;managed&lt;/code&gt; (+ Tsak dashboard), &lt;code&gt;full&lt;/code&gt; (+ BFF login/admin on &lt;code&gt;:8087&lt;/code&gt;). Compose stacks and &lt;code&gt;DOCKER.md&lt;/code&gt; live in &lt;a href="https://github.com/redbase-app/redb-identity/tree/main/redb.Identity/publish/docker" rel="noopener noreferrer"&gt;&lt;code&gt;publish/docker/&lt;/code&gt;&lt;/a&gt;. Images are cosign-signed.&lt;/p&gt;

&lt;p&gt;Not a Docker fan? The &lt;a href="https://github.com/redbase-app/redb-identity/releases" rel="noopener noreferrer"&gt;&lt;strong&gt;releases&lt;/strong&gt;&lt;/a&gt; page has a self-contained archive (Worker + modules, also SQLite + HTTPS out of the box, cosign-signed): unpack and run &lt;code&gt;start-full.ps1&lt;/code&gt; / &lt;code&gt;start-full.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For embedding in your own project — packages are on nuget.org. Take what you need — the host picks the provider:&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;# Core OIDC / OAuth 2.1 engine (OpenIddict on redb.Route) + a storage provider&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Identity.Core&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Postgres.Pro&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c"&gt;# or redb.MSSql.Pro / redb.SQLite.Pro&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# HTTP facade (OIDC + management + SCIM)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Identity.Http&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Typed client SDK (IIdentityClient) — for BFFs / services&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Identity.Client&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or build the &lt;code&gt;.tpkg&lt;/code&gt; modules from source and drop them into a Tsak worker:&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="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Identity&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\scripts\pack-tpkg.ps1&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# → redb.Identity.Core.Module.tpkg + redb.Identity.Http.tpkg&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full dev setup with real infrastructure:&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;# 0. (optional) redb.CLI globally — for DB work from the terminal&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--global&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.CLI&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 1. Dev compose (Postgres + GreenMail + mock IdP + Redis + LDAP)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;compose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Identity/deploy/observability/docker-compose.yml&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 1.1. Initialize the REDB schema&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;postgres&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Host=localhost;Database=redb;Username=postgres;Password=postgres"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-v&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 2. Run the worker with the tpkg modules&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Tsak/src/redb.Tsak.Worker&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 3. In another window — run a demo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;redb.Identity/demos&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;demo_discovery_jwks.ps1&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c"&gt;# OIDC discovery + JWKS + manual verification&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;demo_authcode_pkce.ps1&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c"&gt;# full PKCE round-trip&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;demo_jwks_rotation.ps1&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c"&gt;# runtime signing-key rotation&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run_all.ps1&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="c"&gt;# every probe&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check a live server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:5000/.well-known/openid-configuration | jq
curl http://localhost:5000/.well-known/jwks | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the one-shot first-admin bootstrap (self-seals via a sentinel flag; a second call returns &lt;code&gt;410 Gone&lt;/code&gt;):&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;-X&lt;/span&gt; POST http://localhost:5000/internal/bootstrap-admin &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;'{ "username": "admin", "password": "...", "email": "admin@local" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full README — every endpoint table, the 79-event audit catalogue, the OpenTelemetry metrics map — is in the &lt;a href="https://github.com/redbase-app/redb-identity" rel="noopener noreferrer"&gt;repo&lt;/a&gt;. Issues and feedback via &lt;a href="https://github.com/redbase-app/redb-identity/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt; and Discussions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;redb.Identity is the fourth option that was missing. Not an ASP.NET-bound engine where every endpoint is an HTTP middleware. Not a standalone IAM box with its own runtime and deploy. And not "roll your own" for the third time.&lt;/p&gt;

&lt;p&gt;Three things you don't get elsewhere in one package:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protocol ≠ transport.&lt;/strong&gt; Every endpoint is a &lt;code&gt;direct-vm://&lt;/code&gt; route. HTTP is the first facade, but the next module calls &lt;code&gt;token&lt;/code&gt; with no network, zero-copy. Want another transport — drop a &lt;code&gt;.tpkg&lt;/code&gt;. Want HTTP gone — drop the facade, keep Core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No migrations.&lt;/strong&gt; Users, applications, scopes are C# classes. Add a field and it's in prod the next instant. Custom claims are natively queryable, no &lt;code&gt;jsonb&lt;/code&gt;, no hand-rolled indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity as part of your project.&lt;/strong&gt; Take just the engine, zero facades, and call OAuth endpoints with a method call from inside your process — down to an addon project that mints a token in the same transaction as its own domain data. An identity server as a library, not a service next door.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plus: PostgreSQL / MSSQL / SQLite from one codebase (1767 tests green on all three), RFC numbers cited right in the code, DPoP / PAR / DCR / SCIM / MFA / WebAuthn / back-channel logout, live JWKS rotation with no restart, a password-hashing pipeline with Argon2id + upgrade-on-login, audit "anywhere" proven by 8 integration tests against real brokers, logical-level backup + cross-provider migration, a cluster with no external coordination, host-agnostic deployment from a 30-line worker to a Tsak cluster, and a reference Blazor admin — all Apache 2.0.&lt;/p&gt;

&lt;p&gt;If you try it, tell me in the comments which embedding shape fits you: service next door, in-process library, or a full server behind HTTP. Deeper dives are coming in the series — WebAuthn/passkeys as a Route processor, DPoP step by step, SCIM sync with AD, MFA without a god-class.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sources and release: &lt;a href="https://github.com/redbase-app/redb-identity" rel="noopener noreferrer"&gt;github.com/redbase-app/redb-identity&lt;/a&gt;. About the redb database: &lt;a href="https://redbase.app/" rel="noopener noreferrer"&gt;redbase.app&lt;/a&gt;. Past posts on the &lt;a href="https://dev.to/rinat_kozin"&gt;profile&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If this was useful — a ⭐ on GitHub helps others find it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>oauth</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The RabbitMQ connector in redb.Route: the whole broker in one URI — RPC, competing consumers, dead-lettering. Still leaving MassTransit</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:07:01 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/the-rabbitmq-connector-in-redbroute-the-whole-broker-in-one-uri-rpc-competing-consumers-18m0</link>
      <guid>https://dev.to/rinat_kozin/the-rabbitmq-connector-in-redbroute-the-whole-broker-in-one-uri-rpc-competing-consumers-18m0</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%2Fb8r19gitp6htxpfawvz7.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%2Fb8r19gitp6htxpfawvz7.jpg" alt="redb.Route.RabbitMQ" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We did Kafka earlier in this series. Now it's &lt;strong&gt;RabbitMQ&lt;/strong&gt;'s turn, and this one leans hard on how you actually use it. The &lt;code&gt;redb.Route.RabbitMQ&lt;/code&gt; connector sits on top of the official &lt;code&gt;RabbitMQ.Client&lt;/code&gt; 7.x, but you don't write a "client" — you write routes, and the whole broker collapses into a single URI:&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;"rabbitmq://orders?exchange=shop&amp;amp;exchangeType=topic&amp;amp;routingKey=order.*&amp;amp;concurrentConsumers=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;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Order 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="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;Read it once and you already know the whole story: consume from queue &lt;code&gt;orders&lt;/code&gt;, bound to the topic exchange &lt;code&gt;shop&lt;/code&gt; on &lt;code&gt;order.*&lt;/code&gt;, four consumers wide, log it, pass it on. No "open a channel," no "declare a binding," no "set the QoS" — the connector handles every bit of that.&lt;/p&gt;

&lt;p&gt;About "leaving MassTransit," let me be straight with you: it's not that MassTransit can't do this — it can, RPC included (request/response via &lt;code&gt;IRequestClient&amp;lt;T&amp;gt;&lt;/code&gt;). The difference is the &lt;strong&gt;model&lt;/strong&gt;. MassTransit is a bus: message contracts, consumers, bus configuration — you buy into its whole worldview at once. redb.Route is explicit routes in the Apache Camel spirit: an endpoint is a URI, integration patterns are steps in a route, the transport is an abstraction underneath. Fewer ceremonies around contracts and registrations, more plain "from here → to there → and here's what happens along the way." Which one fits you is a matter of taste and of the job in front of you; this post is about what RabbitMQ looks like in the second model.&lt;/p&gt;

&lt;p&gt;Here's the map: the anatomy of the URI, &lt;strong&gt;every parameter as a reference table&lt;/strong&gt; (what it does and why), the properties the consumer and producer move to and from headers, competing consumers (concurrency), broadcasting across cluster nodes (a real production example), RPC (RabbitMQ makes this one especially clean), and the two EIP patterns that fit a broker like a glove — &lt;strong&gt;Filter&lt;/strong&gt; (on compiled predicates) and &lt;strong&gt;Dead Letter Channel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;All the code is in English; the examples aren't invented — they live in &lt;code&gt;redb.Route/demos/redb.Route.Demo/Routes&lt;/code&gt;, the same demo project that runs in CI.&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;/blockquote&gt;




&lt;h2&gt;
  
  
  Anatomy of the URI
&lt;/h2&gt;

&lt;p&gt;An endpoint is a string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rabbitmq://queue-name?host=localhost&amp;amp;exchange=my-exchange&amp;amp;exchangeType=topic&amp;amp;routingKey=order.*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scheme &lt;code&gt;rabbitmq://&lt;/code&gt;, the URI host is the queue name, then query parameters (names are in the table below). The same string works as a consumer (in &lt;code&gt;From(...)&lt;/code&gt;) and as a producer (in &lt;code&gt;.To(...)&lt;/code&gt;) — the role is decided by where the endpoint sits in the route. You don't have to repeat the connection: register a connection factory once and point at it with &lt;code&gt;connectionFactory=name&lt;/code&gt; — host, credentials and SSL live in one place, and the connection gets reused.&lt;/p&gt;

&lt;p&gt;One detail worth pausing on: parameter values are &lt;strong&gt;templates&lt;/strong&gt;. &lt;code&gt;routingKey=order.new&lt;/code&gt; is a constant, but &lt;code&gt;routingKey=order.${header.type}&lt;/code&gt; is &lt;strong&gt;dynamic&lt;/strong&gt; — the key is computed per message from its header (the same mechanism as Kafka's partition key). So "a string" here doesn't mean "static only" — a &lt;code&gt;${...}&lt;/code&gt; inside the value turns it into an expression.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;On the fluent builder.&lt;/strong&gt; The connector also ships a builder, &lt;code&gt;Rabbit.Queue("orders")…&lt;/code&gt;, but it's &lt;strong&gt;expression-first&lt;/strong&gt;: the string-ish methods take an expression so a producer value can be computed per message (&lt;code&gt;.RoutingKey(Header("order.type"))&lt;/code&gt; — a dynamic key, Kafka-style). For a plain constant there's now a &lt;code&gt;string&lt;/code&gt; overload too — &lt;code&gt;.Host("localhost")&lt;/code&gt; and &lt;code&gt;.RoutingKey("order.new")&lt;/code&gt; just work, and &lt;code&gt;${...}&lt;/code&gt; still interpolates. In this post I use the URI form — for static config it's shorter and easier to read at a glance; the dynamic-via-expressions side gets its own writeup.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Every parameter
&lt;/h2&gt;

&lt;p&gt;The reason to bookmark this post. Names are exactly as they appear in the URI; defaults are what you get out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Broker host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;port&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5672&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AMQP port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;username&lt;/code&gt; / &lt;code&gt;password&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;guest&lt;/code&gt; / &lt;code&gt;guest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;virtualHost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Virtual host — resource isolation inside the broker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connectionFactory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Name of a factory in the registry: settings in one place, connection reused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clientName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redb.Route&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connection name — shows up in the management UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Exchange
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exchange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Exchange name; empty = the default exchange (routed by queue name)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exchangeType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;direct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;direct&lt;/code&gt; / &lt;code&gt;topic&lt;/code&gt; / &lt;code&gt;fanout&lt;/code&gt; / &lt;code&gt;headers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exchangeDurable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Survives a broker restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exchangeAutoDelete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete once it's no longer in use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;declare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create the exchange + queue on startup if they don't exist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Queue
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(from the URI host)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Name; empty = an auto-generated exclusive queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;durable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Survives a restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;autoDelete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete once the last consumer leaves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exclusive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only one consumer allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Routing and message
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;routingKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(empty)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Key for publish and bind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;contentType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Message content type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messageTtl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Message TTL, ms (&lt;code&gt;x-message-ttl&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expires&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Queue is deleted after N ms with no consumers (&lt;code&gt;x-expires&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Consumer
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;concurrentConsumers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The one and only concurrency knob&lt;/strong&gt; — up to N messages handled at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prefetchCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prefetch per consumer; keep it ≥ &lt;code&gt;concurrentConsumers&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;autoAck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; = at-most-once (no requeue on error); &lt;code&gt;false&lt;/code&gt; = at-least-once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transacted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transactional AMQP channel (mutually exclusive with &lt;code&gt;autoAck&lt;/code&gt;)&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;&lt;em&gt;(auto)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Return an undeliverable message; defaults to &lt;code&gt;true&lt;/code&gt; for direct/headers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  RPC
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replyTo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turn on request/reply: publish the request and wait for the correlated response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;60&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long to wait for the reply, seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxOutstandingConfirms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2048&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cap on unconfirmed publishes — so you don't trip over the client's rate limiter under load&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Queue limits and dead-lettering
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;maxLength&lt;/code&gt; / &lt;code&gt;maxLengthBytes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Queue cap in messages / bytes (&lt;code&gt;x-max-length[-bytes]&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;overflow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;On overflow: &lt;code&gt;drop-head&lt;/code&gt; / &lt;code&gt;reject-publish&lt;/code&gt; / &lt;code&gt;reject-publish-dlx&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deadLetterExchange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;DLX — where dead messages go (&lt;code&gt;x-dead-letter-exchange&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deadLetterRoutingKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Routing key used when dead-lettering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;queueType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;classic&lt;/code&gt; / &lt;code&gt;quorum&lt;/code&gt; (a replicated queue for a cluster)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxPriority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Max priority for priority queues&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Resilience and TLS
&lt;/h3&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's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;automaticRecovery&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-recover the connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;topologyRecoveryEnabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Re-declare exchanges/queues/binds after recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;recoveryInterval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retry interval for recovery, seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;heartbeat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;60&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Heartbeat interval, seconds — broker and client ping each other so a dead connection gets noticed in time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connectionTimeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;60&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Don't hang on an unreachable node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turn on TLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sslServerName&lt;/code&gt; / &lt;code&gt;sslCertPath&lt;/code&gt; / &lt;code&gt;sslCertPassphrase&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;SNI and a client cert for mTLS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The startup validator won't let you shoot yourself in the foot: &lt;code&gt;prefetch&lt;/code&gt;/&lt;code&gt;concurrentConsumers&lt;/code&gt;/&lt;code&gt;timeout&lt;/code&gt; must be &amp;gt; 0, and &lt;code&gt;autoAck&lt;/code&gt; + &lt;code&gt;transacted&lt;/code&gt; together is flat-out rejected.&lt;/p&gt;




&lt;h2&gt;
  
  
  AMQP properties ↔ headers: a full round-trip
&lt;/h2&gt;

&lt;p&gt;The connector maps a RabbitMQ message's properties to exchange headers &lt;strong&gt;both ways&lt;/strong&gt; — on the consumer and on the producer. When the consumer takes a delivery, it spreads &lt;code&gt;BasicProperties&lt;/code&gt; across the headers; when the producer sends, it reads them back and puts them on the outgoing message. So a &lt;code&gt;consume → produce&lt;/code&gt; hop carries every AMQP property straight through, and you can set or override any of them by just placing a header.&lt;/p&gt;

&lt;p&gt;Naming comes in two tiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delivery metadata&lt;/strong&gt; — there's no standard AMQP name for these, so they get the &lt;code&gt;redbRmq.&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;What's in it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbRmq.Exchange&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the exchange the message arrived on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbRmq.RoutingKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the delivery's routing key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbRmq.Redelivered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;redelivery flag (after a nack/requeue or a consumer crash)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbRmq.DeliveryTag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;delivery tag (used for the ack)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redbRmq.ConsumerTag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;consumer tag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Standard message properties&lt;/strong&gt; — under their well-known names, &lt;strong&gt;no prefix&lt;/strong&gt; (everyone knows them already, and the round-trip carries them as-is):&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;AMQP property&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CorrelationId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;correlation-id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ReplyTo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reply-to — the address to reply to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MessageId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;message-id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Priority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;priority 0–9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Expiration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;message TTL, ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Type&lt;/code&gt; / &lt;code&gt;AppId&lt;/code&gt; / &lt;code&gt;UserId&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;type / app / user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ContentType&lt;/code&gt; / &lt;code&gt;ContentEncoding&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;content-type / encoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Persistent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;persistent / transient&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;redbRmq.Redelivered&lt;/code&gt; earns its keep: use it to handle "this is already the second attempt" separately — send the redelivery down a different route instead of looping forever on requeue.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ReplyTo&lt;/code&gt; header ≠ the &lt;code&gt;replyTo&lt;/code&gt; parameter.&lt;/strong&gt; Similar names, different things. The URI parameter &lt;code&gt;replyTo=true&lt;/code&gt; switches the client into RPC mode (below). The &lt;code&gt;ReplyTo&lt;/code&gt; header is the AMQP &lt;em&gt;reply-to&lt;/em&gt; property itself: set it on a normal send and you're pointing the message's reply address directly, and the producer will carry the header into the property. All of this works because the mapping is two-way — the consumer doesn't just read these; the producer honors them too.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Recipe 1: pub/sub
&lt;/h2&gt;

&lt;p&gt;A fanout exchange fans a copy out to every subscriber; the key is ignored. The producer publishes to the exchange, each consumer declares its own queue and binds:&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;// publisher — publishes to a fanout exchange (a copy to every subscriber)&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;"timer://tick?period=5000"&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;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BuildEvent&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://events-pub?exchange=events&amp;amp;exchangeType=fanout"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// two independent subscribers — each with its own queue&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;"rabbitmq://audit?exchange=events&amp;amp;exchangeType=fanout&amp;amp;declare=true"&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://audit"&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;"rabbitmq://email?exchange=events&amp;amp;exchangeType=fanout&amp;amp;declare=true"&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://email"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want smarter routing? Switch to &lt;code&gt;exchangeType=topic&lt;/code&gt; and a &lt;code&gt;routingKey&lt;/code&gt; with wildcards (&lt;code&gt;order.*&lt;/code&gt;, &lt;code&gt;#.error&lt;/code&gt;). Only the exchange type and the key change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe 2: competing consumers (concurrency)
&lt;/h2&gt;

&lt;p&gt;One parameter — &lt;code&gt;concurrentConsumers&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;"rabbitmq://orders?concurrentConsumers=8&amp;amp;prefetchCount=16"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// up to 8 in flight&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;HandleOrder&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 the &lt;strong&gt;single knob&lt;/strong&gt; for consumer concurrency: it sets both the channel's AMQP dispatch concurrency and the app-level semaphore. The default is &lt;code&gt;1&lt;/code&gt; (strictly serial, order preserved). Set &lt;code&gt;N &amp;gt; 1&lt;/code&gt; and per-queue ordering is no longer guaranteed, your handler has to be thread-safe, and you keep &lt;code&gt;prefetchCount&lt;/code&gt; ≥ N.&lt;/p&gt;

&lt;p&gt;A note for anyone who kicked the tires on an earlier build: before 3.2.2, &lt;code&gt;concurrentConsumers&lt;/code&gt; quietly gave you no parallelism thanks to a trap in &lt;code&gt;RabbitMQ.Client&lt;/code&gt; 7.x (the channel was pinned to serial dispatch). Since 3.2.2 it's the one knob and it actually works — if you set &lt;code&gt;N &amp;gt; 1&lt;/code&gt; and "it somehow didn't speed up," update.&lt;/p&gt;

&lt;p&gt;Acknowledgements are a parameter too. The default is &lt;strong&gt;at-least-once&lt;/strong&gt;: &lt;code&gt;ack&lt;/code&gt; after a clean pass, &lt;code&gt;nack&lt;/code&gt; + requeue on error. Want fire-and-forget? &lt;code&gt;autoAck=true&lt;/code&gt; flips you to &lt;strong&gt;at-most-once&lt;/strong&gt; (the broker settles the delivery on dispatch; an error doesn't bring the message back).&lt;/p&gt;

&lt;p&gt;And if what you need to parallelize is the &lt;em&gt;processing&lt;/em&gt; inside a route rather than the &lt;em&gt;intake&lt;/em&gt; (a serial source but heavy work), there's an orthogonal EIP step, &lt;code&gt;.Threads(N)&lt;/code&gt;. &lt;code&gt;concurrentConsumers&lt;/code&gt; scales reading from the queue; &lt;code&gt;.Threads&lt;/code&gt; scales the work; they compose freely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe 3: broadcasting across cluster nodes (local-cache invalidation)
&lt;/h2&gt;

&lt;p&gt;A pattern straight from production. A service runs as a cluster of several nodes, each with its own local cache. Write to the database on any node, and you want the cache to refresh on &lt;strong&gt;all&lt;/strong&gt; of them. The classic move over RabbitMQ: an audit producer publishes a domain event to a &lt;strong&gt;durable topic exchange&lt;/strong&gt;, and every node hangs its own temporary queue off it and listens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer&lt;/strong&gt; (wherever the DB write happens) — a route per event kind, all into one exchange &lt;code&gt;lt.events&lt;/code&gt;, each with its own routing key. Stamp the headers up front so subscribers can route/filter without cracking open the body. The connection comes from a shared factory:&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;// shared stamp: domain headers for subscribers — routing without parsing the body&lt;/span&gt;
&lt;span class="n"&gt;Action&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="n"&gt;stamp&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;a&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;GetBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuditEvent&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;"CorrelationId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CorrelationId&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;"EntityType"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityType&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;"EventType"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventType&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;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct://audit.entity.created"&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;"audit-entity-created"&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;stamp&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="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;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;GetBody&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuditEvent&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;"rabbitmq://?connectionFactory=RabbitMQ&amp;amp;exchange=lt.events&amp;amp;exchangeType=topic"&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;routingKey=dal.events.created&amp;amp;exchangeDurable=true"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// updated / deleted / batch / error — same exchange, routingKey=dal.events.{updated|deleted|…}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A consumer on every node&lt;/strong&gt; — here's the whole trick. &lt;code&gt;queue=&lt;/code&gt; empty + &lt;code&gt;autoDelete=true&lt;/code&gt;: each node gets its &lt;strong&gt;own&lt;/strong&gt; server-named temporary queue bound to the shared exchange. A node disconnects, its queue is gone. So a copy of &lt;strong&gt;every&lt;/strong&gt; event lands on &lt;strong&gt;every&lt;/strong&gt; node:&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;// created and updated — two consumers into one shared handler&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;"rabbitmq://?connectionFactory=RabbitMQ&amp;amp;exchange=lt.events&amp;amp;exchangeType=topic"&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;routingKey=dal.events.created&amp;amp;queue=&amp;amp;autoDelete=true&amp;amp;expires=300000"&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;concurrentConsumers=2&amp;amp;prefetchCount=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;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cache-consumer-created"&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://cache-update"&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;"rabbitmq://?connectionFactory=RabbitMQ&amp;amp;exchange=lt.events&amp;amp;exchangeType=topic"&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;routingKey=dal.events.updated&amp;amp;queue=&amp;amp;autoDelete=true&amp;amp;expires=300000"&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;concurrentConsumers=2&amp;amp;prefetchCount=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;RouteId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cache-consumer-updated"&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://cache-update"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// one processor for both streams — refreshes this node's local cache&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://cache-update"&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;"cache-update"&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CacheUpdateProcessor&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the key bit — the contrast with Recipe 2. That was &lt;strong&gt;competing consumers&lt;/strong&gt;: one shared durable queue, the work split across consumers (each message handled by exactly one of them). This is the opposite: every node has its &lt;strong&gt;own&lt;/strong&gt; ephemeral queue on the shared exchange → a &lt;strong&gt;broadcast&lt;/strong&gt;, every node sees every event. Same connector, opposite topology, and the whole difference is a single parameter (&lt;code&gt;queue=name&amp;amp;durable=true&lt;/code&gt; versus &lt;code&gt;queue=&amp;amp;autoDelete=true&lt;/code&gt;). A DB write on any node → one event to the exchange → the local cache refreshes on every node, with no polling and no single point of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe 4: RPC (request/reply)
&lt;/h2&gt;

&lt;p&gt;This is where it gets genuinely nice. Classic RabbitMQ RPC means a temporary reply queue, a &lt;code&gt;correlationId&lt;/code&gt;, and waiting for the answer. Here it's one parameter on the client and &lt;strong&gt;nothing&lt;/strong&gt; on the server. Here's how the demo wires it (&lt;code&gt;DemoEndpoints.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;// server — an ordinary consumer; declare creates the queue&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RabbitConsumer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;"rabbitmq://demo-rpc-queue?host=localhost&amp;amp;port=5672&amp;amp;username=admin&amp;amp;password=admin&amp;amp;declare=true"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// client — replyTo=true turns on RPC, timeout=15 — wait no longer than 15s&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RabbitProducer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="s"&gt;"rabbitmq://demo-rpc-queue?host=localhost&amp;amp;port=5672&amp;amp;username=admin&amp;amp;password=admin&amp;amp;replyTo=true&amp;amp;timeout=15"&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;strong&gt;client&lt;/strong&gt; is a plain &lt;code&gt;.To(...)&lt;/code&gt;, except it doesn't fire and forget — it waits for the reply and rolls on carrying it (&lt;code&gt;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="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;"[3-RABBIT] → Sending to RabbitMQ RPC..."&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;RabbitProducer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                      &lt;span class="c1"&gt;// publish the request and WAIT for the reply&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;"[3-RABBIT] ← stamp.rabbit=${header.stamp.rabbit}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// the reply is already on the exchange&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;server&lt;/strong&gt; is just a consumer — there's nothing to configure for RPC. The connector sees &lt;code&gt;reply-to&lt;/code&gt; on the incoming message, makes the route &lt;code&gt;InOut&lt;/code&gt;, and sends whatever's in &lt;code&gt;Out&lt;/code&gt; back to the reply queue with the same &lt;code&gt;correlationId&lt;/code&gt;. The entire "server" 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="n"&gt;RabbitConsumer&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-rabbit-worker"&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;"[RABBIT-W] ▶ Received: ${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;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stamp.rabbit"&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;$"ok:&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;HH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;mm&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;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[RABBIT-W] ◀ Stamped, replying"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// the reply goes out on its own&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a word about reply queues or correlation — the client sent a request and got back the &lt;code&gt;stamp.rabbit&lt;/code&gt; header the server stamped on. Clean request/reply over a broker, with none of the temporary-queue plumbing by hand.&lt;/p&gt;

&lt;p&gt;This is exactly where the contrast with MassTransit shows. It has request/response too — but that's &lt;code&gt;IRequestClient&amp;lt;TRequest&amp;gt;&lt;/code&gt;, typed &lt;code&gt;TRequest&lt;/code&gt;/&lt;code&gt;TResponse&lt;/code&gt; contracts, and a consumer registered on the bus. Powerful, but it's a separate abstraction you have to step into. Here it's &lt;code&gt;replyTo=true&lt;/code&gt; in the URI, and on the server side, nothing at all: an ordinary consumer, and the connector saw &lt;code&gt;reply-to&lt;/code&gt; and replied. Two takes on the same problem: contracts and a bus versus one flag on an endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe 5 (EIP): Filter — let only what matters through
&lt;/h2&gt;

&lt;p&gt;Message Filter is a plain, perpetually useful EIP: let through only the messages that satisfy a condition and quietly drop the rest. In redb.Route that's &lt;code&gt;.Filter(...)&lt;/code&gt; … &lt;code&gt;.EndFilter()&lt;/code&gt; (or the shorter &lt;code&gt;.End()&lt;/code&gt; — same thing): the block between them runs only for messages that pass, and for everything else the route ends right there. And this is where the compiled predicate engine shows up — the same one we got into in the expressions piece.&lt;/p&gt;

&lt;p&gt;The condition can be written three ways. As a &lt;strong&gt;predicate&lt;/strong&gt; over a header (or the body):&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;"rabbitmq://orders?concurrentConsumers=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;Filter&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;"score"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;       &lt;span class="c1"&gt;// score &amp;gt; 50&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://vip-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;EndFilter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The predicate set is everything you'd expect: &lt;code&gt;.isEqualTo&lt;/code&gt; / &lt;code&gt;.isNotEqualTo&lt;/code&gt; / &lt;code&gt;.isGreaterThan&lt;/code&gt; / &lt;code&gt;.isLessThan&lt;/code&gt; / &lt;code&gt;.isGreaterThanOrEqualTo&lt;/code&gt; / &lt;code&gt;.isLessThanOrEqualTo&lt;/code&gt; / &lt;code&gt;.isBetween(a, b)&lt;/code&gt; / &lt;code&gt;.contains(...)&lt;/code&gt;. On the left, &lt;code&gt;Header("...")&lt;/code&gt; or &lt;code&gt;Body()&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;Filter&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;"email"&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;"@example"&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://internal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndFilter&lt;/span&gt;&lt;span class="p"&gt;();&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="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"grade"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isEqualTo&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;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"direct://grade-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;EndFilter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a &lt;strong&gt;string expression&lt;/strong&gt; — the &lt;code&gt;${...}&lt;/code&gt; resolves and the result is coerced to a boolean. For a truthy check, just the value; for a &lt;strong&gt;comparison&lt;/strong&gt;, wrap it in &lt;code&gt;logical(...)&lt;/code&gt; (a bare &lt;code&gt;${header.score &amp;gt; 50}&lt;/code&gt; inside a template comes back as an empty string — you can't write it that way):&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;Filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${header.status}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;// 'active' → truthy → passes&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;"active only"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndFilter&lt;/span&gt;&lt;span class="p"&gt;();&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="s"&gt;"${logical(header.score &amp;gt; 50)}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// comparison → "True"/"False"&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://vip"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndFilter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or as a &lt;strong&gt;lambda&lt;/strong&gt;, when you want arbitrary C#:&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;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;=&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="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="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"urgent"&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://urgent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndFilter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handy thing is filtering on the very headers the consumer already stamped — say &lt;code&gt;.Filter(Header("redbRmq.RoutingKey").contains("order"))&lt;/code&gt;, so only messages with the routing key you care about make it into the block.&lt;/p&gt;

&lt;h3&gt;
  
  
  How an expression becomes a &lt;code&gt;bool&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Under the hood, for each message Filter evaluates the expression to &lt;code&gt;object?&lt;/code&gt; and coerces it to &lt;code&gt;bool&lt;/code&gt; by the &lt;code&gt;ConvertToBoolean&lt;/code&gt; rules:&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;bool&lt;/span&gt;   &lt;span class="err"&gt;→&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;is&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="err"&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="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;otherwise&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;non&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="kt"&gt;string&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;empty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;null&lt;/span&gt;   &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;anything&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;number&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;non&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="err"&gt;→&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which explains how the three forms behave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;predicate&lt;/strong&gt; like &lt;code&gt;Header("amount").isGreaterThan(1000)&lt;/code&gt; is an &lt;code&gt;IPredicate&lt;/code&gt; with a ready &lt;code&gt;Func&amp;lt;IExchange,bool&amp;gt;&lt;/code&gt;. No coercion needed: a real boolean straight from the comparison. It compiles to a delegate and gets cached — not reflection per message, but a fast call.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;template string&lt;/strong&gt; like &lt;code&gt;"${header.status}"&lt;/code&gt; — the &lt;code&gt;${...}&lt;/code&gt; interpolates to a value, then the truthy rules apply. &lt;code&gt;status="active"&lt;/code&gt; → non-empty string → &lt;code&gt;true&lt;/code&gt; (that's why the demo says "active is truthy"); and &lt;code&gt;"${header.enabled}"&lt;/code&gt; with &lt;code&gt;"false"&lt;/code&gt; → &lt;code&gt;bool.TryParse&lt;/code&gt; → &lt;code&gt;false&lt;/code&gt;. A comparison written straight into the template (&lt;code&gt;"${header.score &amp;gt; 50}"&lt;/code&gt;) doesn't count — it comes back empty (i.e. &lt;code&gt;false&lt;/code&gt;); for a comparison you need &lt;code&gt;logical(...)&lt;/code&gt;, as above.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;lambda&lt;/strong&gt; &lt;code&gt;ex =&amp;gt; …&lt;/code&gt; — whatever you return is the predicate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "a boolean expression" is either an honest &lt;code&gt;bool&lt;/code&gt; from a predicate/comparison, or a truthy coercion of an interpolated value. An empty string, &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;"false"&lt;/code&gt; cut it off; a non-empty string, a number, &lt;code&gt;true&lt;/code&gt; let it through.&lt;/p&gt;

&lt;h3&gt;
  
  
  The DSL is open: your own step is an extension method
&lt;/h3&gt;

&lt;p&gt;Worth noticing one thing here. The string and predicate forms &lt;code&gt;Filter(string)&lt;/code&gt; / &lt;code&gt;Filter(IPredicate)&lt;/code&gt; &lt;strong&gt;aren't on the &lt;code&gt;IRouteDefinition&lt;/code&gt; interface&lt;/strong&gt; — that has only the canonical &lt;code&gt;Filter(Func&amp;lt;IExchange,bool&amp;gt;)&lt;/code&gt; and &lt;code&gt;Filter(IExpression)&lt;/code&gt;. The string form is an &lt;strong&gt;extension method&lt;/strong&gt;, a thin façade over the canonical one:&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;// the whole string overload (redb.Route/…/RouteDefinitionCamelDslExtensions.cs):&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;FilterDefinition&lt;/span&gt; &lt;span class="nf"&gt;Filter&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;IRouteDefinition&lt;/span&gt; &lt;span class="n"&gt;self&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;simpleTemplate&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;self&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StringExpression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;simpleTemplate&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// wrap the string in a template expression&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which carries an important corollary: &lt;strong&gt;the DSL is extensible, and a step of your own is added the exact same way&lt;/strong&gt; — an extension method that delegates to the canonical one. Want a custom "verb" in your routes? 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="c1"&gt;// your own step: pass only "urgent" in the body&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;FilterDefinition&lt;/span&gt; &lt;span class="nf"&gt;OnlyUrgent&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;IRouteDefinition&lt;/span&gt; &lt;span class="n"&gt;self&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;self&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;=&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="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="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"urgent"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// and in a route it reads like a native step:&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;"rabbitmq://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;OnlyUrgent&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://urgent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndFilter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No magic, no engine edits — just an extension method over &lt;code&gt;IRouteDefinition&lt;/code&gt;. That's how the framework itself does the Camel-compatible forms, the string templates, and the predicate overloads. So if a step is missing for you, you write it yourself — no forking the connector.&lt;/p&gt;

&lt;p&gt;We've only brushed the edge of expressions here — &lt;code&gt;Header(...)&lt;/code&gt;, &lt;code&gt;Body()&lt;/code&gt;, &lt;code&gt;${...}&lt;/code&gt;, &lt;code&gt;logical(...)&lt;/code&gt;. Expressions in redb.Route are really a small compiled language of their own: access to the body and headers, exchange properties, indexers (&lt;code&gt;items[0]&lt;/code&gt;), string functions, JSONPath and XPath. All of it gets tokenized, parsed into an AST, and compiled to a delegate (&lt;code&gt;System.Linq.Expressions&lt;/code&gt;, not reflection), with a cache. It's a big topic — the engine gets its own post; it's earned it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipe 6 (EIP): Dead Letter Channel
&lt;/h2&gt;

&lt;p&gt;Of the Hohpe &amp;amp; Woolf patterns, &lt;strong&gt;Dead Letter Channel&lt;/strong&gt; fits RabbitMQ perfectly — because on the broker it's a built-in feature (&lt;code&gt;x-dead-letter-exchange&lt;/code&gt;), not something emulated in code. The idea: a message that didn't process (or expired on TTL, or didn't fit the queue's limit) isn't lost and doesn't spin forever on requeue — it heads to the morgue, a separate dead-letter exchange you can pick apart at your leisure.&lt;/p&gt;

&lt;p&gt;It's set right in the working queue's URI:&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;// working queue: on nack-without-requeue / expiry / overflow the message goes to the DLX&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;"rabbitmq://orders?declare=true"&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;deadLetterExchange=orders.dlx"&lt;/span&gt;          &lt;span class="c1"&gt;// where&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;deadLetterRoutingKey=orders.failed"&lt;/span&gt;     &lt;span class="c1"&gt;// with which key&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;messageTtl=30000"&lt;/span&gt;                       &lt;span class="c1"&gt;// not processed in 30s → DLX&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;overflow=reject-publish-dlx&amp;amp;maxLength=100000"&lt;/span&gt;   &lt;span class="c1"&gt;// and overflow goes there too&lt;/span&gt;
        &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&amp;amp;concurrentConsumers=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;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HandleOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// the morgue: a separate route listens on the DLX — logs and stores into redb for triage&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;"rabbitmq://orders.dead?declare=true&amp;amp;exchange=orders.dlx&amp;amp;exchangeType=direct&amp;amp;routingKey=orders.failed"&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;"[DLQ] dead-lettered: ${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;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="p"&gt;=&amp;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;SaveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;BuildDeadLetter&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hand-rolled retry loop, no "sleep and try again" — reliability is handed to the broker, and what's left on your side is a clean triage route that writes the problem messages into a database. And the queue doesn't balloon under a surge: the excess leaves for the DLX right away instead of piling up in the broker's memory.&lt;/p&gt;




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

&lt;p&gt;RabbitMQ in redb.Route is routes where the whole broker is one URI: &lt;code&gt;From("rabbitmq://…")&lt;/code&gt; / &lt;code&gt;.To("rabbitmq://…")&lt;/code&gt;, with all the channel juggling, QoS, binds, reply queues and dead-lettering tucked behind the string's parameters. Every exchange type, durable/quorum queues, priorities, TTL, DLX, auto-recovery for a cluster, TLS/mTLS — the set is complete, and what you end up with is a short, legible string. Competing consumers turn on with one parameter, RPC with one flag on the client and zero config on the server, Filter trims the noise with compiled predicates, and Dead Letter Channel is a couple of parameters over the native DLX.&lt;/p&gt;

&lt;p&gt;The full demo is in &lt;code&gt;redb.Route/demos/redb.Route.Demo&lt;/code&gt; — it comes up against a local RabbitMQ (&lt;code&gt;admin/admin&lt;/code&gt;) and runs as a self-test. If something bites in your scenario, say so in the comments.&lt;/p&gt;

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

&lt;p&gt;If this was useful — a ⭐ on GitHub helps others find it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>c</category>
      <category>rabbitmq</category>
      <category>integration</category>
    </item>
    <item>
      <title>redb 3.3.0: an enterprise .NET stack you actually own — typed store, a homegrown Apache Camel, and a runtime with a dashboard (all free)</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Thu, 09 Jul 2026 16:45:47 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redb-330-an-enterprise-net-stack-you-actually-own-typed-store-a-homegrown-apache-camel-and-3gd1</link>
      <guid>https://dev.to/rinat_kozin/redb-330-an-enterprise-net-stack-you-actually-own-typed-store-a-homegrown-apache-camel-and-3gd1</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%2Fol6x5xlkotf698k65il7.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%2Fol6x5xlkotf698k65il7.webp" alt="redb ecosystem" width="800" height="1200"&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;Say "enterprise .NET stack" out loud and most people picture a menagerie: a database from one vendor, a bus from another, an ORM with migrations, a separate orchestrator, something for observability — and a layer of glue between all of it that you write yourself and then debug yourself at 2 a.m.&lt;/p&gt;

&lt;p&gt;We took the other road. For the past six months we've been building this as &lt;strong&gt;one coherent ecosystem&lt;/strong&gt;: a typed store, &lt;strong&gt;redb&lt;/strong&gt;, on top of Postgres/MSSQL/SQLite; an integration engine, &lt;strong&gt;redb.Route&lt;/strong&gt; (our take on Apache Camel for .NET); and a runtime, &lt;strong&gt;redb.Tsak&lt;/strong&gt;, with a dashboard, hot-reload, and clustering. Three layers, one codebase, one style.&lt;/p&gt;

&lt;p&gt;Today we shipped &lt;strong&gt;3.3.0&lt;/strong&gt; — a synchronized bump across the whole ecosystem. This isn't a "we added a couple of features" release. It's the one where we &lt;strong&gt;fixed the things that were quietly broken under load&lt;/strong&gt; (that's the headline, honestly), added two new transports, closed the loop on RAG for LLMs, and made concurrency work on any source. Code below, no fluff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And the part everyone asks about first: as of 3.3.0, every Pro package is free. No licenses, no keys, no sign-up — you install the package and use it.&lt;/strong&gt; Change tracking, bulk ops, the advanced cache, analytics, the Tsak cluster with its coordinator and failover — all in the box, same in dev and in prod. &lt;code&gt;dotnet add package redb.Postgres.Pro&lt;/code&gt; and it just works. No paywall, no license server, nothing to activate.&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;/blockquote&gt;




&lt;h2&gt;
  
  
  The three layers, in one breath
&lt;/h2&gt;

&lt;p&gt;So the rest of this makes sense, here's the cast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;redb&lt;/strong&gt; — a typed store for .NET. You write a plain POCO, tag it with an attribute, and work with it through full LINQ, server-side, with no migrations and no &lt;code&gt;Include&lt;/code&gt;. Providers: Postgres, MSSQL, SQLite. Free and Pro editions (change tracking, bulk, cache, analytics).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;redb.Route&lt;/strong&gt; — an integration engine in the Camel spirit: a route DSL (&lt;code&gt;From(...).…​.To(...)&lt;/code&gt;), 30+ connectors (Kafka, RabbitMQ, HTTP, gRPC, S3, LLM, and so on), enterprise integration patterns, transactions, telemetry. The glue — except it's a real engine with tests, not something you hand-rolled over a weekend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;redb.Tsak&lt;/strong&gt; — a runtime that takes redb.Route routes and turns them into a production service: a dashboard, hot-reloadable modules (&lt;code&gt;.tpkg&lt;/code&gt;), live context management, metrics, a cluster with a coordinator and failover. Ships as NuGet packages, Docker images, and standalone archives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the code samples are in English (they always were); everything's in the repo, so you can reproduce it.&lt;/p&gt;

&lt;p&gt;To make the bottom layer concrete rather than a claim — here's what redb looks like in practice. A class, an attribute, and then full LINQ that runs server-side, no migrations, no &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 csharp"&gt;&lt;code&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;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;span class="c1"&gt;// write&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="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;Props&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;Tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"work"&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="s"&gt;"hello"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// read — server-side query, the param comes straight from your code, no hand-written JOINs&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;notes&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;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="s"&gt;"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;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema comes from &lt;code&gt;SyncSchemeAsync&amp;lt;Note&amp;gt;()&lt;/code&gt; — generated off the class itself, no migration files. This same layer stores both the knowledge-base chunks from the RAG example later on and your own domain objects, exactly alike.&lt;/p&gt;




&lt;h1&gt;
  
  
  redb.Route: what's new
&lt;/h1&gt;

&lt;p&gt;Starting with the engine, because that's where most of the meat is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two new transports: Amazon SQS/SNS and Telegram
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;redb.Route.Sqs&lt;/code&gt; — an AWS queue and topic in one package
&lt;/h3&gt;

&lt;p&gt;One package, two schemes: &lt;code&gt;sqs://&lt;/code&gt; (queue: consumer + producer) and &lt;code&gt;sns://&lt;/code&gt; (topic: publisher + SNS→SQS fan-out). Under the hood it's the native AWS SDK for .NET v4 — not a reinvented client.&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;redb.Route.Sqs.Fluent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Consumer: 5 competing workers, long-poll, at-least-once (delete on success — ack is automatic)&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;Sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Queue&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;WaitTimeSeconds&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="nf"&gt;ConcurrentConsumers&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="nf"&gt;Process&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;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&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="c1"&gt;// Publish an event to an SNS topic (fans out to subscribed queues)&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;"timer://tick?period=5000"&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;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BuildEvent&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;Sns&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;"order-events"&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 does long-polling, &lt;code&gt;ConcurrentConsumers(N)&lt;/code&gt; (real competing loops), a visibility timeout you can extend while a handler runs, transactional ack via &lt;code&gt;.Transacted()&lt;/code&gt;, and FIFO. The producer sends single and batched messages with FIFO group/dedup ids; the SNS publisher handles subject, message structure, FIFO, and an SNS→SQS auto-subscription. Point &lt;code&gt;serviceUrl=&lt;/code&gt; at LocalStack or ElasticMQ and it all works locally; the full AWS credential chain is honored, and W3C trace context rides along across the hop. Details in &lt;code&gt;redb.Route.Sqs/README.md&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;redb.Route.Telegram&lt;/code&gt; — a bot as a route
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;telegram://&lt;/code&gt; scheme, built on &lt;code&gt;Telegram.Bot&lt;/code&gt;. The consumer is a long-poller (&lt;code&gt;receive&lt;/code&gt;, a single &lt;code&gt;getUpdates&lt;/code&gt; stream per token); the producer covers &lt;code&gt;send&lt;/code&gt; / &lt;code&gt;document&lt;/code&gt; / &lt;code&gt;photo&lt;/code&gt; / &lt;code&gt;edit&lt;/code&gt; / &lt;code&gt;delete&lt;/code&gt; / &lt;code&gt;answer&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;// Echo bot: take an update, reply into the same chat&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;Tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Receive&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;span class="nf"&gt;Process&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;=&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;Out&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;$"You said: &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="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;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&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="nf"&gt;ChatId&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="n"&gt;TelegramHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatId&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out of the box: it honors the 429 &lt;code&gt;retry_after&lt;/code&gt; contract (waits and retries instead of falling over), validates &lt;code&gt;parseMode&lt;/code&gt; (throws on a typo rather than quietly shipping raw markup), supports inline/reply keyboards (&lt;code&gt;WithInlineKeyboard&lt;/code&gt; / &lt;code&gt;WithReplyKeyboard&lt;/code&gt;), a webhook-unpack pipeline (&lt;code&gt;UnpackTelegramUpdate&lt;/code&gt;), and a fluent DSL (&lt;code&gt;Tg.Receive/Send/Document/...&lt;/code&gt;). Delivery is &lt;strong&gt;at-most-once&lt;/strong&gt; — Telegram advances the update offset the moment it hands you the update, and that's documented, not a surprise. Parallelism comes from &lt;code&gt;.Threads(N)&lt;/code&gt; (more on that below), with consumer telemetry and producer spans.&lt;/p&gt;

&lt;p&gt;Two transports people actually run: SQS for anything living on AWS, Telegram for ChatOps and bots. Both are tested against emulators.&lt;/p&gt;

&lt;h2&gt;
  
  
  RAG, end to end: &lt;code&gt;knowledge://&lt;/code&gt;, &lt;code&gt;embed://&lt;/code&gt; and &lt;code&gt;.Knowledge()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;redb.Route.Llm&lt;/code&gt; already had a universal OpenAI-compatible provider and a native Anthropic one. In 3.3.0 we &lt;strong&gt;closed the RAG loop&lt;/strong&gt; — from ingesting documents to answering grounded on them, all inside routes.&lt;/p&gt;

&lt;p&gt;What landed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;knowledge://&lt;/code&gt; (an ingest scheme)&lt;/strong&gt; — a producer that takes the message body as a document, chops it into chunks (deterministic character windows with overlap), embeds each one if an &lt;code&gt;IEmbeddingProvider&lt;/code&gt; is registered, and upserts into the &lt;code&gt;IKnowledgeStore&lt;/code&gt;. Loading documents becomes a route:
&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="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file://docs?include=*.md"&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;"knowledge://handbook"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chunk ids are &lt;code&gt;{docId}#{index}&lt;/code&gt;, so re-ingesting the same document replaces its chunks in place instead of piling up duplicates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;embed://&lt;/code&gt; (embeddings as a step)&lt;/strong&gt; — the mirror image of &lt;code&gt;llm://&lt;/code&gt;: &lt;code&gt;To("embed://openai")&lt;/code&gt; turns the message body (a text, or a batch of texts with order preserved) into a vector on &lt;code&gt;Out.Body&lt;/code&gt;. The URI host names a connection factory, so different routes pick different embedding models by name.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;.Knowledge(collection, k)&lt;/code&gt; (a retrieval DSL step)&lt;/strong&gt; — pulls the top-K chunks for the current message and &lt;strong&gt;injects them into the system prompt&lt;/strong&gt;, so the next &lt;code&gt;.To("llm://…")&lt;/code&gt; answers off them:&lt;br&gt;
&lt;/p&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="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://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;Knowledge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"handbook"&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;5&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"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Semantic when an &lt;code&gt;IEmbeddingProvider&lt;/code&gt; is around (embed the query → cosine &lt;code&gt;SearchAsync&lt;/code&gt;), keyword otherwise (&lt;code&gt;SearchTextAsync&lt;/code&gt;, a server-side &lt;code&gt;LIKE&lt;/code&gt; over an indexed column). No store wired or nothing retrieved? The step is a no-op and never breaks the pipeline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;knowledge_search&lt;/code&gt; (a ready-made tool)&lt;/strong&gt; — an &lt;code&gt;.AsLlmTool&lt;/code&gt; route over the search, so the agent can hit the knowledge base itself: input &lt;code&gt;{query, top_k?, collection?}&lt;/code&gt; → &lt;code&gt;{results:[…]}&lt;/code&gt;. You can &lt;strong&gt;pin&lt;/strong&gt; the collection, which makes the model's argument moot — that's how you fence an agent into a single tenant or document set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full loop: &lt;code&gt;knowledge://&lt;/code&gt; ingest → embeddings → keyword/semantic search → the &lt;code&gt;knowledge_search&lt;/code&gt; tool or &lt;code&gt;.Knowledge()&lt;/code&gt; injection. And no external vector store unless you want one — the chunks live in redb, and search is just a server-side query.&lt;/p&gt;

&lt;p&gt;We also killed an annoying one along the way: several of the LLM connector's JSON serializers used the default encoder, which escapes every non-ASCII character as &lt;code&gt;\uXXXX&lt;/code&gt;. On tool results that roughly 6×'d the tokens the model saw for Cyrillic/CJK content (and could leak literal &lt;code&gt;\u…&lt;/code&gt; into the text); in the knowledge store it buried chunk text so the new keyword &lt;code&gt;LIKE&lt;/code&gt; couldn't match a raw non-ASCII query. Switched to &lt;code&gt;UnsafeRelaxedJsonEscaping&lt;/code&gt; everywhere it mattered. If your data isn't pure ASCII, you'll feel this in both the token bill and the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;.Threads(N)&lt;/code&gt; — concurrency on any source
&lt;/h2&gt;

&lt;p&gt;There's a Camel-style processing-concurrency stage now: &lt;code&gt;From(...).Threads(N)…EndThreads()&lt;/code&gt; caps a section of the route at N. The point: even a strictly serial source (a poll consumer, MQTT, a single request thread) can now chew through up to N messages at once — no named &lt;code&gt;seda://&lt;/code&gt; endpoint required.&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;"mqtt://sensors"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// serial by nature&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Threads&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;// up to 8 in flight&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;ex&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HeavyWork&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndThreads&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nice part is that it's &lt;strong&gt;adaptive to the exchange pattern&lt;/strong&gt;. For InOnly (fire-and-forget) it's a hand-off: clone plus a worker pool, a transaction boundary just like &lt;code&gt;.To("seda://")&lt;/code&gt;. For InOut it runs the body &lt;strong&gt;inline on the same exchange under a &lt;code&gt;SemaphoreSlim&lt;/code&gt; gate&lt;/strong&gt;, so the reply — on &lt;code&gt;Out&lt;/code&gt; or &lt;code&gt;In&lt;/code&gt; — survives intact, request/reply (RPC) works through it, and the ambient transaction flows into the inline body. You get &lt;code&gt;.MaxQueueSize(n)&lt;/code&gt; and &lt;code&gt;.EnqueueTimeout(TimeSpan)&lt;/code&gt;. Ordering isn't preserved at N &amp;gt; 1 — that's the price of parallelism, and we say so plainly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big one: &lt;code&gt;ConcurrentConsumers(N)&lt;/code&gt; finally does something
&lt;/h2&gt;

&lt;p&gt;Now the important — and frankly embarrassing — part. Brace yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ConcurrentConsumers(N)&lt;/code&gt; was silently giving you no parallelism on the brokers.&lt;/strong&gt; The option existed, you passed a number, it even sized an internal &lt;code&gt;SemaphoreSlim&lt;/code&gt; — and no actual concurrency came out. The cause differed per connector; the symptom was always the same: the consumer processed one message at a time, &lt;code&gt;unacked&lt;/code&gt; climbed to the prefetch limit, and the route never kept up.&lt;/p&gt;

&lt;p&gt;What it was, and what it is now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RabbitMQ.&lt;/strong&gt; The channel was built with the three-argument &lt;code&gt;CreateChannelOptions(...)&lt;/code&gt; ctor, leaving the fourth parameter (&lt;code&gt;consumerDispatchConcurrency&lt;/code&gt;) at its compile-time default — and in &lt;code&gt;RabbitMQ.Client&lt;/code&gt; 7.2.1 that default is &lt;code&gt;1&lt;/code&gt;, not &lt;code&gt;null&lt;/code&gt;. A non-null per-channel value &lt;em&gt;overrides&lt;/em&gt; the connection-level setting, so every channel was pinned to serial dispatch and the value from the URI/factory was quietly dropped. Now &lt;code&gt;ConcurrentConsumers&lt;/code&gt; is the single knob for consumer parallelism: the channel always opens with an explicit dispatch concurrency. (&lt;code&gt;redb.Route.RabbitMQ&lt;/code&gt; 3.2.2.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka.&lt;/strong&gt; Plus a new &lt;code&gt;EnableAutoCommit&lt;/code&gt; option (default &lt;code&gt;true&lt;/code&gt;) to bring offset-settle in line with RabbitMQ's post-process ack — and a fix for the transacted producer that was throwing &lt;code&gt;Local: Erroneous state&lt;/code&gt; on the deferred send. (&lt;code&gt;redb.Route.Kafka&lt;/code&gt; 3.2.1.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AMQP 1.0 and IBM MQ.&lt;/strong&gt; Same class of bug: a serial receive loop &lt;code&gt;await&lt;/code&gt;ed &lt;code&gt;Process&lt;/code&gt; inline before pulling the next message, so the &lt;code&gt;SemaphoreSlim&lt;/code&gt; was a dead gate — grabbed and released by the one loop. Now &lt;code&gt;ConcurrentConsumers(N)&lt;/code&gt; means &lt;strong&gt;N genuine competing consumers&lt;/strong&gt;, each with its own session/connection and loop (AMQPNetLite sessions and the IBM MQ managed client aren't thread-safe, so the concurrency comes from N independent workers, never from sharing a link). For IBM MQ it's also a correctness fix: the syncpoint is connection-scoped and &lt;code&gt;Backout()&lt;/code&gt; rolls back the whole connection, so each worker &lt;em&gt;must&lt;/em&gt; own its own. IBM MQ topics get clamped to a single subscriber with a warning — N non-durable subscriptions would each get a &lt;em&gt;copy&lt;/em&gt; of every message rather than share the load. (&lt;code&gt;redb.Route.Amqp&lt;/code&gt; / &lt;code&gt;redb.Route.IbmMq&lt;/code&gt; 3.2.1.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three hotfixes (RabbitMQ 3.2.2, Kafka 3.2.1, Amqp/IbmMq 3.2.1) are now folded into the single 3.3.0 bump. Each ships a live regression test against a real broker: publish a batch, set &lt;code&gt;ConcurrentConsumers(5)&lt;/code&gt;, assert observed max concurrency is above 1, and assert &lt;code&gt;ConcurrentConsumers(1)&lt;/code&gt; stays strictly serial.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads-up when you upgrade.&lt;/strong&gt; If a route of yours set &lt;code&gt;ConcurrentConsumers(N &amp;gt; 1)&lt;/code&gt; and "worked," it was working serially. After 3.3.0 it actually parallelizes: per-queue ordering is no longer preserved on that route, and its handlers need to be thread-safe. Routes left at the default &lt;code&gt;1&lt;/code&gt; are untouched — strictly serial, exactly as before.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bundled in are a new RabbitMQ &lt;code&gt;AutoAck&lt;/code&gt; (broker-side auto-ack / at-most-once, the Kafka &lt;code&gt;EnableAutoCommit&lt;/code&gt; analogue) and a fix for a double &lt;code&gt;BasicAck&lt;/code&gt; when a route-level &lt;code&gt;.Transacted()&lt;/code&gt; wraps a non-transacted consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-exchange connections: the end of the captive singleton
&lt;/h2&gt;

&lt;p&gt;The second half of that same problem lives in the engine core. &lt;code&gt;IRedbService&lt;/code&gt; wraps one non-thread-safe DB connection (think EF &lt;code&gt;DbContext&lt;/code&gt;). The DSL steps &lt;code&gt;ProcessWithRedb(...)&lt;/code&gt;, &lt;code&gt;SetBodyFromRedb(...)&lt;/code&gt;, &lt;code&gt;SetHeaderFromRedb(...)&lt;/code&gt;, and &lt;code&gt;BeginRedbTransaction()&lt;/code&gt; used to fall back to a &lt;strong&gt;single&lt;/strong&gt; &lt;code&gt;IRedbService&lt;/code&gt; captured from the root DI container. Under real concurrency (a Splitter with parallel processing, SEDA, that very &lt;code&gt;ConcurrentConsumers(N)&lt;/code&gt;, concurrent HTTP requests) two exchanges drove that one connection at once — and the driver threw &lt;em&gt;"A command is already in progress"&lt;/em&gt; / &lt;em&gt;"connection is busy"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now each exchange gets its &lt;strong&gt;own DI scope → its own scoped &lt;code&gt;IRedbService&lt;/code&gt; → its own pooled connection&lt;/strong&gt;, cached on the exchange and disposed with it. The unscoped singleton is only used when there's no exchange at all (a single-threaded seed). There's also &lt;code&gt;controller.Redb()&lt;/code&gt; for controllers: it resolves the per-request instance instead of the shared captive singleton.&lt;/p&gt;

&lt;p&gt;While we were in there we closed the matching DI-scope/connection leaks on the exchange lifecycle — &lt;code&gt;ThreadsProcessor&lt;/code&gt;, &lt;code&gt;SedaProducer&lt;/code&gt;/&lt;code&gt;VmProducer&lt;/code&gt; (leaked the clone on a failed hand-off), &lt;code&gt;WireTapProcessor&lt;/code&gt; (leaked the tap clone when a user &lt;code&gt;onPrepare&lt;/code&gt;/&lt;code&gt;newBody&lt;/code&gt; callback threw), and the scheduled &lt;code&gt;llm://&lt;/code&gt; and &lt;code&gt;exec://&lt;/code&gt; consumers (never disposed their per-tick exchange). Everything now disposes in &lt;code&gt;finally&lt;/code&gt;. And we made lazy producer start-up thread-safe: on a cold start under concurrency you could catch a &lt;code&gt;NullReferenceException&lt;/code&gt; (a Redis producer whose &lt;code&gt;_db&lt;/code&gt; was still null, say); both paths are single-flight now, and a producer only reads as started once &lt;code&gt;ConnectAsync()&lt;/code&gt; is fully done.&lt;/p&gt;

&lt;p&gt;Short version: 3.3.0 is the release after which you can actually push redb.Route hard and stop chasing flaky connection errors. That's the line between "works in the demo" and "works in prod."&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together: a RAG bot on Telegram, in a handful of routes
&lt;/h2&gt;

&lt;p&gt;So the new pieces don't read like a shopping list, here's how they snap together. Goal: a Telegram bot that answers from your knowledge base (a folder of markdown) instead of making things up. Three routes — ingest, receive-and-answer — and it runs on a concurrent source.&lt;/p&gt;

&lt;p&gt;First, ingest. One route: read the files, chunk them, embed them, upsert. The store is redb; no separate vector engine.&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. Ingest: a markdown folder → chunks with embeddings in redb&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;"file://docs?include=*.md"&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;"knowledge://handbook?chunkChars=1000&amp;amp;overlap=100&amp;amp;embed=true"&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 bot. Take an update from Telegram, pull the top-5 relevant chunks for the question text, inject them into the system prompt, ask the model. &lt;code&gt;.Knowledge()&lt;/code&gt; decides on its own — semantic if an &lt;code&gt;IEmbeddingProvider&lt;/code&gt; is wired, keyword otherwise.&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;// 2. Question from Telegram → RAG → answer back into the same chat&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;Tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Receive&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;span class="nf"&gt;Threads&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="c1"&gt;// up to 4 conversations in parallel&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;"handbook"&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;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// top-5 chunks into the system 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="s"&gt;"llm://claude"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;// answer grounded on them&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndThreads&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;Tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&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="nf"&gt;ChatId&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="n"&gt;TelegramHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatId&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 the whole thing. Notice what isn't there: no hand-rolling of vectors, no separate embeddings service, no bespoke glue between Telegram and the LLM, no connection pool to babysit. &lt;code&gt;.Threads(4)&lt;/code&gt; gives you parallel conversations on a source that hands out updates one at a time, and each worker gets its own connection when it touches redb.&lt;/p&gt;

&lt;p&gt;Want the model to decide &lt;em&gt;when&lt;/em&gt; to hit the knowledge base rather than doing it on every turn? Swap &lt;code&gt;.Knowledge()&lt;/code&gt; for the &lt;code&gt;knowledge_search&lt;/code&gt; tool and pin the collection so it can't wander outside &lt;code&gt;handbook&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;// register the tool once; the collection is pinned, so the agent is fenced into 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;AddRoutes&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;KnowledgeSearchTool&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;KnowledgeSearchOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Collection&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"handbook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EmbeddingProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;      &lt;span class="c1"&gt;// set → semantic; unset → keyword&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;Tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Receive&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;span class="nf"&gt;Threads&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;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llm://claude?tools=knowledge_search"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EndThreads&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;Tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&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="nf"&gt;ChatId&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="n"&gt;TelegramHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatId&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same set of primitives — &lt;code&gt;knowledge://&lt;/code&gt;, &lt;code&gt;embed://&lt;/code&gt;, &lt;code&gt;.Knowledge()&lt;/code&gt;, &lt;code&gt;knowledge_search&lt;/code&gt;, &lt;code&gt;telegram://&lt;/code&gt;, &lt;code&gt;.Threads()&lt;/code&gt; — all of it new or matured in 3.3.0. Each piece is small on its own; together they're a production RAG bot with zero external dependencies beyond the model.&lt;/p&gt;




&lt;h1&gt;
  
  
  redb (the DB core): what made it in
&lt;/h1&gt;

&lt;p&gt;Three fixes and one guard rail in the core, all in the same concurrency-and-connections theme.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fail-fast guard on the provider connection&lt;/strong&gt; (&lt;code&gt;RedBase.Postgres&lt;/code&gt;, &lt;code&gt;RedBase.MSSql&lt;/code&gt;, &lt;code&gt;RedBase.SQLite&lt;/code&gt; + &lt;code&gt;.Pro&lt;/code&gt;). If the same &lt;code&gt;IRedbService&lt;/code&gt; is entered from two threads at once, the provider now throws a clear &lt;code&gt;InvalidOperationException&lt;/code&gt; that names the cause — instead of the murky driver error (&lt;em&gt;"A command is already in progress"&lt;/em&gt;, &lt;em&gt;"connection is busy"&lt;/em&gt;, &lt;em&gt;"another read operation is already in progress"&lt;/em&gt;). It's a cheap &lt;code&gt;Interlocked&lt;/code&gt; check with zero cost on the normal single-threaded path. It backstops the per-exchange connections in redb.Route: if someone &lt;em&gt;does&lt;/em&gt; share an instance, they hear about it immediately and in plain English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The query parser choked on &lt;code&gt;array.Contains(x)&lt;/code&gt; in &lt;code&gt;WhereRedb&lt;/code&gt; on .NET 9 / C# 13.&lt;/strong&gt; A &lt;code&gt;string[]&lt;/code&gt; (or any array) &lt;code&gt;.Contains(x)&lt;/code&gt; inside a predicate now binds to the &lt;code&gt;ReadOnlySpan&lt;/code&gt; overload (&lt;code&gt;MemoryExtensions.Contains&lt;/code&gt;) instead of &lt;code&gt;Enumerable.Contains&lt;/code&gt;, which the filter parser was rejecting with &lt;code&gt;NotSupportedException&lt;/code&gt;. The parser recognizes &lt;code&gt;MemoryExtensions.Contains&lt;/code&gt;, unwraps the array→span conversion, and translates it to the same &lt;code&gt;IN&lt;/code&gt; clause. Small, but it bit you out of nowhere on the new compiler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ComputeHash()&lt;/code&gt; NRE on an object with &lt;code&gt;Props == null&lt;/code&gt;.&lt;/strong&gt; The generic &lt;code&gt;ComputeFor&amp;lt;TProps&amp;gt;&lt;/code&gt; path dereferenced the object before the null check. Added the guard — the path now returns &lt;code&gt;null&lt;/code&gt; (→ &lt;code&gt;Guid.Empty&lt;/code&gt;) consistently instead of throwing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A connection-pool leak on transaction/connection dispose&lt;/strong&gt; (&lt;code&gt;RedBase.Postgres&lt;/code&gt;, &lt;code&gt;RedBase.MSSql&lt;/code&gt;, &lt;code&gt;RedBase.SQLite&lt;/code&gt; + &lt;code&gt;.Pro&lt;/code&gt;). A throw from the driver's transaction &lt;code&gt;DisposeAsync()&lt;/code&gt; (possible mid error-storm on an already-broken connection) skipped disposing &lt;code&gt;_connection&lt;/code&gt;, so the physical connection never went back to the pool. Since &lt;code&gt;SaveAsync&lt;/code&gt; runs inside an explicit transaction, every write armed this path — under a burst of failures the leak fed itself and eventually drained the pool (symptom: a healthy pool suddenly climbs past &lt;code&gt;MaxPoolSize&lt;/code&gt; with connection-timeout errors, cleared only by a restart). Dispose now runs through &lt;code&gt;try/finally&lt;/code&gt; on both the connection and the transaction wrapper, so the connection always goes back, and the dispose fault is no longer swallowed — it propagates so it stays visible.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  redb.Tsak (the runtime): what made it in
&lt;/h1&gt;

&lt;p&gt;The runtime picks up everything above and adds its own.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The new connectors are bundled.&lt;/strong&gt; &lt;code&gt;redb.Route.Sqs&lt;/code&gt; (&lt;code&gt;sqs://&lt;/code&gt;, &lt;code&gt;sns://&lt;/code&gt;) and &lt;code&gt;redb.Route.Telegram&lt;/code&gt; (&lt;code&gt;telegram://&lt;/code&gt;) now ship in the distribution, so modules on Tsak can use them without wiring anything extra. Technically that's a change to the shared-assembly layer — and a small war story from behind the curtain: that's exactly where we caught that the image build script didn't know about the new connectors. We built the image locally, ran the container, watched the logs print &lt;code&gt;Loaded shared assembly redb.Route.Sqs&lt;/code&gt; and &lt;code&gt;redb.Route.Telegram&lt;/code&gt;, and only then published.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dashboard scheduler shows cron jobs even without a &lt;code&gt;Quartz&lt;/code&gt; config section.&lt;/strong&gt; Previously, with no &lt;code&gt;Quartz&lt;/code&gt; config Tsak registered no shared scheduler, a cron route spun up its own per-context in-memory one, and the management &lt;code&gt;_system&lt;/code&gt; context couldn't see it — so the scheduler page was blank even though the route was running. Tsak now always hands out one shared &lt;code&gt;IScheduler&lt;/code&gt; (falling back to an in-memory &lt;code&gt;RAMJobStore&lt;/code&gt; when there's no config). No cluster and no database needed for a single node; &lt;code&gt;AdoJobStore&lt;/code&gt; is still how you persist and share jobs across nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Users admin API uses a per-request scoped &lt;code&gt;IRedbService&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;UsersController&lt;/code&gt; used to resolve the shared captive singleton, so concurrent admin requests could collide. Now it's a per-request instance via &lt;code&gt;controller.Redb()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Folded in from 3.2.1:&lt;/strong&gt; the Endpoints page no longer hides anonymous contexts it was still counting in the stats, and the standalone web archive now starts on the documented port 8080 instead of the default 5000.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It ships the usual three ways: NuGet packages, Docker images &lt;code&gt;redb-tsak-{worker,web,stack}&lt;/code&gt; (.NET 9; tags &lt;code&gt;:3.3.0-net9&lt;/code&gt;, &lt;code&gt;:3.3.0&lt;/code&gt;, &lt;code&gt;:latest&lt;/code&gt;) on &lt;code&gt;ghcr.io/redbase-app&lt;/code&gt;, cosign-signed, and standalone archives &lt;code&gt;redb-tsak-3.3.0-linux-x64.tar.gz&lt;/code&gt; / &lt;code&gt;redb-tsak-3.3.0-win-x64.zip&lt;/code&gt; with &lt;code&gt;checksums.txt&lt;/code&gt; and &lt;code&gt;.bundle&lt;/code&gt; signatures on the GitHub release. All on &lt;strong&gt;.NET 9&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[SCREENSHOT: the Tsak dashboard on localhost:8080 — the context list with the system echo route]&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  About Pro being free — once more, with detail
&lt;/h2&gt;

&lt;p&gt;I said it up top, but the "what's the catch" question always comes first, so: there's no catch. Pro stays proprietary (the source is closed), but across the whole 3.x line it's handed out &lt;strong&gt;for free and with no bookkeeping&lt;/strong&gt; — no licenses, keys, sign-up, license server, or node/volume limits.&lt;/p&gt;

&lt;p&gt;Concretely, what used to cost money and now doesn't: on the redb core — change tracking, bulk ops, the advanced cache, analytical queries (grouping, window functions); on Tsak — the cluster with its coordinator, the distributed lock, leader election, failover. Exactly the stuff other stacks charge for. Here it's &lt;code&gt;dotnet add package&lt;/code&gt; and you're working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Upgrading from 3.2.x: the short checklist
&lt;/h2&gt;

&lt;p&gt;The release is API-compatible, but there's one behavioral thing worth a look — the parallelism, of course.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Walk your routes that set &lt;code&gt;ConcurrentConsumers(N &amp;gt; 1)&lt;/code&gt;.&lt;/strong&gt; They ran serially before (the bug); now they genuinely parallelize. Make sure the handlers on those routes are thread-safe and you aren't leaning on message order. If order matters, keep &lt;code&gt;ConcurrentConsumers(1)&lt;/code&gt; — that's the default and it hasn't changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check shared state in your handlers.&lt;/strong&gt; Anything that "happened to work" because the route was effectively single-threaded can now run from N threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IRedbService&lt;/code&gt; in routes — nothing to do.&lt;/strong&gt; Per-exchange connections turned on by themselves; if anything, those old &lt;em&gt;"A command is already in progress"&lt;/em&gt; errors under concurrency will disappear. If you were manually caching one &lt;code&gt;IRedbService&lt;/code&gt; across exchanges to route around the DSL — stop; the core will now tell you fail-fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One version, across the board.&lt;/strong&gt; Everything moves to &lt;code&gt;3.3.0&lt;/code&gt;; you don't need to mix &lt;code&gt;redb.Route&lt;/code&gt; 3.3.0 with a 3.2.0 connector (older 3.2.x connectors are API-compatible — they just don't carry the fresh concurrency fixes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tsak: rebuild or pull a fresh image.&lt;/strong&gt; If you build your own distribution, the new connectors (&lt;code&gt;Sqs&lt;/code&gt;, &lt;code&gt;Telegram&lt;/code&gt;) only land in the shared layer after a rebuild; the published &lt;code&gt;:3.3.0&lt;/code&gt; images already have them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is urgent or build-breaking — it's a "go see where parallelism actually turned on for you" pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting it
&lt;/h2&gt;

&lt;p&gt;Core and providers, from NuGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package redb.Core
dotnet add package redb.Postgres      &lt;span class="c"&gt;# or redb.MSSql / redb.SQLite&lt;/span&gt;
dotnet add package redb.Postgres.Pro  &lt;span class="c"&gt;# Pro — free, no key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine and the connectors you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package redb.Route
dotnet add package redb.Route.Sqs
dotnet add package redb.Route.Telegram
dotnet add package redb.Route.Llm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime, as an image or an archive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/redbase-app/redb-tsak-stack:3.3.0
&lt;span class="c"&gt;# or a standalone archive from the v3.3.0 GitHub release (linux-x64 / win-x64), cosign-signed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The images are cosign-signed; the public key ships in the release (&lt;code&gt;cosign.pub&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cosign verify &lt;span class="nt"&gt;--key&lt;/span&gt; cosign.pub ghcr.io/redbase-app/redb-tsak-worker:3.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;3.3.0 is about maturity. The new transports (SQS/SNS, Telegram) and the finished RAG loop are nice, but they aren't the point of this release. The point is that we found and fixed a &lt;strong&gt;systemic hole in concurrency&lt;/strong&gt;: &lt;code&gt;ConcurrentConsumers(N)&lt;/code&gt; gave you no parallelism on any broker, and a shared captive &lt;code&gt;IRedbService&lt;/code&gt; fell apart under load with murky connection errors. That's exactly the class of bug you don't see in a demo and that goes off in prod under load. Now concurrency is honest — on the brokers, on any source via &lt;code&gt;.Threads(N)&lt;/code&gt;, and with per-exchange connections.&lt;/p&gt;

&lt;p&gt;On top of that: the whole stack is on 3.3.0 in lockstep, all on .NET 9, and every Pro package is free with no key. Your own database, your own integration engine, your own runtime with a dashboard and a cluster — one ecosystem, not a menagerie.&lt;/p&gt;

&lt;p&gt;If you take it for a spin, drop a comment with what shows up in your setup. Individual connectors and EIP patterns get their own deep-dives in the series.&lt;/p&gt;

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

&lt;p&gt;If this was useful — a ⭐ on GitHub helps others find it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>llm</category>
      <category>sqlite</category>
    </item>
    <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 console"&gt;&lt;code&gt;&lt;span class="go"&gt;dotnet run --project EchoWorker
&lt;/span&gt;&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 shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5099/api/notes &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;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;tag&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;work&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
curl &lt;span class="s2"&gt;"http://localhost:5099/api/notes?tag=work"&lt;/span&gt;
&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 shell"&gt;&lt;code&gt;curl.exe &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5099/api/notes &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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"tag":"work","text":"hello"}'&lt;/span&gt;
curl.exe &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5099/api/notes &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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"tag":"home","text":"other"}'&lt;/span&gt;
curl.exe &lt;span class="s2"&gt;"http://localhost:5099/api/notes?tag=work"&lt;/span&gt;
&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 powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http://localhost:5099/api/notes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ContentType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{"tag":"work","text":"hello"}'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5099/api/notes?tag=work"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&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 json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"saved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000019&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="nl"&gt;"saved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000022&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="nl"&gt;"Tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"work"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"Text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"hello"&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 &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 console"&gt;&lt;code&gt;&lt;span class="go"&gt;dotnet build EchoModule -c Debug
&lt;/span&gt;&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 console"&gt;&lt;code&gt;&lt;span class="go"&gt;Archive:  output/EchoModule.tpkg
  Length      Name
---------  ----
       49  EchoModule.config.json
    13312  EchoModule.dll
      108  manifest.json
&lt;/span&gt;&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 console"&gt;&lt;code&gt;&lt;span class="go"&gt;docker compose up -d
&lt;/span&gt;&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 shell"&gt;&lt;code&gt;curl.exe &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5099/api/notes &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;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"tag":"work","text":"hello"}'&lt;/span&gt;
curl.exe &lt;span class="s2"&gt;"http://localhost:5099/api/notes?tag=work"&lt;/span&gt;
&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;

&lt;p&gt;If this was useful — a ⭐ on GitHub helps others find 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;

&lt;p&gt;If this was useful — a ⭐ on GitHub helps others find it.&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;

&lt;p&gt;If this was useful — a ⭐ on GitHub helps others find it.&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>
  </channel>
</rss>
