<?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: Hitesh</title>
    <description>The latest articles on DEV Community by Hitesh (@hiteshk97).</description>
    <link>https://dev.to/hiteshk97</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%2F4051790%2Fe5ec7ed7-0434-4581-b45f-49e8a3ffcad1.jpg</url>
      <title>DEV Community: Hitesh</title>
      <link>https://dev.to/hiteshk97</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hiteshk97"/>
    <language>en</language>
    <item>
      <title>Delete the Event Switch: Runtime Dispatch with dynamic in C#</title>
      <dc:creator>Hitesh</dc:creator>
      <pubDate>Thu, 13 Aug 2026 04:36:07 +0000</pubDate>
      <link>https://dev.to/hiteshk97/delete-the-event-switch-runtime-dispatch-with-dynamic-in-c-19mk</link>
      <guid>https://dev.to/hiteshk97/delete-the-event-switch-runtime-dispatch-with-dynamic-in-c-19mk</guid>
      <description>&lt;p&gt;A &lt;code&gt;switch&lt;/code&gt; over event types is dynamic dispatch. You just wrote it by hand, took on the maintenance yourself, and skipped the help the runtime was ready to give you.&lt;/p&gt;

&lt;p&gt;You've written this method. Every event-driven codebase has 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="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseEvent&lt;/span&gt; &lt;span class="n"&gt;@event&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;@event&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OrderPlaced&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;HandleAsync&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;PaymentFailed&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;HandleAsync&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;InventoryReserved&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;HandleAsync&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;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;NotSupportedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;$"No handler for '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It's also a lookup table you maintain by hand, keyed on runtime type, mapping each type to the method that should run. That's the exact job overload resolution already does for you. The compiler does it for free, at every call site, and it never forgets to add a case.&lt;/p&gt;

&lt;p&gt;Let me show you how to hand that job back to the runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Say we have a small, closed hierarchy of domain events that we own:&lt;br&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;abstract&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;BaseEvent&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;record&lt;/span&gt; &lt;span class="nc"&gt;OrderPlaced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseEvent&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;record&lt;/span&gt; &lt;span class="nc"&gt;PaymentFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;OrderId&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;Reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseEvent&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;record&lt;/span&gt; &lt;span class="nc"&gt;InventoryReserved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;OrderId&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;Qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A processor needs to route each event to the right handler. The switch above does it, but the pattern-match arm and the target method are saying the same thing twice: &lt;em&gt;"if it's an &lt;code&gt;OrderPlaced&lt;/code&gt;, call the &lt;code&gt;OrderPlaced&lt;/code&gt; overload."&lt;/em&gt; The type check &lt;em&gt;is&lt;/em&gt; the dispatch. We just wrote it out longhand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the runtime dispatch it
&lt;/h2&gt;

&lt;p&gt;Cast to &lt;code&gt;dynamic&lt;/code&gt; at a single boundary, and let the C# runtime binder pick the overload based on the argument's actual runtime type:&lt;br&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;EventProcessor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The only boundary where the type is late-bound.&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;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseEvent&lt;/span&gt; &lt;span class="n"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;@event&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="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderPlaced&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="c1"&gt;// handle the order...&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="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentFailed&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="c1"&gt;// compensate, alert, retry...&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="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InventoryReserved&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;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;// Least-specific overload: the intentional fallback (more on this below).&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseEvent&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotSupportedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"No handler is registered for '&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="nf"&gt;GetType&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The switch is gone. Adding a fourth event now means adding its record and &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;Dispatch&lt;/code&gt; overload. Nothing central to edit, nothing to forget.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the boundary and the handlers have different names
&lt;/h3&gt;

&lt;p&gt;Small thing, but it matters. The public entry point is &lt;code&gt;HandleAsync&lt;/code&gt; and the overload set is &lt;code&gt;Dispatch&lt;/code&gt;. That's on purpose. If the boundary were also called &lt;code&gt;Dispatch(BaseEvent)&lt;/code&gt;, it would clash with the fallback overload, because two methods with the same signature are a compile error, and &lt;code&gt;public&lt;/code&gt; versus &lt;code&gt;private&lt;/code&gt; doesn't break the tie. Giving the boundary its own name gets you out of that, and it reads better anyway. One method's job is &lt;em&gt;"enter the dispatch"&lt;/em&gt; and the other set's job is &lt;em&gt;"handle a specific event."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;dynamic&lt;/code&gt; removes the conditional, not the responsibility
&lt;/h2&gt;

&lt;p&gt;This is the part that makes or breaks the pattern. &lt;code&gt;dynamic&lt;/code&gt; deletes your &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;switch&lt;/code&gt;, but it does not delete the need to handle an event you never wrote a handler for. It only changes &lt;em&gt;how you find out&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With no fallback, an unrecognized type throws a &lt;code&gt;RuntimeBinderException&lt;/code&gt; from deep inside the framework. That's a confusing error that doesn't even name your domain. So add the fallback on purpose:&lt;br&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="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseEvent&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotSupportedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;$"No handler is registered for '&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="nf"&gt;GetType&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's why it works. Every event &lt;em&gt;is a&lt;/em&gt; &lt;code&gt;BaseEvent&lt;/code&gt;, so the binder always has at least the fallback overload available, and it picks the &lt;strong&gt;most-derived&lt;/strong&gt; one that fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runtime type is &lt;code&gt;OrderPlaced&lt;/code&gt;, so &lt;code&gt;Dispatch(OrderPlaced)&lt;/code&gt; is more specific than &lt;code&gt;Dispatch(BaseEvent)&lt;/code&gt;, and the specific handler wins.&lt;/li&gt;
&lt;li&gt;Runtime type has no dedicated overload, so only &lt;code&gt;Dispatch(BaseEvent)&lt;/code&gt; fits, and you get your &lt;code&gt;NotSupportedException&lt;/code&gt;, thrown from your code, with a message you control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same guarantee the &lt;code&gt;_&lt;/code&gt; arm gave you in the switch, now written as an overload.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this actually behaves at runtime
&lt;/h2&gt;

&lt;p&gt;Two questions people always ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Isn't &lt;code&gt;dynamic&lt;/code&gt; slow?"&lt;/strong&gt; There's a one-time cost the first time a given runtime type flows through the boundary. The DLR builds a call site and works out the overload. That result gets cached per runtime type. After warm-up, dispatching another &lt;code&gt;OrderPlaced&lt;/code&gt; is about as fast as a normal virtual call, and you're not paying reflection costs on every event. For most event pipelines this just doesn't matter. For a genuinely hot path chewing through millions of events a second, benchmark it against a &lt;code&gt;Dictionary&amp;lt;Type, Func&amp;lt;…&amp;gt;&amp;gt;&lt;/code&gt; or a source generator before you commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Does &lt;code&gt;dynamic&lt;/code&gt; leak everywhere?"&lt;/strong&gt; No, and this is the discipline that keeps it sane. &lt;code&gt;dynamic&lt;/code&gt; lives in exactly one expression: &lt;code&gt;(dynamic)@event&lt;/code&gt;. Every &lt;code&gt;Dispatch&lt;/code&gt; overload is ordinary, statically typed C#. They get full IntelliSense, full type checking, and they're easy to unit-test on their own. The late binding stays in one small, contained spot instead of spreading through the class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharp edges worth knowing
&lt;/h2&gt;

&lt;p&gt;A few things that bite if you're not ready for them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Null.&lt;/strong&gt; A null &lt;code&gt;dynamic&lt;/code&gt; has no runtime type for the binder to work with. That's why the boundary guards with &lt;code&gt;ArgumentNullException.ThrowIfNull&lt;/code&gt; before the cast. Skip it and you get a murky runtime error instead of a clear one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return type.&lt;/strong&gt; The expression &lt;code&gt;Dispatch((dynamic)@event)&lt;/code&gt; is itself typed &lt;code&gt;dynamic&lt;/code&gt; at compile time, so returning it from a &lt;code&gt;Task&lt;/code&gt;-returning method adds a runtime conversion to &lt;code&gt;Task&lt;/code&gt;. Keep every overload returning &lt;code&gt;Task&lt;/code&gt; (or a &lt;code&gt;Task&lt;/code&gt; subtype). If a handler ever returns something you can't await, you find out at runtime, not at compile time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling goes quiet.&lt;/strong&gt; "Find all references" on &lt;code&gt;Dispatch(OrderPlaced)&lt;/code&gt; won't show it being called, because statically it isn't. IDE navigation and some analyzers lose the thread across the &lt;code&gt;dynamic&lt;/code&gt; hop. You're trading a bit of compile-time visibility for a lot less churn.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When this is the right tool, and when it isn't
&lt;/h2&gt;

&lt;p&gt;Use it when your event types are a &lt;strong&gt;closed, trusted hierarchy that you own&lt;/strong&gt;: domain events, internal commands, a message set defined in your own assembly. That's the whole premise. The runtime is choosing among a fixed set of methods you wrote, so late binding is safe, and the fallback covers the "someone added a type and forgot a handler" case.&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; point it at arbitrary deserialized payloads or plugin-provided types without checking them first. Late-binding on untrusted input is a footgun. You'd be handing the overload resolver whatever type an attacker or a buggy producer put on the wire. Validate or whitelist the type, &lt;em&gt;then&lt;/em&gt; dispatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about a type-pattern switch?
&lt;/h2&gt;

&lt;p&gt;Fair question. The modern &lt;code&gt;switch&lt;/code&gt; expression over type patterns is a perfectly good alternative:&lt;br&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;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseEvent&lt;/span&gt; &lt;span class="n"&gt;@event&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;@event&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OrderPlaced&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;Dispatch&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;PaymentFailed&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;Dispatch&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;_&lt;/span&gt;               &lt;span class="p"&gt;=&amp;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;NotSupportedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's explicit, statically visible, and pays no binder cost. But notice it's still a table you keep by hand. Adding an event means editing it, and the &lt;code&gt;_&lt;/code&gt; discard means the compiler won't force you to. It has the same "you have to remember the fallback" property as the dynamic version, just spelled out.&lt;/p&gt;

&lt;p&gt;So it comes down to what you want to optimize for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reach for &lt;strong&gt;&lt;code&gt;dynamic&lt;/code&gt;&lt;/strong&gt; when you want adding an event to need &lt;em&gt;only&lt;/em&gt; a new overload, and you're fine trading some static visibility for that.&lt;/li&gt;
&lt;li&gt;Reach for the &lt;strong&gt;switch&lt;/strong&gt; when you want the dispatch table sitting in one readable place and fully visible to your tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both need the deliberate unknown-event branch. Neither is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling it up: resolving handlers from DI
&lt;/h2&gt;

&lt;p&gt;The version so far keeps the handler bodies inside the processor. In real code, they usually live in their own classes, resolved from the container, so they can take their own dependencies. This is where &lt;code&gt;dynamic&lt;/code&gt; really earns its keep, because the awkward part of doing this with DI is the exact thing the cast fixes.&lt;/p&gt;

&lt;p&gt;Define the usual generic handler interface:&lt;br&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TEvent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseEvent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEvent&lt;/span&gt; &lt;span class="n"&gt;@event&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the dispatcher swaps its overload set for a &lt;em&gt;single generic method&lt;/em&gt;, and lets &lt;code&gt;dynamic&lt;/code&gt; pick the type argument instead of the overload:&lt;br&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;EventDispatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IServiceScopeFactory&lt;/span&gt; &lt;span class="n"&gt;scopeFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Same one boundary as before.&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;DispatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseEvent&lt;/span&gt; &lt;span class="n"&gt;@event&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;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;@event&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;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="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;TEvent&lt;/span&gt; &lt;span class="n"&gt;@event&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;where&lt;/span&gt; &lt;span class="n"&gt;TEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseEvent&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&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;scopeFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsyncScope&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;handlers&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="n"&gt;ServiceProvider&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetServices&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;()&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handlers&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotSupportedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;$"No handler registered for '&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;TEvent&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="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;handler&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;handlers&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;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;@event&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;@event&lt;/code&gt; is runtime-type &lt;code&gt;OrderPlaced&lt;/code&gt;, the binder infers &lt;code&gt;TEvent = OrderPlaced&lt;/code&gt; on that generic method, and &lt;code&gt;GetServices&amp;lt;IEventHandler&amp;lt;OrderPlaced&amp;gt;&amp;gt;()&lt;/code&gt; resolves the correctly typed handlers. That's the whole point. &lt;code&gt;dynamic&lt;/code&gt; is bridging &lt;em&gt;runtime type&lt;/em&gt; to &lt;em&gt;generic type parameter&lt;/em&gt;, the one gap you can't cross from a static &lt;code&gt;BaseEvent&lt;/code&gt; reference. Registration is the plain generic story, and with assembly scanning (Scrutor), adding an event touches the dispatcher &lt;strong&gt;zero&lt;/strong&gt; times:&lt;br&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;Scan&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="n"&gt;FromAssemblyOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPlaced&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;AddClasses&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="nf"&gt;AssignableTo&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;IEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsImplementedInterfaces&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScopedLifetime&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things change once DI is in the picture, and both are worth calling out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fallback moves.&lt;/strong&gt; In the overload version, an unknown event got caught by the most-derived overload that fit, and &lt;code&gt;Dispatch(BaseEvent)&lt;/code&gt; threw. Here there's one generic method, so every subtype runs it happily and &lt;code&gt;GetServices&lt;/code&gt; just hands back an empty sequence. The "fail loud on an unhandled event" behavior no longer comes for free, so you put it back with the empty-set check above. Same guarantee, different mechanism. It's a count check now, not overload resolution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifetimes are the real trap.&lt;/strong&gt; Resolving handlers from the container here is fine, not a service-locator smell, because the handler type genuinely isn't known until runtime. That's the textbook case for it, and it's how MediatR dispatches internally. But &lt;em&gt;which&lt;/em&gt; provider you resolve from matters a lot. Event processors are usually long-lived, like a queue consumer or a &lt;code&gt;BackgroundService&lt;/code&gt;, and resolving &lt;em&gt;scoped&lt;/em&gt; handlers off the root provider from a long-lived object is the classic captive-dependency bug that shows up later as an &lt;code&gt;ObjectDisposedException&lt;/code&gt;. That's why the dispatcher takes &lt;code&gt;IServiceScopeFactory&lt;/code&gt; and opens a fresh scope per event instead of hanging onto one provider for its whole life. If your dispatcher already lives inside a scope, say it's resolved per web request, you can inject the scoped &lt;code&gt;IServiceProvider&lt;/code&gt; directly and skip the manual scope.&lt;/p&gt;

&lt;p&gt;One footnote for the reflection-minded. The pre-&lt;code&gt;dynamic&lt;/code&gt; way to do all of this is &lt;code&gt;typeof(IEventHandler&amp;lt;&amp;gt;).MakeGenericType(@event.GetType())&lt;/code&gt; followed by a reflected invoke, usually behind a cached compiled delegate. It's the same idea written out by hand. &lt;code&gt;dynamic&lt;/code&gt; just lets the DLR do the &lt;code&gt;MakeGenericType&lt;/code&gt;, the resolution, and the per-type call-site caching for you. Same trade as the rest of the post: you give up "find all references" on the handlers in exchange for never editing the dispatcher.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The switch was never wrong. It was redundant. Picking a method based on the runtime type of an argument is the whole definition of overload resolution, and the runtime already knows how to do it, faster and more reliably than a hand-kept list of &lt;code&gt;case&lt;/code&gt; labels.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dynamic&lt;/code&gt; just asks it to do that job one moment later than usual. Delete the switch, keep the fallback.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>cleancode</category>
    </item>
    <item>
      <title>Request and Response Migration in .NET</title>
      <dc:creator>Hitesh</dc:creator>
      <pubDate>Tue, 11 Aug 2026 17:56:09 +0000</pubDate>
      <link>https://dev.to/hiteshk97/request-and-response-migration-in-net-59gj</link>
      <guid>https://dev.to/hiteshk97/request-and-response-migration-in-net-59gj</guid>
      <description>&lt;p&gt;If you've maintained a public API long enough, you've opened a folder that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Handlers/
  GetQuoteV1Handler.cs
  GetQuoteV2Handler.cs
  GetQuoteV3Handler.cs   ← the only one anybody has read this year
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Three files, one behaviour. V1 and V2 are copies of V3 with a field dropped and a flag hardcoded.&lt;/p&gt;

&lt;p&gt;Nobody planned this. We shipped &lt;code&gt;GetQuote&lt;/code&gt;, symbol in and price out. A customer wanted the daily change, so v2 grew an &lt;code&gt;IncludeChange&lt;/code&gt; flag. We sold into Europe, so v3 grew &lt;code&gt;Currency&lt;/code&gt;. Every one of those was the right call on the day. And you can't delete the old ones, because there's a mobile app whose release cycle you don't own and a partner who integrated in 2022 and won't look at it again.&lt;/p&gt;

&lt;p&gt;So you keep all three. A pricing fix is three edits. Then one Friday somebody patches two of the three, and from that afternoon v2 quotes a different number than v3. You find out five weeks later from a customer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Asp.Versioning&lt;/code&gt; works out &lt;em&gt;which&lt;/em&gt; version a request is. It has nothing to say about the duplication sitting behind that answer.&lt;/p&gt;
&lt;h2&gt;
  
  
  What we tried first
&lt;/h2&gt;

&lt;p&gt;The instinct is to pull the logic into a shared method with optional parameters:&lt;br&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetQuoteCore&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;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;includeChange&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"USD"&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;But &lt;code&gt;includeChange = false&lt;/code&gt; is v1's behaviour written down as a default in a signature that belongs to the &lt;em&gt;current&lt;/em&gt; version. Nothing connects it to the version it exists for. Delete v1 in three years and that &lt;code&gt;= false&lt;/code&gt; sits there forever, because nobody can prove which caller needs it. And it grows one permanent parameter per version until half the defaults are archaeology.&lt;/p&gt;

&lt;p&gt;Branching on a version number inside one handler is worse: the checks interleave, so you can't read one version end to end, and one request type now has to carry the union of every field any version ever had. A base class with virtual hooks looks like real design but is a fragile base class in a costume, because adding v4 means changing the base, and changing the base changes what v1 does.&lt;/p&gt;

&lt;p&gt;All three share a shape. Each one puts knowledge of &lt;em&gt;every&lt;/em&gt; version in one place, so the thing you edit grows with every version you've shipped, and every edit can touch all of them. What we wanted was the opposite: each version knowing about exactly one neighbour, and old versions being genuinely finished.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea, borrowed from Stripe
&lt;/h2&gt;

&lt;p&gt;Stripe has shipped close to a hundred backwards-incompatible changes and retired zero versions. Code written against their API a decade ago still runs. &lt;a href="https://stripe.com/blog/api-versioning" rel="noopener noreferrer"&gt;Brandur Leach wrote up how&lt;/a&gt;, and it comes down to two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One implementation, always at the newest version. Nothing in it has heard of 2017.&lt;/li&gt;
&lt;li&gt;A small module per breaking change that knows how to undo it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A response gets built at the current version, then walked backwards through those modules until the shape matches what the caller is pinned to. A version stops costing you a copy of your business logic and starts costing you a small transform. Nothing about that is HTTP-specific, so we built it for .NET, in a library called &lt;a href="https://github.com/hiteshk97/versionary" rel="noopener noreferrer"&gt;Versionary&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building it
&lt;/h2&gt;

&lt;p&gt;Start with plain records. No base class, no attributes, no library reference. The current one gets no prefix, because it isn't a version, it's the contract.&lt;br&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;V1&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;record&lt;/span&gt; &lt;span class="nc"&gt;GetQuote&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;Symbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequestContract&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;&amp;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;record&lt;/span&gt; &lt;span class="nc"&gt;Quote&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;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;V2&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;record&lt;/span&gt; &lt;span class="nc"&gt;GetQuote&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;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IncludeChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequestContract&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;&amp;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;record&lt;/span&gt; &lt;span class="nc"&gt;Quote&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;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Change&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Current.&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;record&lt;/span&gt; &lt;span class="nc"&gt;GetQuote&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;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IncludeChange&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;Currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IRequestContract&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;&amp;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;record&lt;/span&gt; &lt;span class="nc"&gt;Quote&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;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Change&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;Currency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then one handler, written for the current shape. This is the only copy of the behaviour, and it has never heard of v1.&lt;br&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;GetQuoteHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPriceFeed&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IVersionaryHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetQuote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Quote&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;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetQuote&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;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;price&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;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PriceAsync&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;Symbol&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;change&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;IncludeChange&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.015m&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="m"&gt;0m&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;Quote&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;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;change&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;Currency&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;And the piece that replaces the two deleted handlers, one migrator per hop:&lt;br&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;V1QuoteMigrator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;IMigrator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;V1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetQuote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetQuote&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// forward:  the request goes up&lt;/span&gt;
    &lt;span class="n"&gt;IMigrator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;V1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;          &lt;span class="c1"&gt;// backward: the response comes down&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;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetQuote&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;MigrateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetQuote&lt;/span&gt; &lt;span class="n"&gt;input&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;=&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetQuote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IncludeChange&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;public&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;V1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;MigrateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quote&lt;/span&gt; &lt;span class="n"&gt;input&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;=&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;V1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&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;IncludeChange: false&lt;/code&gt; is the whole point. That one literal is the entire v1 behaviour, in one line you can put in front of a reviewer. It used to be an emergent property of a 200-line handler nobody had opened since 2023. Both directions live in one class on purpose, because the request transform and the response transform are two halves of the same decision, and split across files someone updates one and forgets the other.&lt;/p&gt;

&lt;p&gt;Each migrator only speaks to its immediate neighbour. V1's knows v2 and nothing else, even after v3 ships. The chain does the rest.&lt;br&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;AddVersionary&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterFromAssemblyContaining&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetQuoteHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;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;"/v1/quotes/{symbol}"&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IVersionarySender&lt;/span&gt; &lt;span class="n"&gt;sender&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;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;TypedResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&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;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&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;V1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetQuote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&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;That endpoint mentions no migration and no response type. The contract declares what it returns, so asking a v1 request for a v2 response won't compile. What actually happens:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V1.GetQuote ──► V2.GetQuote ──► GetQuote ──► [ the one handler ]
                                                     │
V1.Quote    ◄─── V2.Quote    ◄─── Quote    ◄─────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two hops up, run, two hops back. The client gets bytes identical to 2022.&lt;/p&gt;
&lt;h2&gt;
  
  
  The harder direction
&lt;/h2&gt;

&lt;p&gt;Stripe mostly walks &lt;em&gt;responses&lt;/em&gt; backwards. Migrating a request &lt;em&gt;forward&lt;/em&gt; is harder, because you have to invent data the caller never sent. A v2 client couldn't tell us the currency, so somebody has to decide it, and sometimes that means a lookup:&lt;br&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;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GetQuote&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;MigrateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;V2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetQuote&lt;/span&gt; &lt;span class="n"&gt;input&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;currency&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;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CurrencyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Symbol&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;GetQuote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IncludeChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&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;That's why migrations are async and migrators come out of DI. Pure reshaping allocates nothing, but a migration that needs a database round trip shouldn't have to fight the API to make one.&lt;/p&gt;
&lt;h2&gt;
  
  
  "So where do I configure the current version?"
&lt;/h2&gt;

&lt;p&gt;This stalled the first design review. The tempting answer is a setting: &lt;code&gt;cfg.CurrentVersion = "v3"&lt;/code&gt;. Don't. It's a second source of truth that has to agree with your handler, it goes stale silently when you add v4 and forget to bump it, and it forces one versioning scheme on an API, an internal service, and a queue consumer that don't share one.&lt;/p&gt;

&lt;p&gt;There's no setting, because the rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A contract is current when nothing migrates away from it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The forward walk is deliberately dumb: take the request's type, look for an outgoing hop, run it, repeat. When the lookup comes back empty, you've arrived, and that's where the handler is. The version map is &lt;em&gt;derived&lt;/em&gt; from the code, not declared next to it and left to rot. Pinning a version that changed &lt;em&gt;behaviour&lt;/em&gt; rather than shape then costs nothing: give it a handler and no outgoing migrator, and its requests arrive untouched.&lt;/p&gt;
&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;Add a version tomorrow, and here's what moves:&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;Changes?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The v1 endpoint&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The v2 endpoint&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A migrator for the new hop&lt;/td&gt;
&lt;td&gt;Added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your handler, now on the new contract&lt;/td&gt;
&lt;td&gt;Changed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two files, neither an endpoint. A v1 endpoint names v1 types and only v1 types, and those can never change.&lt;/p&gt;

&lt;p&gt;The one way to get burned is to add the migrator and forget to move the handler forward, so it strands on an old contract and can never run. That's a startup failure, not a 3am one:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error VER005: A handler is registered for 'Api.V3.GetQuote', but that contract still
              migrates onward to 'Api.GetQuote', so the handler can never run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Cycles, duplicate hops, stranded handlers, ambiguous paths- all checked while &lt;code&gt;AddVersionary&lt;/code&gt; runs and reported together. There's a &lt;code&gt;Graph.Validate()&lt;/code&gt; for a unit test too, and a &lt;code&gt;Graph.Explain()&lt;/code&gt; that prints the version map from the graph itself, so your docs can't drift.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Isn't this just mapping?"
&lt;/h2&gt;

&lt;p&gt;Someone asked me this in review, and for two versions they're right: write &lt;code&gt;MapV1ToCurrent&lt;/code&gt; and &lt;code&gt;MapCurrentToV1&lt;/code&gt; and skip this whole article.&lt;/p&gt;

&lt;p&gt;The difference shows up at three, and it's structural. Hand-rolled mappers all point at the current contract:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V1 ───┐
V2 ──┐│
V3 ─┐││
    ▼▼▼
  current
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;So when current changes, which is the only reason you're adding a version, every mapper is wrong at once, and the compiler only catches the fields that disappeared. Add a field with a default, and they all still build while quietly doing the wrong thing.&lt;/p&gt;

&lt;p&gt;Chained hops point at their neighbour:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;V1 ──► V2 ──► V3 ──► current
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;V1QuoteMigrator&lt;/code&gt; names v1 and v2 and nothing else. Both types are frozen, so it compiled in 2022 and will compile after v7. Adding a version appends one hop and touches nothing behind it. That's O(n) edits per version versus one. (The same trap catches "each endpoint just calls one service directly": every endpoint names the current service signature, so they all break together, and the version semantics end up scattered across your routing table.)&lt;/p&gt;

&lt;p&gt;So yes, it's mapping. The library is a chain, a lookup, and a validator wrapped around it. The value is entirely in the chain being adjacent rather than radial.&lt;/p&gt;
&lt;h2&gt;
  
  
  On MediatR (optional)
&lt;/h2&gt;

&lt;p&gt;The core has no idea MediatR exists. But if you already dispatch through &lt;code&gt;ISender&lt;/code&gt;, you keep your handlers and behaviours and add one line:&lt;br&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;AddVersionary&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterFromAssemblyContaining&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&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;AddMediatRPipeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The one thing worth knowing: when a v1 request climbs two hops, which validators fire? &lt;code&gt;SinglePass&lt;/code&gt; (the default) validates the arriving and the current contract. &lt;code&gt;Reentrant&lt;/code&gt; re-dispatches each hop, so a validator written for the intermediate &lt;code&gt;V2.GetOrder&lt;/code&gt; also fires, catching a bad migration where it happened rather than three hops later. &lt;code&gt;Reentrant&lt;/code&gt; re-runs &lt;em&gt;everything&lt;/em&gt;, though, so register idempotent behaviours (validation, logging) outermost and once-only ones (transactions, audit, outbox) inside the pipeline. Details are in the repo.&lt;/p&gt;
&lt;h2&gt;
  
  
  When this is the wrong tool
&lt;/h2&gt;

&lt;p&gt;Changes come in three kinds. &lt;strong&gt;Additive&lt;/strong&gt; (a new optional field) usually needs no migrator. &lt;strong&gt;Shape&lt;/strong&gt; (renamed, split, merged, nested) is exactly what this exists for. &lt;strong&gt;Behavioural&lt;/strong&gt; is the trap: the shape is identical, and the meaning moved underneath it. Cancelling used to refund immediately and now queues it. No transform can express that, because the data was never what changed.&lt;/p&gt;

&lt;p&gt;One question settles it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can you write a function from the old shape to the new one that loses nothing a caller relied on?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes, write the migrator. No, pin the version to its own handler and take the second copy. Pinning is the right answer there, not a failure, but be honest: on that endpoint you've saved nothing. Two of ours are pinned.&lt;/p&gt;

&lt;p&gt;A couple of sharp edges: every hop boxes (messages move as &lt;code&gt;object&lt;/code&gt;), and assembly scanning isn't AOT-safe, though there are inline registration forms that are.&lt;/p&gt;

&lt;p&gt;Once the chain exists, it fits things that aren't API calls. &lt;strong&gt;Event upcasting&lt;/strong&gt; is the same forward walk with no handler on the end, because an event stored three years ago just needs bringing up to the current shape. That works because the graph has no idea what a version is, only that one type can become another. Working out which version a message belongs to stays your transport's job.&lt;/p&gt;
&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;Two handlers became two migrators, so the file count didn't move. But there's now exactly one copy of the behaviour. A pricing fix is one edit. The Friday bug where v2 and v3 silently disagree can't happen, because there's nothing left to disagree. And &lt;code&gt;IncludeChange: false&lt;/code&gt; is a better spec for v1 than the v1 handler ever was.&lt;/p&gt;

&lt;p&gt;The asterisk: this reshapes data, not behaviour. If a version differs in what it &lt;em&gt;does&lt;/em&gt;, you pin it, and this bought you nothing. If you've got two versions and no duplication yet, none of this is worth your afternoon. Come back when it starts to hurt. It will.&lt;/p&gt;


&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/hiteshk97" rel="noopener noreferrer"&gt;
        hiteshk97
      &lt;/a&gt; / &lt;a href="https://github.com/hiteshk97/versionary" rel="noopener noreferrer"&gt;
        versionary
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Versionary&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
  &lt;strong&gt;Serve every version of your API from one handler.&lt;/strong&gt;
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://github.com/hiteshk97/versionary/LICENSE" rel="noopener noreferrer"&gt;&lt;img alt="License: MIT" src="https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667"&gt;&lt;/a&gt;
  &lt;a href="https://www.nuget.org/packages/Versionary" rel="nofollow noopener noreferrer"&gt;&lt;img alt="NuGet" src="https://camo.githubusercontent.com/83e64bb1f1cfa595f20d628d77ff68b3d4427e69b14f58d9ca1b2c7354bc380e/68747470733a2f2f696d672e736869656c64732e696f2f6e756765742f762f56657273696f6e6172792e737667"&gt;&lt;/a&gt;
  &lt;a href="https://www.nuget.org/packages/Versionary" rel="nofollow noopener noreferrer"&gt;&lt;img alt="Downloads" src="https://camo.githubusercontent.com/16242b1e039eaf551d09c581c91a3d00767a11f542af8e488ca3469fc075a25c/68747470733a2f2f696d672e736869656c64732e696f2f6e756765742f64742f56657273696f6e6172792e7376673f6c6162656c3d646f776e6c6f616473"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/hiteshk97/versionary/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img alt="CI" src="https://github.com/hiteshk97/versionary/actions/workflows/ci.yml/badge.svg"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/cf92d7bd3490e20edcdff1c34362b66cbf7482d6f07176495d6267dde0e0c6d0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6e65742d382e3025323025374325323031302e302d353132424434"&gt;&lt;img alt="Targets" src="https://camo.githubusercontent.com/cf92d7bd3490e20edcdff1c34362b66cbf7482d6f07176495d6267dde0e0c6d0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6e65742d382e3025323025374325323031302e302d353132424434"&gt;&lt;/a&gt;
&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How you got here&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;You shipped &lt;code&gt;CreateOrder&lt;/code&gt;. It was clean.&lt;/p&gt;

&lt;p&gt;Then finance wanted tax broken out on the response, so you added a flag. That's v2. Europe happened
and orders needed a currency, so that's v3. Payments asked for an idempotency key and you shipped v4.&lt;/p&gt;
&lt;p&gt;Every one of those was the right call on the day you made it. That's what makes this so annoying.&lt;/p&gt;
&lt;p&gt;You can't turn the old versions off. There's a mobile app whose release cycle you don't own. There's
a partner who integrated in 2022 and has no budget to look at it again. There are terminals sitting in
venues that get updated when somebody drives out there with a laptop.&lt;/p&gt;
&lt;p&gt;So all four are live:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;CreateOrderV1Handler  ─┐
CreateOrderV2Handler  ─┤   four handlers
CreateOrderV3Handler  ─┤   one actual behaviour
CreateOrderV4Handler  ─┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Three of them are copies…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/hiteshk97/versionary" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="crayons-card c-embed"&gt;

  &lt;br&gt;
&lt;strong&gt;📦 Install&lt;/strong&gt;

&lt;p&gt;&lt;code&gt;dotnet add package Versionary&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;MIT · &lt;code&gt;net8.0&lt;/code&gt; / &lt;code&gt;net10.0&lt;/code&gt;&lt;br&gt;

&lt;/p&gt;
&lt;/div&gt;


&lt;p&gt;Issues and PRs are welcome. I'd &lt;strong&gt;especially&lt;/strong&gt; like to hear from anyone who made the pin-versus-migrate call on a real behavioural change and regretted which way they went. 👇&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>aspdotnet</category>
      <category>dotnet</category>
      <category>api</category>
    </item>
    <item>
      <title>Ambient Context in .NET</title>
      <dc:creator>Hitesh</dc:creator>
      <pubDate>Sun, 02 Aug 2026 13:33:59 +0000</pubDate>
      <link>https://dev.to/hiteshk97/ambient-context-in-net-3ln3</link>
      <guid>https://dev.to/hiteshk97/ambient-context-in-net-3ln3</guid>
      <description>&lt;p&gt;If you’ve worked in .NET long enough, you’ve probably seen a method signature that looks exactly like 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="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateOrderAsync&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;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UpdateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&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;currentUserId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;currentUserId&lt;/code&gt; at the end? That’s the culprit.&lt;/p&gt;

&lt;p&gt;Nobody &lt;em&gt;chooses&lt;/em&gt; which user they are when updating an order. That information is &lt;strong&gt;ambient&lt;/strong&gt;. It’s just a fact about the current request. But because there’s no obvious place to store it, we end up manually passing it down a bucket brigade: from the controller, to the business service, to another helper service, and finally down to the database just to write an audit log.&lt;/p&gt;

&lt;p&gt;Eventually, someone gets tired of the plumbing and decides to get clever. They inject &lt;code&gt;IHttpContextAccessor&lt;/code&gt; directly into the domain service. Suddenly, your core business logic is tied to &lt;code&gt;Microsoft.AspNetCore.Http&lt;/code&gt;. Everything seems fine, right up until a nightly background job tries to reuse that same service, hits a null &lt;code&gt;HttpContext&lt;/code&gt;, and crashes.&lt;/p&gt;

&lt;p&gt;We escaped this trap by stealing the &lt;em&gt;concept&lt;/em&gt; behind &lt;code&gt;IHttpContextAccessor&lt;/code&gt; without actually using it. It takes about 80 lines of code. Here is exactly how we did it, including answers to the two questions that always come up in code review: "Why is this registered as a Singleton?" and "How does this work when there is no HTTP request?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The Magic Behind &lt;code&gt;IHttpContextAccessor&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before we write our own, let’s look at how Microsoft does it. The trick is actually much simpler than people assume. Here is the real implementation of &lt;code&gt;IHttpContextAccessor&lt;/code&gt;, almost word for word:&lt;br&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;HttpContextAccessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IHttpContextAccessor&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;AsyncLocal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpContextHolder&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_httpContextCurrent&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;public&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;HttpContext&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_httpContextCurrent&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;Context&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;holder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_httpContextCurrent&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;holder&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="c1"&gt;// Clear the trapped HttpContext. It's done.&lt;/span&gt;
                &lt;span class="n"&gt;holder&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="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="k"&gt;value&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;_httpContextCurrent&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;new&lt;/span&gt; &lt;span class="n"&gt;HttpContextHolder&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="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;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&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;HttpContextHolder&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;HttpContext&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="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 things here do all the heavy lifting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AsyncLocal&amp;lt;T&amp;gt;&lt;/code&gt;:&lt;/strong&gt; Think of this as a backpack for your thread. It understands &lt;code&gt;await&lt;/code&gt;. Whatever you put in it flows &lt;em&gt;down&lt;/em&gt; through the entire async call chain, even if the work hops between different background threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;HttpContextHolder&lt;/code&gt; extra layer:&lt;/strong&gt; This looks like pointless extra code, but it earns its keep. When a new execution context splits off (like a fire-and-forget &lt;code&gt;Task.Run&lt;/code&gt; or a timer), it captures a &lt;em&gt;snapshot&lt;/em&gt; of the current &lt;code&gt;AsyncLocal&lt;/code&gt; value, not a live link back to you. So if we stored the context directly, cleaning up later with &lt;code&gt;_httpContextCurrent.Value = null&lt;/code&gt; only affects our own flow. It can’t reach into the snapshot that stray task already grabbed. A long-lived capture (a static event handler, a persistent timer, a background loop) can then pin the whole request in memory for as long as &lt;em&gt;it&lt;/em&gt; stays alive. By storing a box we can change later (the holder), we get around that. Every snapshot points at the &lt;em&gt;same&lt;/em&gt; holder instance, so setting &lt;code&gt;holder.Context = null&lt;/code&gt; at the end of the request empties the box for all of them at once, and the garbage collector can reclaim the request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice something important: &lt;strong&gt;there is nothing HTTP-specific about either of those tricks.&lt;/strong&gt; So, we just built our own version to hold what we actually cared about: the current user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Our Own
&lt;/h2&gt;

&lt;p&gt;We need four simple types. First, let’s define what a user looks like in our system:&lt;br&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;interface&lt;/span&gt; &lt;span class="nc"&gt;ICurrentUser&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsAuthenticated&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="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;int&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="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ExternalUserId&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="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;string&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;get&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="n"&gt;Email&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="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;Roles&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="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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurrentUser&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICurrentUser&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;readonly&lt;/span&gt; &lt;span class="n"&gt;ICurrentUser&lt;/span&gt; &lt;span class="n"&gt;Anonymous&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;CurrentUser&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;IsAuthenticated&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;init&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;int&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;init&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;ExternalUserId&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;init&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;UserName&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;init&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;Email&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;init&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;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;Roles&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;init&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;Notice how everything uses &lt;code&gt;init&lt;/code&gt;. The current user is a &lt;em&gt;fact&lt;/em&gt; about the current call, not a variable to be tweaked. If a service wants to change it, it has to replace the whole object. No silent changes allowed.&lt;/p&gt;

&lt;p&gt;Next, we build our ambient store, stealing Microsoft’s "holder box" design:&lt;br&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurrentUserHolder&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;AsyncLocal&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Holder&lt;/span&gt;&lt;span class="p"&gt;&amp;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;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Notice: This never returns null! Unauthenticated calls get CurrentUser.Anonymous.&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;ICurrentUser&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;Value&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;CurrentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anonymous&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;void&lt;/span&gt; &lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICurrentUser&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Empty the box the previous execution contexts are holding...&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="n"&gt;Current&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;existing&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;existing&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;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;// ...then hand this flow a fresh one.&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;Value&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;Holder&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;user&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;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Clear&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;existing&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;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;existing&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;existing&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;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;span class="k"&gt;private&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;Holder&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;ICurrentUser&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We made one deliberate change from Microsoft’s design: &lt;strong&gt;our accessor never returns null.&lt;/strong&gt; Dealing with nullable contexts means writing &lt;code&gt;?.&lt;/code&gt; everywhere. In our system, an unauthenticated call isn’t an "absent" user; it’s an "anonymous" one. This means &lt;code&gt;CurrentUser.Anonymous&lt;/code&gt; is the floor, and calling &lt;code&gt;accessor.CurrentUser.UserId&lt;/code&gt; is always safe.&lt;/p&gt;

&lt;p&gt;Finally, we create the clean, injectable window for our application code to 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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ICurrentUserAccessor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ICurrentUser&lt;/span&gt; &lt;span class="n"&gt;CurrentUser&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="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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurrentUserAccessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICurrentUserAccessor&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;ICurrentUser&lt;/span&gt; &lt;span class="n"&gt;CurrentUser&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CurrentUserHolder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&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;This is &lt;strong&gt;get-only&lt;/strong&gt;. We don’t want random services overriding the current user. Writing to the store goes through &lt;code&gt;CurrentUserHolder.Set&lt;/code&gt;, which should only ever be called by the entry points of your app.&lt;/p&gt;

&lt;p&gt;We register it once in our DI container:&lt;br&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="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICurrentUserAccessor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CurrentUserAccessor&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;And this is where code reviews usually grind to a halt. &lt;strong&gt;"Wait, why is this a Singleton?"&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Singleton Controversy
&lt;/h2&gt;

&lt;p&gt;The temptation is to register the user as a scoped service. I’ve written this exact code myself:&lt;br&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 tempting version. Don't do this.&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;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICurrentUser&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;http&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;IHttpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;MapFromPrincipal&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="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;User&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;It looks gorgeous. You can just inject &lt;code&gt;ICurrentUser&lt;/code&gt; into your constructor without dealing with accessors. But it will break your app in four distinct ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Singletons can’t use it:&lt;/strong&gt; If you have a singleton service (like an EF Core Interceptor, a cache, or a background worker) that needs to know the current user, it will crash. The DI container will yell at you about a "captive dependency": you’ve trapped a short-lived scoped object inside a long-lived singleton.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It freezes in time:&lt;/strong&gt; A scoped instance is created once per scope and cached. If it gets resolved &lt;em&gt;before&lt;/em&gt; your authentication middleware has figured out who the user is, every other service in that request will receive an "Anonymous" user. Silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes aren’t always what you think:&lt;/strong&gt; We treat "Scoped" and "Per HTTP Request" as the same thing because ASP.NET Core does that for us. But in a background worker, there might only be one DI scope for the entire lifetime of the process!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The "Window" Analogy
&lt;/h3&gt;

&lt;p&gt;Look at how ASP.NET Core registers its own accessor:&lt;br&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="n"&gt;TryAddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IHttpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HttpContextAccessor&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;It’s a singleton! This isn’t a mistake. The key is separating the &lt;em&gt;data&lt;/em&gt; from the &lt;em&gt;object that hands you the data&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Think of &lt;code&gt;ICurrentUserAccessor&lt;/code&gt; as a window. The window itself never changes (it’s a Singleton). It has no state, no fields, and holds no memory. But the scenery &lt;em&gt;outside&lt;/em&gt; the window (&lt;code&gt;AsyncLocal&lt;/code&gt;) changes depending on exactly when you look through it.&lt;/p&gt;

&lt;p&gt;By injecting a singleton window, you get hold of a &lt;strong&gt;value provider&lt;/strong&gt;. You ask it for the answer at the exact moment you need it, ensuring you always get the right user, no matter which DI scope you happen to be sitting in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the User
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The current user is only ever written at &lt;strong&gt;entry points&lt;/strong&gt;. Everywhere else just reads it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In an HTTP API, that entry point is a middleware registered right after authentication:&lt;br&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;CurrentUserMiddleware&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="n"&gt;RequestDelegate&lt;/span&gt; &lt;span class="n"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CurrentUserMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RequestDelegate&lt;/span&gt; &lt;span class="n"&gt;next&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;_next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next&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;InvokeAsync&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;httpContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IUserService&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CurrentUserHolder&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ResolveAsync&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;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userService&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="nf"&gt;_next&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="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="n"&gt;CurrentUserHolder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Empty the box!&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// (ResolveAsync left out to keep this short. It just maps ClaimsPrincipal to our CurrentUser)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What about Background Jobs?
&lt;/h3&gt;

&lt;p&gt;This is where the pattern really shines. What if a background queue consumer needs to process a job on behalf of a user?&lt;/p&gt;

&lt;p&gt;The publisher simply stamps the user’s ID onto the message envelope before sending it. When the consumer picks it up, it sets up the ambient 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;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;CurrentUserConsumeFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&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;IConsumeFilter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&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;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;OnConsumeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MessageEnvelope&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ConsumeDelegate&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// For automated background tasks, we use a dedicated system account&lt;/span&gt;
        &lt;span class="n"&gt;CurrentUserHolder&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="n"&gt;SystemPrincipals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BackgroundWorker&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="nf"&gt;next&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="n"&gt;CurrentUserHolder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&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;From this point on, the message handler (and every service it calls) sees the exact same &lt;code&gt;ICurrentUserAccessor&lt;/code&gt; it would see in a web request. &lt;strong&gt;Your business logic never has to know if it was triggered by an HTTP call or a RabbitMQ message.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Massive Payoff: Automatic Audit Columns
&lt;/h2&gt;

&lt;p&gt;If this sounds like a lot of theory, here is the feature that will make you fall in love with it.&lt;/p&gt;

&lt;p&gt;Let’s say your database tables have standard audit columns (&lt;code&gt;CreatedBy&lt;/code&gt;, &lt;code&gt;ModifiedBy&lt;/code&gt;). We can use an EF Core Interceptor to fill these in automatically, &lt;em&gt;globally&lt;/em&gt;, without ever writing it in a service again.&lt;br&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;AuditInterceptor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SaveChangesInterceptor&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="n"&gt;ICurrentUserAccessor&lt;/span&gt; &lt;span class="n"&gt;_accessor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Safely capture the singleton!&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AuditInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICurrentUserAccessor&lt;/span&gt; &lt;span class="n"&gt;accessor&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;_accessor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accessor&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;Stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContext&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="k"&gt;if&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;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="c1"&gt;// Because this runs right when SaveChanges is called, it always gets the correct, current user.&lt;/span&gt;
        &lt;span class="kt"&gt;var&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;_accessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentUser&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;now&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="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;entry&lt;/span&gt; &lt;span class="k"&gt;in&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;ChangeTracker&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;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAuditable&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Added&lt;/span&gt;&lt;span class="p"&gt;)&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;Entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedBy&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedOnUtc&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="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;State&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Added&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;State&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Modified&lt;/span&gt;&lt;span class="p"&gt;)&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;Entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModifiedBy&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModifiedOnUtc&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="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;// (Wire this up to SavingChanges and SavingChangesAsync)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because our &lt;code&gt;ICurrentUserAccessor&lt;/code&gt; is a Singleton and works across HTTP threads and background workers, this interceptor functions perfectly everywhere. No developer ever has to remember to set &lt;code&gt;ModifiedBy = currentUserId&lt;/code&gt; again, which means no developer can forget.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Bonus: You can use this exact same pattern to auto-tag your Serilog logs via custom Enrichers, so every single log is automatically stamped with the active user’s ID!)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Few Real-World Gotchas
&lt;/h2&gt;

&lt;p&gt;If you’re going to use this, keep these sharp edges in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AsyncLocal&lt;/code&gt; flows down, not up:&lt;/strong&gt; If you set the user deep inside a child method, the parent method won’t see it. Always call &lt;code&gt;Set()&lt;/code&gt; at the absolute highest point of your operation (middleware, top of the cron loop, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always &lt;code&gt;Clear()&lt;/code&gt; in a &lt;code&gt;finally&lt;/code&gt; block:&lt;/strong&gt; This is what prevents memory leaks in long-lived applications. Don’t skip it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t treat it like a junk drawer:&lt;/strong&gt; This pattern is for &lt;em&gt;ambient&lt;/em&gt; data (like the user). Don’t start stuffing &lt;code&gt;CurrentOrderId&lt;/code&gt; or &lt;code&gt;IsUserHavingAGoodDay&lt;/code&gt; in there. If you need a new ambient scope (like a Tenant ID), build a separate, dedicated accessor for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One Pattern, Many Uses
&lt;/h2&gt;

&lt;p&gt;Once this is in your toolbox, you start spotting places for it everywhere. The user is just the first example. Anything that is a &lt;em&gt;fact about the current operation&lt;/em&gt;, rather than a real input to your methods, is a good fit.&lt;/p&gt;

&lt;p&gt;The one I reach for most is the &lt;strong&gt;correlation ID&lt;/strong&gt;. When a request kicks off, you generate a short ID and drop it in an ambient holder. Every log line inside that request can pick it up on its own, so you can trace one user’s journey through a noisy log file without threading an ID through every method.&lt;/p&gt;

&lt;p&gt;The real win is that it survives process boundaries. Your publisher stamps the correlation ID onto the message envelope (exactly like we did with the user), the consumer reads it back on the other side and sets its own ambient holder, and suddenly one ID ties together the HTTP call, the queue message, and the three services that handled it downstream. Debugging a distributed system stops feeling like guesswork.&lt;/p&gt;

&lt;p&gt;A few other things that fit the same mold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tenant ID&lt;/strong&gt; in a multi-tenant app, so every query is scoped to the right customer without passing it through every layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request culture or locale&lt;/strong&gt;, so formatting and translations just work wherever you are in the call chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature flag context&lt;/strong&gt;, so a deep-down service can check "is this experiment on for this user?" without being handed the whole context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recipe is always the same: a get-only accessor (the window), an &lt;code&gt;AsyncLocal&lt;/code&gt; holder behind it (the scenery), and a &lt;code&gt;Set()&lt;/code&gt; / &lt;code&gt;Clear()&lt;/code&gt; pair at your entry points. Build a separate, dedicated accessor for each kind of data, resist the urge to cram them all into one, and you get the same clean, leak-free behavior every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;Absolutely. By adding four simple classes and one DI registration, we entirely deleted &lt;code&gt;int currentUserId&lt;/code&gt; from hundreds of method signatures. Audit logs went from a manual chore that was constantly forgotten, to an automated guarantee that works flawlessly in both web apps and background workers.&lt;/p&gt;

&lt;p&gt;Sometimes framework abstractions like &lt;code&gt;IHttpContextAccessor&lt;/code&gt; are 80% of what you need, but the missing 20% makes them unusable. But if you peek underneath the hood and steal the &lt;em&gt;pattern&lt;/em&gt; instead of the class, you can solve massive architectural headaches in less than a hundred lines of code.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>aspnetcore</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
