<?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: dotnet</title>
    <description>The latest articles tagged 'dotnet' on DEV Community.</description>
    <link>https://dev.to/t/dotnet</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/dotnet"/>
    <language>en</language>
    <item>
      <title>Any Language, One Store: How Zaris Speaks the Redis Protocol</title>
      <dc:creator>Zero Heartbeat</dc:creator>
      <pubDate>Thu, 27 Aug 2026 09:23:14 +0000</pubDate>
      <link>https://dev.to/zero_heartbeat_06a3625d7a/any-language-one-store-how-zaris-speaks-the-redis-protocol-2i89</link>
      <guid>https://dev.to/zero_heartbeat_06a3625d7a/any-language-one-store-how-zaris-speaks-the-redis-protocol-2i89</guid>
      <description>&lt;p&gt;Zaris is a distributed key/value store built for .NET, with its own binary protocol and native .NET clients. With 2.0.0, every Zaris node can also expose a Redis-protocol (RESP2) listener — so existing Redis clients, in any language, can connect to it without code changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why put a Redis face on a .NET store
&lt;/h2&gt;

&lt;p&gt;Redis has the widest client-library support of any key-value protocol. Adding RESP compatibility means polyglot teams get one replicated, highly-available store that every service can talk to today, regardless of language. The RESP front-end is opt-in and disabled by default per node.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core hash is Redis-slot-aligned
&lt;/h2&gt;

&lt;p&gt;Zaris uses the same key hashing Redis Cluster uses: &lt;code&gt;CRC16(key) % 16384&lt;/code&gt;. That means cluster-aware Redis clients can route straight to the owning node, with no proxy in between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;redis client ──CRC16(key) % 16384 → slot → owner ──▶ node that holds the key ──▶ reply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Collections use a whole-value model
&lt;/h2&gt;

&lt;p&gt;Native data structures (hashes, lists, sets, sorted sets) go through read-modify-write with compare-and-swap semantics, so concurrent writes don't conflict across replicas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions run on real 2PC, not a simulation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MULTI&lt;/code&gt;/&lt;code&gt;EXEC&lt;/code&gt; executes on Zaris's native two-phase-commit transaction machinery rather than faking it — counter commands like &lt;code&gt;INCR&lt;/code&gt;/&lt;code&gt;DECR&lt;/code&gt; stay properly isolated inside a transaction block.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to fail over
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Load balancer&lt;/strong&gt; (recommended) — any node accepts any key; dead nodes just drop out of rotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentinel&lt;/strong&gt; — Zaris answers Sentinel discovery commands so clients can re-resolve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster&lt;/strong&gt; — CRC16-aligned slot topology, same as Redis Cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's in, and what's cleanly out
&lt;/h2&gt;

&lt;p&gt;Testing against StackExchange.Redis: ~126 commands supported, ~92 cleanly not supported, zero failures. Supported: core strings, collections, Streams with consumer groups, &lt;code&gt;MULTI&lt;/code&gt;/&lt;code&gt;EXEC&lt;/code&gt;/&lt;code&gt;WATCH&lt;/code&gt;, pub/sub. Not supported, by design: Lua scripting, geo commands, HyperLogLog, persistence commands — these assume infrastructure Zaris doesn't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin your client versions
&lt;/h2&gt;

&lt;p&gt;The listener speaks RESP2 only. Pin your Redis client library version rather than floating it, so you don't get an unexpected negotiation attempt at a newer protocol version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli &lt;span class="nt"&gt;-h&lt;/span&gt; node-a.internal &lt;span class="nt"&gt;-p&lt;/span&gt; 6379 SET user:42 &lt;span class="s2"&gt;"ada"&lt;/span&gt;
redis-cli &lt;span class="nt"&gt;-h&lt;/span&gt; node-a.internal &lt;span class="nt"&gt;-p&lt;/span&gt; 6379 GET user:42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node-a.internal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user:42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ada&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user:42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// StackExchange.Redis, unchanged&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConnectionMultiplexer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"node-a.internal:6379"&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;db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDatabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StringSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user:42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ada"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full RESP config, the command compatibility matrix, and migration notes are in the &lt;a href="https://clustron.io/blog/redis-protocol-any-language/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>redis</category>
      <category>showdev</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Should Your Prompt Store Pick Your Model</title>
      <dc:creator>Johnny Z</dc:creator>
      <pubDate>Thu, 27 Aug 2026 06:35:14 +0000</pubDate>
      <link>https://dev.to/stormhub/should-your-prompt-store-pick-your-model-2c4f</link>
      <guid>https://dev.to/stormhub/should-your-prompt-store-pick-your-model-2c4f</guid>
      <description>&lt;p&gt;&lt;a href="https://langfuse.com" rel="noopener noreferrer"&gt;Langfuse&lt;/a&gt; with &lt;code&gt;Microsoft.Extensions.AI&lt;/code&gt; has an appealing story: update prompts without redeploying. A prompt fetches its config blob—model, tokens, temperature—which the code passes straight to the LLM.&lt;/p&gt;

&lt;p&gt;It works. But it puts a boundary in what I'd suggest might be better placed elsewhere — and moving it is a small enough change to be worth exploring.&lt;/p&gt;

&lt;p&gt;This post is about where to move that line in a .NET codebase using &lt;code&gt;Microsoft.Extensions.AI&lt;/code&gt; against OpenAI or Azure OpenAI, with Langfuse as the source of prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the current setup buys you
&lt;/h2&gt;

&lt;p&gt;Let me be fair to it first, because the coupling is a deliberate design, not an accident.&lt;/p&gt;

&lt;p&gt;Langfuse's prompt &lt;code&gt;config&lt;/code&gt; is an optional JSON object versioned alongside the prompt. That means someone can open the Langfuse UI, change the model or a parameter, and ship it — no code change, no redeploy. Combined with labels (pointers to specific versions that your code references), a rollback is just moving the &lt;code&gt;production&lt;/code&gt; label back to an earlier version. For prompt &lt;em&gt;content&lt;/em&gt; iteration, that story is genuinely good, and there is a real audience of people who want model config coupled to prompt versions &lt;em&gt;more&lt;/em&gt; tightly so each version is fully self-describing and reproducible.&lt;/p&gt;

&lt;p&gt;So this is a trade-off, not a bug. The question is whether the thing you are optimizing for — non-engineers tuning prompts without a deploy — is worth what the coupling costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I think this deserves consideration
&lt;/h2&gt;

&lt;p&gt;Three points stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is an untyped blob feeding provider selection.&lt;/strong&gt; The Langfuse &lt;code&gt;config&lt;/code&gt; is arbitrary JSON without schema enforcement. On the other end, whatever LLM plumbing you use will treat that model string as authoritative. A missing key, a stray &lt;code&gt;max_tokens&lt;/code&gt;, or a &lt;code&gt;gpt4o&lt;/code&gt; typo might not fail at build time or deploy time — it could fail on a live request, or silently do something unintended. You have a loosely-typed value driving an infrastructure decision, and the mistake may not surface until traffic hits it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It conflates two change lifecycles with different risk profiles.&lt;/strong&gt; Prompt wording is a content decision: low risk, iterate freely. &lt;em&gt;Which model runs&lt;/em&gt; and &lt;em&gt;what the token ceiling is&lt;/em&gt; are infrastructure, cost, and reliability decisions with different review considerations. When both live in the same editable object, whoever edits prompts effectively has influence over production model selection and spend. That's considerable authority to place in a text field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment parameterization gets awkward.&lt;/strong&gt; The config travels with the prompt version. So "cheap model in dev, strong model in prod" tends to push you toward label gymnastics or separate projects, rather than a straightforward per-environment mapping that lives with your other infra config.&lt;/p&gt;

&lt;h2&gt;
  
  
  The .NET insight that makes the fix clean
&lt;/h2&gt;

&lt;p&gt;Here is the detail that makes this pleasant in &lt;code&gt;Microsoft.Extensions.AI&lt;/code&gt;: for OpenAI and Azure OpenAI, the model identity is bound when you &lt;em&gt;construct&lt;/em&gt; the client, not when you call it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="n"&gt;client&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;AzureOpenAIClient&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;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DefaultAzureCredential&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-deployment"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- model bound HERE&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIChatClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider comes from which client type you build and its endpoint. The deployment comes from &lt;code&gt;GetChatClient&lt;/code&gt;. By the time you have an &lt;code&gt;IChatClient&lt;/code&gt;, it already knows what model it is. You barely need to touch &lt;code&gt;ChatOptions.ModelId&lt;/code&gt; at all.&lt;/p&gt;

&lt;p&gt;That means model selection reduces to: &lt;em&gt;pick the right pre-built client.&lt;/em&gt; And "pick a thing by name" is exactly what keyed dependency injection is for. So the plan is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Langfuse&lt;/strong&gt; owns the prompt template, its variables, and a &lt;em&gt;logical alias&lt;/em&gt; like &lt;code&gt;extractor-strong&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your app&lt;/strong&gt; owns a typed, environment-aware registry mapping each alias to a concrete client plus guardrails, resolved through keyed DI and validated at startup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt says &lt;em&gt;what&lt;/em&gt; it wants done. Your infra config says &lt;em&gt;which deployment and token budget&lt;/em&gt; executes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The registry: engineering-owned, typed, env-aware
&lt;/h2&gt;

&lt;p&gt;Model wiring goes in &lt;code&gt;appsettings.{Environment}.json&lt;/code&gt;, so the same aliases map to different deployments per environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// appsettings.Production.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ModelRegistry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"extractor-strong"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AzureOpenAI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://my-res.openai.azure.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Deployment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4o-prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MaxOutputTokensCeiling"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DefaultTemperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"summarizer-cheap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AzureOpenAI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://my-res.openai.azure.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Deployment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4o-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MaxOutputTokensCeiling"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;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;ModelRegistryOptions&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;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Section&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ModelRegistry"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ModelDefinition&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Models&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="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;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;ModelDefinition&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&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;Provider&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;   &lt;span class="c1"&gt;// "AzureOpenAI" | "OpenAI"&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Endpoint&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="c1"&gt;// Azure only&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Required&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;Deployment&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;!;&lt;/span&gt;  &lt;span class="c1"&gt;// Azure deployment or OpenAI model id&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;200_000&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;MaxOutputTokensCeiling&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="m"&gt;4096&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;float&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;DefaultTemperature&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Register one keyed client per alias, and fail fast
&lt;/h2&gt;

&lt;p&gt;Each alias becomes a keyed &lt;code&gt;IChatClient&lt;/code&gt; with its own middleware pipeline. Validation runs at startup, so a broken registry fails the deploy rather than the first request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelRegistryOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ModelRegistryOptions&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;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ModelRegistryOptions&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;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelRegistryOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"No models configured"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Provider&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="s"&gt;"AzureOpenAI"&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="s"&gt;"AzureOpenAI models require an Endpoint"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateOnStart&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddKeyedChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;BuildInner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOpenTelemetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;     &lt;span class="c1"&gt;// per-model pipeline: telemetry, caching, retries…&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseLogging&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="nf"&gt;BuildInner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelDefinition&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Provider&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"AzureOpenAI"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzureOpenAIClient&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;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&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;DefaultAzureCredential&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;// or ApiKeyCredential&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// model identity bound here&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIChatClient&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s"&gt;"OpenAI"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OPENAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)!)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIChatClient&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;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Unknown provider &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Provider&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;Because &lt;code&gt;AddKeyedChatClient&lt;/code&gt; returns a &lt;code&gt;ChatClientBuilder&lt;/code&gt;, you can attach caching, retries, rate limiting, and OpenTelemetry per model — centrally, once, instead of scattering it across call sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The validation boundary: the actual fix
&lt;/h2&gt;

&lt;p&gt;This is the part that addresses the core complaint. Whatever comes back from Langfuse gets parsed &lt;em&gt;once&lt;/em&gt; into a typed spec. Unknown aliases are rejected. Requested generation parameters are clamped to the registry's ceiling. Nothing free-form reaches the provider.&lt;/p&gt;

&lt;p&gt;Note what the prompt spec deliberately does &lt;strong&gt;not&lt;/strong&gt; contain: no provider, no endpoint, no deployment. A prompt may &lt;em&gt;request&lt;/em&gt; a temperature or a token budget; it may not choose infrastructure.&lt;br&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;// Fetching from Langfuse is a plain HTTP GET — no SDK, no magic.&lt;/span&gt;
&lt;span class="c1"&gt;// Whatever you use, hand this boundary the raw template + config dictionary.&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;PromptSpec&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;ModelAlias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;MaxOutputTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="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;ResolvedPrompt&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;required&lt;/span&gt; &lt;span class="n"&gt;IChatClient&lt;/span&gt; &lt;span class="n"&gt;Client&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;required&lt;/span&gt; &lt;span class="n"&gt;ChatOptions&lt;/span&gt; &lt;span class="n"&gt;Options&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;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Template&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="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;PromptResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IServiceProvider&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;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ModelRegistryOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;registry&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;ResolvedPrompt&lt;/span&gt; &lt;span class="nf"&gt;Resolve&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;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Parse the loose blob into something typed — fail loudly, not at call time.&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;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"modelAlias"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;aliasObj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;aliasObj&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="k"&gt;alias&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;PromptConfigException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"prompt config missing 'modelAlias'"&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;registry&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;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;def&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;PromptConfigException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"unknown model alias '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// typo caught here&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Clamp requested params to the registry guardrails.&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;requested&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"maxOutputTokens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToInt32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxTokens&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requested&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxOutputTokensCeiling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxOutputTokensCeiling&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToSingle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultTemperature&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="n"&gt;ResolvedPrompt&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Client&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;GetRequiredKeyedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IChatClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;// model already bound&lt;/span&gt;
            &lt;span class="n"&gt;Options&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;ChatOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;MaxOutputTokens&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Temperature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;Template&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;template&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;h2&gt;
  
  
  4. The call site
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prompt&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;_prompts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invoice-extractor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vars&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;response&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;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetResponseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole flow. Alias in, correct pre-built client out, guardrailed options attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who owns what now
&lt;/h2&gt;

&lt;p&gt;Langfuse keeps the prompt template, its variables, and a logical &lt;code&gt;modelAlias&lt;/code&gt;. If you want, it can also carry &lt;code&gt;temperature&lt;/code&gt; and &lt;code&gt;maxOutputTokens&lt;/code&gt; — but as &lt;em&gt;requests&lt;/em&gt; that the boundary clamps, not as commands. Non-engineers can still iterate freely and use the Playground. What they can no longer do is pin a raw provider deployment or exceed a token ceiling, because provider, endpoint, deployment, and hard limits live in your environment-specific &lt;code&gt;ModelRegistry&lt;/code&gt;: under code review, resolved through keyed DI, validated at startup.&lt;/p&gt;

&lt;p&gt;The trade-off is a genuine one. You give up some of Langfuse's "one object fully describes the run" reproducibility. In exchange, you get fail-at-startup instead of fail-on-request, environment-parameterized model selection you didn't have before, and a clearer separation between a content decision and an infrastructure one.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you only do one thing
&lt;/h2&gt;

&lt;p&gt;Add the validation boundary in step 3, even if you defer the keyed-registry refactor. Parsing the untyped blob into a typed spec and rejecting unknown aliases is the highest-leverage, lowest-effort change here — it turns a class of production-time failures into a single, obvious throw. The keyed registry is where you want to end up; the boundary is what you can ship this afternoon.&lt;/p&gt;

&lt;p&gt;A nice follow-up, once the registry exists: a startup check that every &lt;code&gt;modelAlias&lt;/code&gt; referenced by your live production prompts actually resolves in the registry, so a missing mapping fails the deploy rather than the first customer request. But start with the boundary. Your prompt store is a good place to manage prompts. It might be worth reconsidering whether it should also dictate which GPU your requests use.&lt;/p&gt;

&lt;p&gt;Please feel free to reach out on twitter &lt;a href="https://twitter.com/roamingcode" rel="noopener noreferrer"&gt;@roamingcode&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>dotnet</category>
      <category>openai</category>
    </item>
    <item>
      <title>Counts Match. The Data Can Still Be Wrong</title>
      <dc:creator>Ivan Rossouw</dc:creator>
      <pubDate>Thu, 27 Aug 2026 06:26:55 +0000</pubDate>
      <link>https://dev.to/iqtechsolutions/counts-match-the-data-can-still-be-wrong-5cm2</link>
      <guid>https://dev.to/iqtechsolutions/counts-match-the-data-can-still-be-wrong-5cm2</guid>
      <description>&lt;p&gt;Many systems eventually need to repartition one persisted record. A batch becomes several processing groups. A container becomes several parcels. A work queue becomes several assignments.&lt;/p&gt;

&lt;p&gt;The domain changes, but the risk is consistent: every child must move from one source into exactly one result without being invented, duplicated, or lost.&lt;/p&gt;

&lt;p&gt;The tempting safeguard is a count comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The source contains twelve items.&lt;/li&gt;
&lt;li&gt;The proposed results contain twelve items altogether.&lt;/li&gt;
&lt;li&gt;Therefore, the split must be valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unfortunately, equal counts prove only cardinality. They do not prove identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind spot in a passing count
&lt;/h2&gt;

&lt;p&gt;Imagine that a source contains the item IDs &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, and &lt;code&gt;C&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A proposed split contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Result one: &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;A&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Result two: &lt;code&gt;C&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source and outputs both contain three entries. A count check passes, even though &lt;code&gt;A&lt;/code&gt; was duplicated and &lt;code&gt;B&lt;/code&gt; disappeared.&lt;/p&gt;

&lt;p&gt;Another invalid proposal might contain &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;B&lt;/code&gt;, and &lt;code&gt;D&lt;/code&gt;. Again, the count matches, but an unknown item has replaced a valid one.&lt;/p&gt;

&lt;p&gt;These are not arithmetic failures. They are failures of identity conservation.&lt;/p&gt;

&lt;h2&gt;
  
  
  State the stronger invariant
&lt;/h2&gt;

&lt;p&gt;For a valid repartition, the original identity set must equal the disjoint union of the output identity sets.&lt;/p&gt;

&lt;p&gt;That gives us three useful requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every original ID appears in the outputs.&lt;/li&gt;
&lt;li&gt;No unknown ID appears in the outputs.&lt;/li&gt;
&lt;li&gt;No ID appears more than once.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Set equality proves membership. A duplicate check proves disjointness. Cardinality remains useful, but only as one part of the invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before mutation
&lt;/h2&gt;

&lt;p&gt;A generalized C# guard can make that rule explicit:&lt;br&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;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EnsureExactPartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;originalIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;partitions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;originalIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToHashSet&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;output&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToArray&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;sourceHasDuplicates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;originalIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;outputHasDuplicates&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Distinct&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourceHasDuplicates&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
        &lt;span class="n"&gt;outputHasDuplicates&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;original&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="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"The requested split is not an exact partition."&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;This example is deliberately small. In a real application, authorization, lifecycle state, concurrency, and ownership rules may add further checks. The important ordering is that the complete proposal is validated before tracked entities or database state are changed.&lt;/p&gt;

&lt;p&gt;That ordering makes failure cheap. An invalid request becomes a rejected request, not a cleanup exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move tracked children instead of copying them
&lt;/h2&gt;

&lt;p&gt;When the children already exist as tracked entities, repartitioning should preserve their identities.&lt;/p&gt;

&lt;p&gt;Creating new child objects by copying fields can accidentally turn a move into duplication. It may also lose historical references, introduce new primary keys, or leave the originals attached to the source.&lt;/p&gt;

&lt;p&gt;Instead, load the authorized source and its children, resolve each requested ID to the existing tracked instance, and re-parent that instance to its destination. With EF Core, this usually means updating the relationship through the navigation property or foreign key and allowing relationship fix-up to track the move.&lt;/p&gt;

&lt;p&gt;The output records are new. The children are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the whole transition atomic
&lt;/h2&gt;

&lt;p&gt;A valid split still should not become partially visible.&lt;/p&gt;

&lt;p&gt;Creating two results, failing on the third, and leaving the source marked as processed produces a state that is difficult to reason about and harder to retry safely.&lt;/p&gt;

&lt;p&gt;Treat these changes as one unit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create every result.&lt;/li&gt;
&lt;li&gt;Move every child.&lt;/li&gt;
&lt;li&gt;Transition the source record.&lt;/li&gt;
&lt;li&gt;Save the complete state atomically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single &lt;code&gt;SaveChanges&lt;/code&gt; call is transactional for supported relational providers. If the workflow requires multiple saves or coordinates additional durable work, use an explicit transaction or an outbox-style boundary.&lt;/p&gt;

&lt;p&gt;The desired outcome is simple: observers see either the original state or the complete repartitioned state, never a mixture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every result an idempotency scope
&lt;/h2&gt;

&lt;p&gt;Operation-level idempotency is helpful, but a split produces several distinct results. Each result therefore needs a stable scope of its own.&lt;/p&gt;

&lt;p&gt;A compound uniqueness boundary such as &lt;code&gt;(operationId, stableResultKey)&lt;/code&gt; lets a retry identify each intended result independently. The result key should come from stable input or deterministic content, not an array position whose meaning may change when ordering changes.&lt;/p&gt;

&lt;p&gt;A database uniqueness constraint should enforce this promise. Application checks improve error messages; the constraint protects correctness under concurrency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-off is intentional friction
&lt;/h2&gt;

&lt;p&gt;Exact set validation allocates collections. Transactions hold resources. Unique indexes add storage and write cost. The implementation is more involved than comparing two integers.&lt;/p&gt;

&lt;p&gt;For very large partitions, validation may need to move closer to the database or use streaming and batching. That changes the mechanism, not the invariant.&lt;/p&gt;

&lt;p&gt;The additional friction buys something valuable: failures occur before mutation, concurrent retries converge, and the persisted model remains explainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that expose the boundary
&lt;/h2&gt;

&lt;p&gt;A compact test suite should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A valid partition in a different order&lt;/li&gt;
&lt;li&gt;One duplicated ID&lt;/li&gt;
&lt;li&gt;One omitted ID&lt;/li&gt;
&lt;li&gt;One foreign ID&lt;/li&gt;
&lt;li&gt;A retry using the same per-result scopes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The negative cases should fail before any entity is re-parented or source state is changed.&lt;/p&gt;

&lt;p&gt;The practical lesson is modest: whenever one persisted record becomes many, ask more than, “Did the counts match?”&lt;/p&gt;

&lt;p&gt;Ask, “Did every identity move exactly once?”&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
      <category>testing</category>
    </item>
    <item>
      <title>Still Using Task.Run() in ASP.NET Core? Fix Your C# Async Await the Right Way</title>
      <dc:creator>qodors</dc:creator>
      <pubDate>Thu, 27 Aug 2026 04:50:14 +0000</pubDate>
      <link>https://dev.to/qodors/still-using-taskrun-in-aspnet-core-fix-your-c-async-await-the-right-way-4bel</link>
      <guid>https://dev.to/qodors/still-using-taskrun-in-aspnet-core-fix-your-c-async-await-the-right-way-4bel</guid>
      <description>&lt;p&gt;Your ASP.NET Core API is slow, so you add Task.Run() and expect things to get better.&lt;/p&gt;

&lt;p&gt;It may look like a quick fix, but Task.Run() does not make every operation faster. In many cases, it only moves the same work to a thread-pool thread. For database queries, HTTP requests, and other I/O work, this does not solve the actual problem.&lt;/p&gt;

&lt;p&gt;This is why it is important to understand how async and await work in your code. The goal is not to make every method async. It is to avoid blocking a thread while the application waits for an operation to finish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Do async and await Actually Do?&lt;/strong&gt;&lt;br&gt;
Many developers think async creates a new thread, but it does not create a new thread by itself.&lt;/p&gt;

&lt;p&gt;When an ASP.NET Core endpoint waits for a database query, it does not need to keep a thread busy while waiting for the response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetProducts&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;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&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 database may take some time to return the products, but the application does not need to keep a thread busy during that time. Once the database operation finishes, the method continues from await.&lt;/p&gt;

&lt;p&gt;This allows an ASP.NET Core application to handle requests without keeping a thread busy during the wait.&lt;/p&gt;

&lt;p&gt;The important thing is to understand what the code is waiting for, not just whether it uses await.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Task.Run() Is Often the Wrong Fix&lt;/strong&gt;&lt;br&gt;
Consider this code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetProducts&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;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&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;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&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;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&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;Although the code uses await, the database call itself is still synchronous. Task.Run() only moves ToList() to a thread-pool thread. That thread stays busy while the synchronous database call runs.&lt;/p&gt;

&lt;p&gt;For normal I/O work in ASP.NET Core, there is usually no reason to do this. Instead, use the async method provided by the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetProducts&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;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&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 better approach is to call the database's async method directly. This avoids wrapping the synchronous call in Task.Run(). Look at the operation behind await instead of adding Task.Run() just because a method looks slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Async for I/O Work&lt;/strong&gt;&lt;br&gt;
A web API often spends a lot of time waiting for other systems. It may query a database, call another API, read a file, or work with cloud storage. In these situations, an async API lets the application wait without keeping a thread busy.&lt;/p&gt;

&lt;p&gt;Common I/O operations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database queries&lt;/li&gt;
&lt;li&gt;HTTP requests&lt;/li&gt;
&lt;li&gt;File operations&lt;/li&gt;
&lt;li&gt;Network calls&lt;/li&gt;
&lt;li&gt;Cloud storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An HTTP request can be handled directly with the async API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&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;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to put the HTTP call inside Task.Run().&lt;/p&gt;

&lt;p&gt;The same applies to Entity Framework Core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application can wait for the database response without blocking a thread.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What About CPU-Heavy Work?&lt;/strong&gt;&lt;br&gt;
CPU-heavy work is different because the application is not waiting for another system. The CPU is actively doing the work.&lt;/p&gt;

&lt;p&gt;For instance:&lt;br&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="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;GenerateReport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Expensive CPU calculation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In some situations, you may choose to move this work to another thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GenerateReportAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GenerateReport&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;However, Task.Run() does not make the calculation itself faster. The CPU still has to perform the same amount of work.&lt;/p&gt;

&lt;p&gt;For CPU-heavy work, Task.Run() can be useful when you intentionally want to move that work to another thread. It should not be used as a general solution for database queries or HTTP requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid .Result and .Wait() in Async Code&lt;/strong&gt;&lt;br&gt;
Another common issue appears when an async method is called but the result is then requested synchronously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetDataAsync&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same problem can happen with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;GetDataAsync&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method may be asynchronous, but .Result and .Wait() block while waiting for it to finish. Instead, let the async operation continue with await:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&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;GetDataAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the code easier to follow and avoids unnecessary blocking.&lt;/p&gt;

&lt;p&gt;A simple rule is: if you have an async operation, use await instead of waiting for it synchronously with .Result or .Wait().&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do You Really Need async Here?&lt;/strong&gt;&lt;br&gt;
Another important point is that not every method needs to be asynchronous.&lt;/p&gt;

&lt;p&gt;Consider 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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Add&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;a&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;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&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;There is no real async work happening here. The method is only adding two numbers, so making it async adds unnecessary code.&lt;/p&gt;

&lt;p&gt;A simple method is better:&lt;br&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="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Add&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;a&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;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use async when the method has something meaningful to wait for, such as a database query, HTTP request, or file operation. For simple calculations that finish immediately, a normal synchronous method is usually clearer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the Async Flow Going&lt;/strong&gt;&lt;br&gt;
An ASP.NET Core application often has several layers:&lt;/p&gt;

&lt;p&gt;Controller&lt;br&gt;
    ↓&lt;br&gt;
Service&lt;br&gt;
    ↓&lt;br&gt;
Repository&lt;br&gt;
    ↓&lt;br&gt;
Database &lt;/p&gt;

&lt;p&gt;If the database operation is async, keep that async flow through the application layers rather than turning it into a synchronous call somewhere in the middle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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="nf"&gt;GetUserAsync&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;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service can await the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&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;_userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUserAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller can then return the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try not to call an async method and then use .Result or .Wait() in another layer. Keeping the async flow consistent makes the code easier to maintain and avoids unnecessary blocking in an ASP.NET Core application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find Out Why the API Is Slow&lt;/strong&gt;&lt;br&gt;
If your ASP.NET Core API is slow, adding Task.Run() should not be the first thing you try. First, find out where the time is actually going.&lt;/p&gt;

&lt;p&gt;Check things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database query time&lt;/li&gt;
&lt;li&gt;External API response time&lt;/li&gt;
&lt;li&gt;CPU usage&lt;/li&gt;
&lt;li&gt;Thread-pool usage&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;li&gt;Garbage collection&lt;/li&gt;
&lt;li&gt;Number of requests&lt;/li&gt;
&lt;li&gt;Slow synchronous code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SQL query may be slow, an external API may take two seconds to respond, or another part of the application may be using too much CPU.&lt;/p&gt;

&lt;p&gt;Find the actual problem before changing the async code. Async code can handle waiting better, but it cannot make a slow database query run faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Take&lt;/strong&gt;&lt;br&gt;
At &lt;a href="https://www.qodors.com/?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=csharp_async_await" rel="noopener noreferrer"&gt;Qodors&lt;/a&gt;, we often see Task.Run() added when a method is slow or when a developer wants to make synchronous code async. A common case is a database call being wrapped in Task.Run() even though the database library already provides an async method.&lt;/p&gt;

&lt;p&gt;Moving the synchronous call to another thread does not fix the database operation. It only changes where that work runs.&lt;/p&gt;

&lt;p&gt;For normal ASP.NET Core API work, use the async methods provided by the library. With Entity Framework Core, that means methods such as ToListAsync() and FirstOrDefaultAsync(). For HTTP calls, use the async methods available in HttpClient.&lt;/p&gt;

&lt;p&gt;Before adding Task.Run(), first ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Am I doing CPU-heavy work, or am I waiting for I/O?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the application is waiting for I/O, use the proper async API. If the work is CPU-heavy, Task.Run() may be useful when there is a clear reason to move that work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Reference&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;async does not create a new thread by itself.&lt;/li&gt;
&lt;li&gt;Use await for async operations.&lt;/li&gt;
&lt;li&gt;Do not wrap normal database calls in Task.Run().&lt;/li&gt;
&lt;li&gt;Use EF Core methods such as ToListAsync() and FirstOrDefaultAsync().&lt;/li&gt;
&lt;li&gt;Avoid .Result and .Wait() in async code.&lt;/li&gt;
&lt;li&gt;Do not make every small method async.&lt;/li&gt;
&lt;li&gt;Task.Run() does not make CPU work faster.&lt;/li&gt;
&lt;li&gt;Keep async calls going through your application layers.&lt;/li&gt;
&lt;li&gt;Find the real performance problem before changing your code.&lt;/li&gt;
&lt;li&gt;Use Task.Run() only when there is a clear reason for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't add Task.Run() just because an ASP.NET Core API feels slow. First find out what the application is waiting for, use async APIs for database and HTTP work, and avoid blocking calls such as .Result and .Wait(). When the work is CPU-heavy, use Task.Run() only when it actually fits the situation.&lt;/p&gt;

&lt;h1&gt;
  
  
  CSharp #DotNet #ASPNetCore #AsyncAwait #CSharpProgramming #DotNetCore #WebAPI #API #Backend #QodorsEdge
&lt;/h1&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>How to Create and Update a Chart with C# and .NET</title>
      <dc:creator>Marko Marks</dc:creator>
      <pubDate>Thu, 27 Aug 2026 03:55:15 +0000</pubDate>
      <link>https://dev.to/marko_marks_9d92a3458102c/how-to-create-and-update-a-chart-with-c-and-net-1ebm</link>
      <guid>https://dev.to/marko_marks_9d92a3458102c/how-to-create-and-update-a-chart-with-c-and-net-1ebm</guid>
      <description>&lt;p&gt;This tutorial uses PlotMarks, an API-based charting service I'm building, for creating, hosting, and updating the chart.&lt;/p&gt;

&lt;p&gt;Push metrics from a C# .NET service to a persistent embeddable chart using HttpClient. No SDK or NuGet packages, just what ships with .NET 8.&lt;/p&gt;

&lt;p&gt;Many backend systems are built with .NET. If you have a service that tracks metrics: request counts, error rates, queue depths, or any time-series number, and you want to publish that data as a chart without building a frontend, you can do it with a few lines of &lt;code&gt;HttpClient&lt;/code&gt; code.&lt;/p&gt;

&lt;p&gt;This article shows how to create a persistent chart slot on &lt;a href="https://www.plotmarks.com" rel="noopener noreferrer"&gt;PlotMarks&lt;/a&gt; and push data to it from a .NET application. The integration requires no SDK or NuGet package beyond what ships with the framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;An ASP.NET Core service counts incoming API requests per hour and publishes the current day's totals as an embedded bar chart. The chart is used on an internal status page. The service pushes a fresh dataset each hour; the page shows the latest numbers when someone opens it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7zhtyml6gzsz3byw5wb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7zhtyml6gzsz3byw5wb.png" alt="Architecture diagram: one-time setup posts to PlotMarks to get a chart ID; the ASP.NET Core service posts data hourly; the status page embeds the same URL" width="799" height="75"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chart creation is a one-time setup step, usually done manually from a console app or a startup routine. Data pushes happen on the production schedule, hourly in this case.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;.NET 8 or later&lt;/li&gt;
&lt;li&gt;A PlotMarks account and API key &lt;a href="https://www.plotmarks.com/sign-up" rel="noopener noreferrer"&gt;sign up free at plotmarks.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PLOTMARKS_API_KEY&lt;/code&gt; set as an environment variable or app secret&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No additional NuGet packages are required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1. Create a chart slot
&lt;/h2&gt;

&lt;p&gt;Run this once to provision the chart. You can place it in a &lt;code&gt;Program.cs&lt;/code&gt; setup block, a one-time migration tool, or a simple console app. Save the chart ID that is returned.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;// Typed models ───────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ChartConfig&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;title&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;xLabel&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;yLabel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateChartRequest&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;type&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;output_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ChartConfig&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateChartResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;embedUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Setup ──────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;BaseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://www.plotmarks.com"&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;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PLOTMARKS_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PLOTMARKS_API_KEY is not set"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-API-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create chart slot ──────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;createBody&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;CreateChartRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"bar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"static_iframe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChartConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"API Requests — Today"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;xLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Hour"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;yLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Requests"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;createResponse&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/charts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;createBody&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;createResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&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;chart&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;createResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFromJsonAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateChartResponse&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Empty response"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Chart ID : &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Embed URL: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedUrl&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;&lt;code&gt;output_type: "static_iframe"&lt;/code&gt; creates a chart that shows the latest pushed dataset when a viewer loads the page. No in-page auto-refresh. This is appropriate here because the page is loaded on demand and hourly data does not need to update while someone has the page open.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2. Push data
&lt;/h2&gt;

&lt;p&gt;Typically called from the part of your service that runs on the hourly schedule:&lt;br&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;// Push hourly request counts ─────────────────────────────────────────────&lt;/span&gt;

&lt;span class="c1"&gt;// In production this comes from your metrics store.&lt;/span&gt;
&lt;span class="c1"&gt;// Keys are hour labels; values are request counts.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hourlyCounts&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;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"00:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"01:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;298&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"02:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;187&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"03:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;143&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"04:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"05:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;334&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"06:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;589&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"07:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;847&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"08:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1204&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"09:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1531&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"10:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1688&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"11:00"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1743&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;dataPoints&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hourlyCounts&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kvp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Requests"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kvp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pushBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;plots&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#4F46E5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;borderRadius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataPoints&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;chartId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ch_abc123"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// from the create step above&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;pushResponse&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/charts/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chartId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pushBody&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pushResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Data pushed successfully"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each call to &lt;code&gt;POST /api/charts/{id}/data&lt;/code&gt; replaces the chart's entire dataset. There is no delta or append mode. Pushing a new dataset with 12 data points replaces the previous one, the chart always shows exactly what was in the most recent push.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3. Embed the chart
&lt;/h2&gt;

&lt;p&gt;The embed URL returned during chart creation is public. No login or API key is needed to view it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://www.plotmarks.com/charts/ch_abc123"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"640"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"360"&lt;/span&gt;
  &lt;span class="na"&gt;frameborder=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"border-radius: 8px"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmeawpkyv3i6hiyxkk7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmeawpkyv3i6hiyxkk7n.png" alt="PlotMarks bar chart showing hourly API request counts from 00:00 to 11:00" width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Making it a background service
&lt;/h2&gt;

&lt;p&gt;If you want the push to happen automatically on a schedule inside the service itself, wrap it in a &lt;code&gt;BackgroundService&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwtwvw8gq7u68sh3vmva.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwtwvw8gq7u68sh3vmva.png" alt="Sequence diagram: BackgroundService queries the metrics store each hour, then posts the data to PlotMarks" width="679" height="506"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HourlyChartUpdater&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundService&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;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_chartId&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;HourlyChartUpdater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_http&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plotmarks"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_chartId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"PlotMarks:ChartId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PlotMarks:ChartId is not configured"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&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;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;PushCurrentHourlyData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;// Wait until the next hour boundary&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nextHour&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="nf"&gt;AddHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHours&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="nf"&gt;AddHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextHour&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="n"&gt;stoppingToken&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;PushCurrentHourlyData&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="c1"&gt;// Retrieve from your metrics store&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;counts&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;GetHourlyCountsFromMetricsStore&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;pushBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;plots&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#4F46E5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;borderRadius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Requests"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&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="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;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;res&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;_http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"/api/charts/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_chartId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;pushBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ct&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Hour&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;Count&lt;/span&gt;&lt;span class="p"&gt;)&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetHourlyCountsFromMetricsStore&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="c1"&gt;// Replace with your actual data source&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;NotImplementedException&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;Register it in &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="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;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plotmarks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&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;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://www.plotmarks.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-API-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"PlotMarks:ApiKey"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HourlyChartUpdater&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Switching to a live chart
&lt;/h2&gt;

&lt;p&gt;If the background service is pushing data automatically, you may also want the chart to refresh in the browser without requiring a page reload. Switch &lt;code&gt;output_type&lt;/code&gt; to &lt;code&gt;"live_iframe"&lt;/code&gt; when creating the chart, for example if it is displayed on a TV screen or a monitoring wall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;createBody&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;CreateChartRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"bar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"live_iframe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChartConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"API Requests Today"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;xLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Hour"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;yLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Requests"&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 data push code and the embed HTML stay exactly the same. The browser-side polling is handled by the embedded chart page automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnv2upsguollkeaypk8ca.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnv2upsguollkeaypk8ca.gif" alt="A live PlotMarks chart updating in place as new data is pushed, with no page reload" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What PlotMarks handles here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Storing the current dataset&lt;/li&gt;
&lt;li&gt;Serving the chart as an embeddable page&lt;/li&gt;
&lt;li&gt;Rendering the chart in the browser&lt;/li&gt;
&lt;li&gt;Polling logic for live charts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The .NET code is two POST requests. PlotMarks removes the need for a hosted frontend that accepts data from the service and renders a chart from it.&lt;/p&gt;




</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Grand Theft Auto VI (GTA 6): The Next Revolution in Open-World Gaming</title>
      <dc:creator>Ahmad Linkbuilder</dc:creator>
      <pubDate>Wed, 26 Aug 2026 22:26:39 +0000</pubDate>
      <link>https://dev.to/ahmad_linkbuilder_20efeb8/grand-theft-auto-vi-gta-6-the-next-revolution-in-open-world-gaming-4gg7</link>
      <guid>https://dev.to/ahmad_linkbuilder_20efeb8/grand-theft-auto-vi-gta-6-the-next-revolution-in-open-world-gaming-4gg7</guid>
      <description>&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Grand_Theft_Auto_VI" rel="noopener noreferrer"&gt;Grand Theft Auto VI&lt;/a&gt; (GTA 6) has become the most anticipated and hyped title in the history of the video game industry. Rockstar Games has consistently redefined standards for open-world gaming with every major release. Following the monumental success of GTA 5 and over a decade of waiting, GTA 6 is positioned to usher in a new era for interactive entertainment.&lt;/p&gt;

&lt;p&gt;This article provides an in-depth breakdown of GTA 6, detailing its release timeline, storyline, playable characters, world design, gameplay mechanics, and technical innovations.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Release Date and Launch Platforms
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Rockstar_Games" rel="noopener noreferrer"&gt;Rockstar Games&lt;/a&gt; has scheduled the official release of GTA 6 for November 19, 2026. The initial launch will focus on current-generation hardware.&lt;/p&gt;

&lt;p&gt;Console Launch: The game will launch exclusively on next-generation consoles, specifically the PlayStation 5 and Xbox Series X|S.&lt;/p&gt;

&lt;p&gt;PC Version: Following Rockstar's historical release patterns, a PC port is expected to follow after the initial console launch window.&lt;/p&gt;

&lt;p&gt;Digital Pre-Orders: Digital pre-orders and pre-loading options will open ahead of the launch date to enable day-one playability without delays.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. World Setting: The State of Leonida and Vice City
&lt;/h2&gt;

&lt;p&gt;GTA 6 takes place within the State of Leonida, a fictional depiction modeled heavily after modern-day Florida. The centerpiece of this expansive region is the iconic, neon-lit metropolis of Vice City.&lt;/p&gt;

&lt;h3&gt;
  
  
  Map Scale and Biome Diversity
&lt;/h3&gt;

&lt;p&gt;The world map for GTA 6 is significantly larger and more detailed than GTA 5's Los Santos, featuring several distinct biomes:&lt;/p&gt;

&lt;p&gt;Vice City: A sprawling urban center filled with art deco architecture, Ocean Beach, shopping districts, and vibrant nightlife.&lt;/p&gt;

&lt;p&gt;Leonida Keys: An archipelago inspired by the Florida Keys, featuring maritime boat yards, beaches, and coastal environments.&lt;/p&gt;

&lt;p&gt;Grassrivers: Dangerous, expansive wetlands inspired by the Everglades, populated by swamps, dense vegetation, and native wildlife.&lt;/p&gt;

&lt;p&gt;Port Gellhorn &amp;amp; Ambrosia: Industrial hubs, small suburban towns, manufacturing plants, and territories controlled by local biker gangs.&lt;/p&gt;

&lt;p&gt;Mount Kalaga National Park: A northern wilderness area dominated by dense forests, elevated terrain, and rural activity centers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Protagonists and Narrative: Lucia and Jason
&lt;/h2&gt;

&lt;p&gt;The narrative of GTA 6 centers around themes of modern crime, trust, and survival. For the first time in the franchise's modern 3D era, the game introduces a playable female protagonist.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Protagonist Duo (Bonnie &amp;amp; Clyde Dynamic)
&lt;/h3&gt;

&lt;p&gt;The storyline focuses on two main characters tied together by circumstance and criminal ambition:&lt;/p&gt;

&lt;p&gt;Lucia Caminos: The franchise's first female co-protagonist. Tough, strategic, and resourceful, Lucia enters the criminal underworld to survive following her release from the Leonida Penitentiary.&lt;/p&gt;

&lt;p&gt;Jason Duval: A pragmatic operator with a military background who handles low-level operations around the Keys region.&lt;/p&gt;

&lt;p&gt;Character Dynamics: Lucia and Jason operate as both romantic partners and partners-in-crime. Following a failed heist, the duo finds themselves caught in a broader conflict across Leonida's criminal networks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supporting Cast
&lt;/h3&gt;

&lt;p&gt;The broader narrative introduces several key side characters, including Brian Heder (a local boat yard owner), Cal Hampton, Boobie Ike, and Dre'Quan Priest, all of whom influence side activities and main storyline progression.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Next-Gen Gameplay Mechanics and Physics
&lt;/h2&gt;

&lt;p&gt;Rockstar Games has rebuilt several underlying systems for GTA 6, drawing from advancements introduced in Red Dead Redemption 2 to increase structural realism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Character Switching System
&lt;/h3&gt;

&lt;p&gt;Players can seamlessly switch between Jason and Lucia during free-roam exploration or execute coordinated tactics when both characters are deployed together during heists and missions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced NPC AI and World Dynamics
&lt;/h3&gt;

&lt;p&gt;Non-Playable Characters (NPCs) feature enhanced artificial intelligence, reacting dynamically to player behavior, outfit choices, driving habits, and regional events.&lt;/p&gt;

&lt;p&gt;The law enforcement response system has been overhauled to emphasize tactical maneuvers, roadblocks, and coordinated encirclement rather than straightforward chases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Physicalized Inventory Management
&lt;/h3&gt;

&lt;p&gt;Moving away from carrying an unlimited array of weapons, players manage inventory through vehicle trunks, duffel bags, and physical holsters, adding tactical considerations to mission preparation.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Graphics, Visual Systems, and Environmental Rendering
&lt;/h2&gt;

&lt;p&gt;Powered by an updated iteration of the RAGE Engine, GTA 6 introduces significant technical enhancements:&lt;/p&gt;

&lt;p&gt;Water and Environmental Physics: Real-time fluid simulation governs ocean tides, boat wakes, waves, and weather-driven storm surges.&lt;/p&gt;

&lt;p&gt;Lighting and Ray Tracing: Advanced ray tracing handles reflection management across Vice City's glass structures, wet pavement, and neon displays.&lt;/p&gt;

&lt;p&gt;Character Fidelity: Real-time sweat mechanics, dynamic hair physics, precise physical damage modeling, and expanded facial animations enhance cinematic presentation.&lt;/p&gt;

&lt;p&gt;Ecosystem and Wildlife: Wetlands and rural environments feature diverse wildlife—including alligators, deer, marine life, and bird species—operating within an active food chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Social Media Integration and Cultural Satire
&lt;/h2&gt;

&lt;p&gt;Satire of contemporary culture remains a core pillar of the series. GTA 6 adapts this focus to reflect the modern digital landscape:&lt;/p&gt;

&lt;p&gt;In-Game Social Platforms: Short-form video networks mirroring platforms like TikTok and Instagram are integrated into the world. In-game feeds broadcast NPC footage, street events, and ambient occurrences in real time.&lt;/p&gt;

&lt;p&gt;Cultural Parodies: The game lampoons modern influencer culture, reality television trends, and tech-driven lifestyles through in-world branding and media broadcasts.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The Evolution of GTA Online
&lt;/h2&gt;

&lt;p&gt;Building upon the multi-year ecosystem of GTA 5, the multiplayer component for GTA 6 is engineered as a long-term platform.&lt;/p&gt;

&lt;p&gt;The new map of Leonida will serve as the foundation for expanded properties, business operations, and multi-stage online heists.&lt;/p&gt;

&lt;p&gt;Improved server architecture and built-in support for community-driven roleplay features are designed into the online framework from launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;GTA 6 represents more than a standard software release; it stands as a major landmark event for the entertainment industry. By combining high-fidelity graphics, structural physics, complex character dynamics, and open-world freedom, Rockstar Games aims to set a new benchmark for gaming design.&lt;/p&gt;

</description>
      <category>gta6</category>
      <category>dotnet</category>
      <category>rockstargames</category>
      <category>unity3d</category>
    </item>
    <item>
      <title>redb 3.7.1: props search up to 100x faster. An alternative to EF Core, or a companion to it</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Wed, 26 Aug 2026 17:14:21 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redb-371-props-search-up-to-100x-faster-an-alternative-to-ef-core-or-a-companion-to-it-12og</link>
      <guid>https://dev.to/rinat_kozin/redb-371-props-search-up-to-100x-faster-an-alternative-to-ef-core-or-a-companion-to-it-12og</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fke5pmwlha71aj7zlow5c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fke5pmwlha71aj7zlow5c.png" alt="redb.Core" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query cost did not depend on what you searched for: the filter sat above GROUP BY. Now the cut happens before the aggregate, on three engines, with no application code changed. Up to 100x on a date range and 5.3x on a full result set in SQL Server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is a class of performance problem you cannot see on small data and cannot miss on large. Ours looked like this: a query hunting for one rare order number among a hundred thousand objects took exactly as long as a search that found nothing at all. Selectivity did not move the clock. At all.&lt;/p&gt;

&lt;p&gt;The cause was the shape of the generated SQL. Props values live one per row, so the query first folds them into a wide row through &lt;code&gt;GROUP BY&lt;/code&gt; and only then applies the filter. The condition sat &lt;strong&gt;above&lt;/strong&gt; the aggregate, filtering the result of a fold rather than a column. No index can help there: by the time the condition runs, the engine has already read and folded every value of every object in the scheme. The trigram index on strings sat unused.&lt;/p&gt;

&lt;p&gt;3.7.1 adds a step that narrows the object set &lt;strong&gt;before&lt;/strong&gt; the aggregate runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The planner walks the filter tree and tries to express it as a condition on a single value row. A row belongs to exactly one structure, so a disjunction falls out naturally: "this is a Position row and it contains the needle, or this is a Department row and it contains the needle". That condition is spliced straight into the value scan, and the fold now receives an already narrowed set.&lt;/p&gt;

&lt;p&gt;The key property: the prefilter is built as a &lt;strong&gt;superset&lt;/strong&gt;. It may let extra objects through, it may never lose one. The authoritative filter stays exactly where it was, above &lt;code&gt;GROUP BY&lt;/code&gt;. Anything the planner cannot analyse yields no prefilter and today's behaviour unchanged, so the worst outcome is the absence of a speedup rather than a change of results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What already worked before
&lt;/h2&gt;

&lt;p&gt;Without this caveat the numbers below read wrong, so it goes here rather than in a footnote.&lt;/p&gt;

&lt;p&gt;Cutting before the aggregate has always existed in redb. Just not for props, but for the object's own fields: id, parent, creation and modification dates, name. Such a condition was spliced straight into the value scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_object&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o_src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;_objects&lt;/span&gt; &lt;span class="n"&gt;o_src&lt;/span&gt;
                     &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o_src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_id_scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In code that is &lt;code&gt;WhereRedb&lt;/code&gt;, and it is what production queries have been leaning on all along. In our own production and in redb.Identity such filters are almost everywhere: pick within a subtree, within a date range, within a set of ids, by owner. As long as objects were cut by &lt;code&gt;_objects&lt;/code&gt;, the fold received an already narrowed set and ran fast.&lt;/p&gt;

&lt;p&gt;The hole was exactly where there is nothing to cut by on &lt;code&gt;_objects&lt;/code&gt;. Then the only selective condition left was props, and props sat above the aggregate and cut nothing: one rare order number cost the same as searching for nothing. 3.7.1 closes that case, and the two cuts now compose: first by objects, then by value rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was measured
&lt;/h2&gt;

&lt;p&gt;Two shapes, both plain LINQ, no special calls. And both &lt;strong&gt;without&lt;/strong&gt; &lt;code&gt;WhereRedb&lt;/code&gt;, which is the worst case: nothing to narrow by on &lt;code&gt;_objects&lt;/code&gt;, the whole weight falls on props.&lt;/p&gt;

&lt;p&gt;The first is one search box across several fields. This is what a UI search looks like when the user has typed a word and you have to look in both the job title and the department.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Design"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Department&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Design"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByRedb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One needle, &lt;code&gt;Design&lt;/code&gt;, two fields, an &lt;code&gt;OR&lt;/code&gt; between them. In the table below this is the needle row. Before 3.7.1 that query read and folded the values of &lt;strong&gt;every&lt;/strong&gt; employee in the scheme and only then checked for "Design". Now the rows that obviously cannot match are cut on the way in.&lt;/p&gt;

&lt;p&gt;The second is a date range matching 3 288 objects out of 100 000, or 3.3%.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hired&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HireDate&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2030&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HireDate&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2031&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;All three engines seeded identically: 100 000 objects, roughly 8.4M value rows, statistics refreshed. Server-side time, best of three runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;query&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;th&gt;SQL Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;one needle across two string fields, ordered&lt;/td&gt;
&lt;td&gt;188 → &lt;strong&gt;91 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;1150 → &lt;strong&gt;311 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;55 → &lt;strong&gt;17 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the same, whole result, no paging&lt;/td&gt;
&lt;td&gt;338 → &lt;strong&gt;156 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;1201 → &lt;strong&gt;314 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;7037 → &lt;strong&gt;1327 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;date range, 3.3% of the scheme&lt;/td&gt;
&lt;td&gt;154 → &lt;strong&gt;1.5 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;17 → &lt;strong&gt;under 1 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;2 → 2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read this table as the upper bound of the gain rather than as an expectation for any query. If you have a &lt;code&gt;WhereRedb&lt;/code&gt;, you were already fast and the absolute difference will be smaller. The gain grows the less there was to narrow by on &lt;code&gt;_objects&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One caveat about what is being measured. This is query time, without materialising objects in memory. A real call adds materialisation on top, and materialisation is identical in both modes. So &lt;strong&gt;the search itself got faster by exactly the ratios above&lt;/strong&gt;, while the share of that gain in the end-to-end call depends on how many objects you pull.&lt;/p&gt;

&lt;p&gt;Which is where a second, independent lever comes in: projections. &lt;code&gt;Select&lt;/code&gt; fetches only the fields you actually need instead of the whole object. One mechanism cuts the engine's work, the other cuts the materialiser's, and they compose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three engines, three different mechanisms
&lt;/h2&gt;

&lt;p&gt;The same algebra, and every engine wins for its own reason. Worth knowing before predicting numbers on your own data.&lt;/p&gt;

&lt;p&gt;PostgreSQL engages the trigram GIN and reads far fewer rows: the string index returns 40 958 rows where the structure index returns 200 000. SQLite engages a covering partial index and stops going back to the table. SQL Server reads the same pages but saves CPU instead, because the string column is &lt;code&gt;NVARCHAR(MAX)&lt;/code&gt; and the comparison carries an explicit collation.&lt;/p&gt;

&lt;p&gt;The spread shows most clearly on the date range: a hundredfold gain on PostgreSQL, sixteenfold on SQLite, and nothing whatsoever on SQL Server.&lt;/p&gt;

&lt;p&gt;That last one is not a misfire, it is a sign that there was nothing left to cut. The condition matches 3.3% of the scheme, the query asks for a hundred rows, and the work is already bounded by the limit rather than by the size of the scheme: at that density you find a hundred matches after visiting roughly three thousand objects. Two milliseconds before, two after. Remove the limit, force a walk over the whole scheme, and the gain shows up there too: 88 ms against 65. That is a useful rule of thumb for your own data. The prefilter pays off where the engine would otherwise fold the entire scheme, and gives nothing where a limit already keeps the work small.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means in practice
&lt;/h2&gt;

&lt;p&gt;We got very close to a flat table with an index. On simple conditions, an equality or a range on one field, the query now travels an index the way it would travel an ordinary column of an ordinary table.&lt;/p&gt;

&lt;p&gt;And it keeps what a flat schema cannot have by construction. No row multiplication from joins: values fold per object in a single pass, and related collections do not turn a result set into a cartesian product you then have to collapse. No &lt;code&gt;Include&lt;/code&gt; either: the graph loads by depth rather than by naming every branch by hand. On deep graphs that lands somewhere a classic ORM built on joins and &lt;code&gt;Include&lt;/code&gt; does not reach.&lt;/p&gt;

&lt;p&gt;Hence the two-sided positioning in the title. Nobody is asking you to replace EF Core wholesale: the two live side by side in one application, on one database. The relational part, where the schema is stable and the table is flat, stays with EF, which is the right home for it. redb takes what a flat model finds hard: shifting sets of fields, heterogeneous entities inside one scheme, trees and graphs that would otherwise become a chain of &lt;code&gt;Include&lt;/code&gt; and a pile of duplicated rows. The choice is about the shape of your data, not about ideology.&lt;/p&gt;

&lt;p&gt;There is no head-to-head benchmark against EF in this article, and I am not going to invent one. What is described here are properties of the execution model, not a measurement we did not take.&lt;/p&gt;

&lt;p&gt;That measurement is coming separately. A deep dive is in preparation with code for both sides, execution plans and numbers: a simple condition against a flat indexed table, a graph several levels deep against &lt;code&gt;Include&lt;/code&gt;, collections where a join multiplies rows, and what projections do about all of it. This post is short and about one change; that one will be long and about where this model wins and where it loses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundaries, honestly
&lt;/h2&gt;

&lt;p&gt;The prefilter is opt-in through &lt;code&gt;EnablePvtPrefilter&lt;/code&gt; and off by default.&lt;/p&gt;

&lt;p&gt;Today it covers a disjunction over selective fields and a range or equality on a single field. A top-level &lt;code&gt;AND&lt;/code&gt; across different fields does not qualify: a value row belongs to one structure, and a conjunction of different fields is not expressible at row level. Filters over arrays, dictionaries, &lt;code&gt;null&lt;/code&gt; checks, cross-field comparisons and computed expressions are recognised as unanalysable and yield no prefilter.&lt;/p&gt;

&lt;p&gt;There is also a case where the prefilter had to step aside. On SQLite a query with a limit and no ordering at all streams without the prefilter and stops at the hundredth group; with it the planner switches to a multi-index OR, loses the ordering, needs a temporary B-tree and materialises everything. Measured: 8-12 ms against 388-521 ms, with no overlap between the ranges. So on SQLite, in that single shape, no prefilter is emitted. PostgreSQL knows no such trouble, and SQL Server cannot even produce the offending shape, because its paging is required to carry an &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A word on how this was verified. The invariant that results must match row for row is held by a suite of differential tests: every query runs twice, with the flag and without, over the same data, and the id sets are compared. That suite caught a defect that would otherwise have shipped: the row form cuts rows rather than objects, so an object qualifying through one branch of a disjunction lost the column values belonging to the other branches. The object set stayed intact, which is why fifteen hundred existing tests noticed nothing, while &lt;code&gt;DistinctBy&lt;/code&gt; and &lt;code&gt;OrderBy&lt;/code&gt; over such a field quietly lied. Fixed before publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;Search finally costs what you are searching for. A rare condition is now cheap instead of costing the same as a query that finds nothing.&lt;/p&gt;

&lt;p&gt;Three engines, one flag, zero changes in application code. The same LINQ, the same model, nothing to rewrite: turn it on and go.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;up to 100x&lt;/strong&gt; faster on a date range in PostgreSQL, 154 ms became one and a half;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3.7x&lt;/strong&gt; on string search in SQLite and twice as fast in PostgreSQL;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5.3x&lt;/strong&gt; on a full result set in SQL Server, where seven seconds became 1.3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And not a single row of any result changed. The prefilter is a superset by construction, and the invariant is pinned by differential tests that run every query both ways and compare id sets exactly. That is not a figure of speech but a working tool, and it is what caught the defect that would otherwise have shipped.&lt;/p&gt;

&lt;p&gt;Compose that with projections and you get two independent levers: one cuts the engine's work, the other the materialiser's. On simple conditions the query travels an index the way a flat table would, and on deep graphs you keep what a flat schema does not have: no rows multiplied by joins, no &lt;code&gt;Include&lt;/code&gt; written by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it lives
&lt;/h2&gt;

&lt;p&gt;The prefilter lives inside &lt;a href="https://github.com/redbase-app/redb" rel="noopener noreferrer"&gt;redb&lt;/a&gt; and works on all three Pro providers: PostgreSQL, SQL Server, SQLite. One flag, &lt;code&gt;EnablePvtPrefilter&lt;/code&gt; in &lt;code&gt;RedbServiceConfiguration&lt;/code&gt;, turns it on. It is a step of query compilation rather than a separate mode: the same LINQ, the same result set, the same result, the same observability. The difference is that the aggregate now receives not the whole scheme, but only what could possibly match.&lt;/p&gt;

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




&lt;p&gt;More of my writing: &lt;a href="https://redbase.app/articles" rel="noopener noreferrer"&gt;redbase.app/articles&lt;/a&gt;, and on &lt;a href="https://dev.to/rinat_kozin"&gt;dev.to&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>sql</category>
      <category>postgres</category>
      <category>sqlite</category>
    </item>
    <item>
      <title>10 EF Core Performance Mistakes That Ship to Production</title>
      <dc:creator>Majdi Zlitni</dc:creator>
      <pubDate>Wed, 26 Aug 2026 16:16:05 +0000</pubDate>
      <link>https://dev.to/majdizlitni/10-ef-core-performance-mistakes-that-ship-to-production-1gpc</link>
      <guid>https://dev.to/majdizlitni/10-ef-core-performance-mistakes-that-ship-to-production-1gpc</guid>
      <description>&lt;p&gt;If your API is fast with seed data but slows down in production, EF Core is rarely the root cause. It is usually the amplifier.&lt;/p&gt;

&lt;p&gt;Most teams do not have one catastrophic query. They have many small query-shape decisions that compound into high P95 latency, lock pressure, and unnecessary database scale-up.&lt;/p&gt;

&lt;p&gt;This article is a practical field guide to 10 EF Core performance mistakes I keep seeing in production .NET APIs, and the fixes that usually move the needle first.&lt;/p&gt;

&lt;h2&gt;
  
  
  In this post
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why this matters in production&lt;/li&gt;
&lt;li&gt;How to spot issues fast&lt;/li&gt;
&lt;li&gt;Architecture map of the anti-patterns&lt;/li&gt;
&lt;li&gt;Quick summary of all 10 mistakes&lt;/li&gt;
&lt;li&gt;Mistake 1: N+1 Queries&lt;/li&gt;
&lt;li&gt;Mistake 2: Missing AsNoTracking&lt;/li&gt;
&lt;li&gt;Mistake 3: Loading Whole Entities&lt;/li&gt;
&lt;li&gt;Mistake 4: No Pagination&lt;/li&gt;
&lt;li&gt;Mistake 5: Client-Side Evaluation&lt;/li&gt;
&lt;li&gt;Mistake 6: Tracking Read-Only Data&lt;/li&gt;
&lt;li&gt;Mistake 7: Cartesian Explosion&lt;/li&gt;
&lt;li&gt;Mistake 8: No Compiled Queries&lt;/li&gt;
&lt;li&gt;Mistake 9: Missing Indexes&lt;/li&gt;
&lt;li&gt;Mistake 10: SaveChanges in a Loop&lt;/li&gt;
&lt;li&gt;Batch ops caveat: ExecuteUpdate and ExecuteDelete&lt;/li&gt;
&lt;li&gt;Decision matrix: what to fix first&lt;/li&gt;
&lt;li&gt;What EF Core 10 improves&lt;/li&gt;
&lt;li&gt;Troubleshooting checklist&lt;/li&gt;
&lt;li&gt;Production baseline checklist&lt;/li&gt;
&lt;li&gt;Wrap-up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What counts as a performance mistake?
&lt;/h2&gt;

&lt;p&gt;An EF Core performance mistake is any data-access pattern that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;returns correct results in development&lt;/li&gt;
&lt;li&gt;passes tests&lt;/li&gt;
&lt;li&gt;but degrades badly under real data volume or concurrency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: correctness is not enough. Query shape is architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to detect issues quickly
&lt;/h2&gt;

&lt;p&gt;Before changing code, instrument first.&lt;br&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="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseNpgsql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableSensitiveDataLogging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDevelopment&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this as your first pass:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;many SELECTs per request: likely N+1&lt;/li&gt;
&lt;li&gt;huge result row count for small parent set: likely cartesian explosion&lt;/li&gt;
&lt;li&gt;read endpoints with heavy tracking snapshots: missing AsNoTracking&lt;/li&gt;
&lt;li&gt;list endpoints without LIMIT/TOP: missing pagination&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The anti-pattern map
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsa11r4dbbvc2cfq01etw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsa11r4dbbvc2cfq01etw.png" alt="EF Core anti-pattern architecture map" width="510" height="1846"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prefer scalable quality for sharing and zoom:&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The 10 mistakes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;N+1 queries&lt;/li&gt;
&lt;li&gt;Loading full entities instead of projections&lt;/li&gt;
&lt;li&gt;Missing AsNoTracking on read paths&lt;/li&gt;
&lt;li&gt;No pagination on list endpoints&lt;/li&gt;
&lt;li&gt;Client-side evaluation patterns&lt;/li&gt;
&lt;li&gt;Tracking read-only graphs&lt;/li&gt;
&lt;li&gt;Cartesian explosion from multiple collection Includes&lt;/li&gt;
&lt;li&gt;No compiled queries on hot paths&lt;/li&gt;
&lt;li&gt;Missing indexes on hot filters and joins&lt;/li&gt;
&lt;li&gt;SaveChanges inside loops&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Plus one operational caveat: ExecuteUpdate and ExecuteDelete bypass the change tracker and should be coordinated with explicit transaction boundaries when mixed with tracked changes.&lt;/p&gt;

&lt;p&gt;&lt;a id="mistake-1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1) N+1 Queries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;You load a parent set, then each navigation access triggers another database round trip.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smell
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Request count looks normal&lt;/li&gt;
&lt;li&gt;SQL command count explodes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Use projection or eager loading intentionally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OrderDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&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;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;ItemCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use projection for API contracts. Use Include when domain logic needs full related aggregates in memory.&lt;/p&gt;

&lt;p&gt;&lt;a id="mistake-2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2) Missing AsNoTracking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Tracking every entity in read paths increases memory and CPU for change detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Default to no tracking on query endpoints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Posts&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Published&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PublishedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For write paths, opt back in explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a id="mistake-3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3) Loading Whole Entities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Fetching full rows when you only need 2-3 columns inflates payload and materialization cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Project to DTOs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserListItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AvatarUrl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Projection usually improves all of these at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;less SQL payload&lt;/li&gt;
&lt;li&gt;lower materialization cost&lt;/li&gt;
&lt;li&gt;better API boundary control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="mistake-4"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4) No Pagination
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Unbounded lists eventually become accidental load tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Apply deterministic ordering and pagination.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Articles&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&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;Page&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&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;PageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always cap server-side page size to prevent accidental heavy requests.&lt;/p&gt;

&lt;p&gt;&lt;a id="mistake-5"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5) Client-Side Evaluation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Non-translatable logic forces EF Core to pull data and evaluate in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Keep filters SQL-translatable, move complex logic after narrowing rows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;filtered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Settled&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reference&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule: filter, sort, and paginate before materialization. Terminal operators like ToListAsync end translation and execute the query.&lt;/p&gt;

&lt;p&gt;&lt;a id="mistake-6"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6) Tracking Read-Only Data
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Long-running request flows with tracked entities increase memory pressure and GC churn.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Use one of these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AsNoTracking for pure reads&lt;/li&gt;
&lt;li&gt;AsNoTrackingWithIdentityResolution when graph identity consistency matters
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTrackingWithIdentityResolution&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use AsNoTrackingWithIdentityResolution when you need consistent identity for repeated references in the same result graph without full tracking overhead.&lt;/p&gt;

&lt;p&gt;&lt;a id="mistake-7"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7) Cartesian Explosion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Multiple collection Includes on one query create row multiplication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Use split queries when shape is unavoidable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsSplitQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Split queries trade one huge cross-product for multiple smaller round trips. Measure both modes on your dataset.&lt;/p&gt;

&lt;p&gt;&lt;a id="mistake-8"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8) No Compiled Queries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Hot endpoints repeatedly pay query translation overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Precompile stable high-frequency queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetUserByIdCompiled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;EF&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompileAsyncQuery&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&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;GetUserByIdCompiled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply compiled queries selectively to the highest-throughput read paths.&lt;/p&gt;

&lt;p&gt;&lt;a id="mistake-9"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  9) Missing Indexes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Critical filters run as scans instead of seeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Create indexes for common predicates and sort keys.&lt;br&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;modelBuilder&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;&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also verify generated SQL and query plan. If your API filter is index-friendly but still scanning, check collation mismatches and implicit conversions.&lt;/p&gt;

&lt;p&gt;Index guidance for hot endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;index frequent WHERE columns&lt;/li&gt;
&lt;li&gt;index JOIN keys&lt;/li&gt;
&lt;li&gt;index ORDER BY columns used with pagination&lt;/li&gt;
&lt;li&gt;prefer composite indexes that match filter plus sort order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="mistake-10"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  10) SaveChanges in a Loop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;Each SaveChanges call is a round trip and transaction boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix
&lt;/h3&gt;

&lt;p&gt;Batch entity changes, then persist once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkProcessed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this runs inside import or reconciliation jobs, moving SaveChanges out of the loop is often the fastest immediate win.&lt;/p&gt;

&lt;p&gt;&lt;a id="batch-ops-caveat"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch operations caveat: ExecuteUpdate and ExecuteDelete
&lt;/h2&gt;

&lt;p&gt;ExecuteUpdate and ExecuteDelete bypass the change tracker.&lt;/p&gt;

&lt;p&gt;If you mix them with tracked updates in the same unit of work, wrap all operations in an explicit transaction to protect consistency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteUpdateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;setters&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;setters&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expired&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;audit&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;CleanupAudit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;RanAtUtc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Affected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;affectedRows&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CleanupAudits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;batch operators bypass change tracker&lt;/li&gt;
&lt;li&gt;SaveChanges interceptors may not run for those operations&lt;/li&gt;
&lt;li&gt;mixed write workflows should be explicit about consistency boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision matrix: what to fix first
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;First fix&lt;/th&gt;
&lt;th&gt;Then&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;many SQL commands per request&lt;/td&gt;
&lt;td&gt;projection or Include cleanup&lt;/td&gt;
&lt;td&gt;check for lazy loading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;large payload and high serialization cost&lt;/td&gt;
&lt;td&gt;projection DTOs&lt;/td&gt;
&lt;td&gt;AsNoTracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;high read-path memory&lt;/td&gt;
&lt;td&gt;AsNoTracking&lt;/td&gt;
&lt;td&gt;query-level projection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;huge row count from multi-Include&lt;/td&gt;
&lt;td&gt;AsSplitQuery&lt;/td&gt;
&lt;td&gt;endpoint-specific projection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slow large updates/deletes&lt;/td&gt;
&lt;td&gt;ExecuteUpdate/ExecuteDelete&lt;/td&gt;
&lt;td&gt;transaction + audit strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list endpoint timeout&lt;/td&gt;
&lt;td&gt;pagination&lt;/td&gt;
&lt;td&gt;indexes + projection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hot endpoint CPU overhead&lt;/td&gt;
&lt;td&gt;compiled query&lt;/td&gt;
&lt;td&gt;caching strategy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What EF Core 10 helps with
&lt;/h2&gt;

&lt;p&gt;EF Core 10 improves the baseline, but does not remove bad query-shape costs.&lt;/p&gt;

&lt;p&gt;Useful platform gains to leverage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;improved LINQ ergonomics for complex joins&lt;/li&gt;
&lt;li&gt;stronger behavior around split-query ordering consistency&lt;/li&gt;
&lt;li&gt;runtime improvements in .NET 10 that help materialization paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bottom line: framework improvements help good patterns more than bad ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting checklist
&lt;/h2&gt;

&lt;p&gt;If performance is still poor after applying fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;inspect SQL plans, not just LINQ&lt;/li&gt;
&lt;li&gt;verify index usage in production-like data&lt;/li&gt;
&lt;li&gt;confirm no hidden lazy-loading path in serialization&lt;/li&gt;
&lt;li&gt;verify query count per request in high-traffic endpoints&lt;/li&gt;
&lt;li&gt;re-check page size limits and default sort columns&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A practical baseline for production APIs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Default reads to no tracking&lt;/li&gt;
&lt;li&gt;Project to DTOs, not entities&lt;/li&gt;
&lt;li&gt;Always paginate collection endpoints&lt;/li&gt;
&lt;li&gt;Measure query count per request&lt;/li&gt;
&lt;li&gt;Add indexes for every top-traffic filter&lt;/li&gt;
&lt;li&gt;Compile truly hot queries&lt;/li&gt;
&lt;li&gt;Avoid SaveChanges inside loops&lt;/li&gt;
&lt;li&gt;Treat batch operations as transaction-sensitive writes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EF Core performance is rarely about a single trick. It is about query-shape discipline applied consistently.&lt;/p&gt;

&lt;p&gt;If your P95 latency is climbing, start here before scaling the database tier.&lt;/p&gt;

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

&lt;p&gt;Most EF Core incidents are not caused by obscure ORM bugs. They come from convenient defaults used past their safe limits.&lt;/p&gt;

&lt;p&gt;If you adopt one rule, make it this: shape data intentionally at query time. Projection, no-tracking reads, pagination, and index-aware filters will solve most production regressions before you need heavier architecture changes.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>efcore</category>
      <category>performance</category>
    </item>
    <item>
      <title>System Design: Payment Processing System</title>
      <dc:creator>Rhuturaj Takle</dc:creator>
      <pubDate>Wed, 26 Aug 2026 15:18:29 +0000</pubDate>
      <link>https://dev.to/rhuturaj_takle/system-design-payment-processing-system-1h8d</link>
      <guid>https://dev.to/rhuturaj_takle/system-design-payment-processing-system-1h8d</guid>
      <description>&lt;h1&gt;
  
  
  System Design: Payment Processing System
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A capstone system design walkthrough — designing a payment processing system end to end — covering the core domain model, the ledger as the system's source of truth, idempotency and exactly-once-effect guarantees, integrating with external payment gateways and card networks, handling asynchronous webhooks, reconciliation, fraud and risk checks, and the specific correctness and compliance demands that make payments a uniquely unforgiving system design problem.&lt;/em&gt;&lt;/p&gt;




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

&lt;ol&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Why Payment Systems Are a Different Kind of Hard&lt;/li&gt;
&lt;li&gt;The Core Domain Model&lt;/li&gt;
&lt;li&gt;The Ledger: Double-Entry Bookkeeping as the Source of Truth&lt;/li&gt;
&lt;li&gt;Idempotency: The Single Most Important Property&lt;/li&gt;
&lt;li&gt;Integrating with Payment Gateways and Card Networks&lt;/li&gt;
&lt;li&gt;The Payment State Machine&lt;/li&gt;
&lt;li&gt;Webhooks: Handling Asynchronous Gateway Callbacks&lt;/li&gt;
&lt;li&gt;The Saga: Coordinating Payment Across Multiple Services&lt;/li&gt;
&lt;li&gt;Reconciliation&lt;/li&gt;
&lt;li&gt;Fraud and Risk Checks&lt;/li&gt;
&lt;li&gt;Data Security and Compliance&lt;/li&gt;
&lt;li&gt;Consistency, Availability, and the CAP Trade-off for Money&lt;/li&gt;
&lt;li&gt;Scaling the System&lt;/li&gt;
&lt;li&gt;Observability for a Payment System&lt;/li&gt;
&lt;li&gt;Common Pitfalls&lt;/li&gt;
&lt;li&gt;Quick Reference Table&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A payment processing system takes the general system design vocabulary covered in this series' System Design guide — databases, caching, queues, load balancing — and applies it to a domain where the ordinary consequences of a bug are dramatically higher: a double-charged customer, a lost payment, or a corrupted ledger isn't a degraded user experience, it's real money moved incorrectly, sometimes irreversibly. This guide walks through designing such a system end to end, drawing directly on this series' DDD, Event-Driven Architecture, Database Migrations, and Secret Management guides, each of which turns out to be load-bearing infrastructure for getting payments right rather than optional architectural polish.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → Payment API → [validate, risk-check] → Payment Gateway (Stripe/Adyen/etc.) → Card Network → Bank
                              ↓                           ↓ (async webhook)
                          Ledger (source of truth)  ←  Payment State Machine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1. Why Payment Systems Are a Different Kind of Hard
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The cost of a bug is measured in money, not just user experience
&lt;/h3&gt;

&lt;p&gt;Most systems covered in this series can tolerate a transient bug with a bounded, recoverable cost — a stale cache entry, a brief outage, a duplicate email. A payment system's failure modes are different in kind: a duplicate charge is real money taken from a real customer without their consent; a lost payment confirmation can mean a customer paid but never received their order, or a merchant shipped goods without ever being paid. This is why idempotency (Section 4) and the ledger's correctness (Section 3) dominate this guide's concerns more than raw throughput does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Money must reconcile — silently "close enough" isn't a valid state
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A social media "likes" counter being off by one, briefly, is invisible and harmless.
A ledger being off by one cent, ANYWHERE, is a genuine defect that must be found and explained.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlike most eventually-consistent systems covered in this series' Event-Driven Architecture guide, where "briefly stale, then converges" is an acceptable trade-off, a payment ledger must reconcile to the cent, provably, against the external systems (card networks, banks) it represents — this is a stricter correctness bar than "eventually consistent," and Section 9's reconciliation process exists specifically to enforce it continuously, not just trust that it holds.&lt;/p&gt;

&lt;h3&gt;
  
  
  You are almost never processing the actual money movement yourself
&lt;/h3&gt;

&lt;p&gt;A critical, freeing realization for the design that follows: a payment processing &lt;em&gt;system&lt;/em&gt;, in the overwhelming majority of real-world designs, does not itself move money between bank accounts — it orchestrates a request to a &lt;strong&gt;payment gateway&lt;/strong&gt; (Stripe, Adyen, Braintree, or a similar processor), which in turn talks to card networks (Visa, Mastercard) and banks. Your system's job is to reliably record intent, submit the request, track the outcome, and maintain an accurate internal ledger of what happened — not to reimplement banking infrastructure, which is precisely the kind of "don't build what a specialized provider already does well" guidance echoed in this series' Secret Management and OAuth2/OIDC guides for identity, applied here to money movement.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The Core Domain Model
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Modeled with DDD, per this series' companion guide
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;PaymentId&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;Value&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;record&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;MinorUnits&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="c1"&gt;// e.g., 4999 minor units + "USD" = $49.99 — see Section 3's note on this&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Initiated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Authorized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Captured&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Refunded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PartiallyRefunded&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Payment&lt;/span&gt; &lt;span class="c1"&gt;// the AGGREGATE ROOT, per this series' DDD guide&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;PaymentId&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&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;Money&lt;/span&gt; &lt;span class="n"&gt;Amount&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="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt; &lt;span class="n"&gt;Status&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;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaymentEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_domainEvents&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="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Authorize&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;gatewayAuthorizationId&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;Status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Initiated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Cannot authorize a payment in status &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorized&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_domainEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentAuthorizedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gatewayAuthorizationId&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;void&lt;/span&gt; &lt;span class="nf"&gt;Capture&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;Status&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Cannot capture a payment in status &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Captured&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_domainEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PaymentCapturedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Amount&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;This directly applies this series' DDD guide's aggregate pattern — &lt;code&gt;Payment&lt;/code&gt; is the aggregate root, enforcing its own state transitions (you cannot capture a payment that was never authorized) rather than trusting every caller to check status before mutating it, and raising domain events at exactly the points those transitions genuinely occur.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why money should never be a floating-point or plain &lt;code&gt;decimal&lt;/code&gt; type without care
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Floating-point arithmetic on money is a well-known, serious source of rounding errors&lt;/span&gt;
&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// binary floating point cannot represent this exactly&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Store money as an integer count of the smallest currency unit (cents, minor units)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;MinorUnits&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="c1"&gt;// 4999 minor units = $49.99&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Representing money as &lt;code&gt;double&lt;/code&gt; risks genuine, real rounding errors accumulating over many operations — the standard, widely-adopted practice is storing an amount as an integer number of the currency's smallest unit (cents for USD, pence for GBP), only converting to a display-formatted decimal string at the presentation layer, never performing arithmetic in that display format. &lt;code&gt;decimal&lt;/code&gt; in C# is safer than &lt;code&gt;double&lt;/code&gt; for money (base-10, not binary floating point), but many production payment systems still prefer integer minor units specifically for unambiguous cross-language, cross-system interoperability — worth being deliberate about which convention a given system adopts and applying it consistently everywhere money is represented.&lt;/p&gt;

&lt;h3&gt;
  
  
  Value objects for currency-safety
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Money&lt;/span&gt; &lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;other&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;Currency&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;other&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cannot add different currencies"&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;this&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;MinorUnits&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MinorUnits&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinorUnits&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;As covered in this series' DDD guide's value object discussion, wrapping a raw amount in a &lt;code&gt;Money&lt;/code&gt; value object that enforces currency-matching on any arithmetic operation prevents an entire class of bugs (accidentally adding USD to EUR) at the type level, rather than relying on every call site to remember to check currencies match.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Ledger: Double-Entry Bookkeeping as the Source of Truth
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why a simple "balance" column is insufficient
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ❌ A single mutable balance column has no audit trail and is trivially corruptible by a single bad UPDATE&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A payment system needs more than "what is the current balance" — it needs an immutable, auditable record of &lt;em&gt;every&lt;/em&gt; movement of money that ever occurred, and the ability to prove, at any point, exactly how the current balance was arrived at. A mutable balance column, updated in place, destroys that history the moment it's overwritten, and provides no structural protection against a bug (or a malicious actor) silently corrupting a balance with no trace of how it happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  Double-entry bookkeeping: every movement recorded as two balanced entries
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;ledger_entries&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;transaction_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- groups the debit and credit belonging to one logical movement&lt;/span&gt;
    &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_minor_units&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- positive for a credit, negative for a debit&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&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="c1"&gt;-- A $49.99 payment captured: money moves from "customer owes" to "merchant receivable"&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;ledger_entries&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_minor_units&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="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a1b2c3d4-...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* customer_receivable_account */&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'a1b2c3d4-...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* merchant_payable_account */&lt;/span&gt;    &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- these two rows, sharing one transaction_id, must ALWAYS sum to zero&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Double-entry bookkeeping&lt;/strong&gt; — the centuries-old accounting technique this system borrows directly — records every movement of money as (at least) two balanced entries: a debit from one account and a credit to another, always summing to exactly zero for any given transaction. This isn't accounting ceremony for its own sake; it's a structural, mathematically-verifiable invariant: at any point, summing every ledger entry for a given transaction ID must equal zero, and summing every entry for a given account gives that account's genuine, provable current balance, derived entirely from the append-only history rather than trusted as a separately-maintained, corruptible number.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ledger table is append-only, never updated or deleted
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Correcting a mistake means inserting a NEW, compensating entry — never UPDATE or DELETE an existing row&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;ledger_entries&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount_minor_units&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="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'correction-e5f6...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* customer_receivable_account */&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- reverses the original debit&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'correction-e5f6...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* merchant_payable_account */&lt;/span&gt;    &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- reverses the original credit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This directly echoes the append-only log philosophy covered in this series' Kafka and Event Sourcing (via the DDD and Event-Driven Architecture guides) discussions — the ledger is never mutated in place; a mistake is corrected by inserting a new, compensating entry that reverses the original, preserving the complete, honest history of everything that happened, including the mistake and its correction, rather than erasing evidence that a mistake occurred at all. This property is what makes the ledger auditable and, critically, what regulators and auditors expect from any genuine financial system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Balance as a derived, always-recomputable value
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount_minor_units&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;current_balance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ledger_entries&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An account's current balance is always a &lt;code&gt;SUM&lt;/code&gt; query over its ledger entries — never a separately-stored, independently-updatable number that could drift out of sync with the entries that supposedly produced it. For performance (summing potentially millions of historical entries on every balance check is genuinely expensive), a cached/materialized balance is a reasonable optimization (directly connecting to this series' caching and materialized-view discussions), but it must always be treated as a derived cache of the ledger's truth, recomputable and re-verifiable against it at any time — never the authoritative source itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Idempotency: The Single Most Important Property
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why this is even more critical here than in any other system covered in this series
&lt;/h3&gt;

&lt;p&gt;As covered throughout this series' RabbitMQ, Kafka, Azure Service Bus, and Event-Driven Architecture guides, every messaging technology provides at-least-once delivery, and every network call can time out ambiguously (did the request actually succeed server-side, or not, before the client gave up waiting?) — for most systems, a resulting duplicate action is an annoyance (a duplicate email, a slightly wasted computation). For a payment system, an un-idempotent retry means &lt;strong&gt;charging a customer twice for the same purchase&lt;/strong&gt;, which is precisely why idempotency is this guide's single most emphasized property.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotency keys: the standard mechanism
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/payments"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreatePayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FromHeader&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;"Idempotency-Key"&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;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CreatePaymentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_idempotencyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetResultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&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="c1"&gt;// the SAME response as the original request, no new charge attempted&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;payment&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;_paymentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessAsync&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_idempotencyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveResultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&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 the concrete implementation of the idempotency pattern introduced generally in this series' Redis guide's rate-limiting section and REST guide's discussion — a client generates a unique idempotency key for each &lt;em&gt;logical&lt;/em&gt; payment attempt (not regenerated on retry) and includes it on every request, including retries; the server checks whether that key has already been processed and, if so, returns the &lt;em&gt;original&lt;/em&gt; result rather than attempting the charge again. This is precisely how Stripe, Adyen, and every major payment gateway's own API is designed, and any payment system built on top of one should propagate this exact same discipline to its own client-facing API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotency at every layer the payment touches, not just the outermost API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → Payment API (idempotency key checked here)
            → Payment Gateway call (the GATEWAY also expects and enforces its own idempotency key)
            → Ledger write (a database-level unique constraint on transaction_id prevents a duplicate insert)
            → Event published (per this series' Event-Driven Architecture guide, consumers must ALSO be idempotent)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Idempotency needs to be enforced at every hop, not just the client-facing entry point — the call to the external payment gateway itself should include its own idempotency key (most major gateways support and expect this natively), the ledger write should have a database constraint preventing a duplicate transaction ID from ever being inserted twice, and any downstream event consumers (per this series' Event-Driven Architecture guide) must independently be idempotent against redelivery, since a payment system is exactly the kind of system where "we'll just be extra careful" is not an acceptable substitute for structural, enforced guarantees at every layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Integrating with Payment Gateways and Card Networks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The layers between your system and an actual bank
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your system → Payment Gateway (Stripe, Adyen, Braintree) → Card Network (Visa, Mastercard) → Issuing Bank
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;payment gateway&lt;/strong&gt; is the specialized third party that actually handles the sensitive complexity of talking to card networks and banks — authorization, settlement, PCI compliance for card data handling (Section 11) — so that a payment system, in the overwhelming majority of real designs, never directly touches raw card numbers or talks to a card network itself at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization vs. capture: a two-phase pattern most gateways support
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Phase 1: authorize — places a hold on the customer's funds, doesn't yet move money&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;authResult&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;_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AuthorizeAsync&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;AuthorizeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cardToken&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GatewayAuthorizationId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Phase 2: capture — actually moves the money, typically once the order genuinely ships&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;captureResult&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;_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CaptureAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GatewayAuthorizationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Capture&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separating &lt;strong&gt;authorization&lt;/strong&gt; (verifying funds are available and placing a hold) from &lt;strong&gt;capture&lt;/strong&gt; (actually completing the charge) is a deliberate, widely-used design pattern — it lets a merchant confirm a customer can pay &lt;em&gt;before&lt;/em&gt; committing to ship an order, and only finalize the charge once the order genuinely ships, reducing the need for refunds on orders that turn out to be unfulfillable, and directly mapping onto the &lt;code&gt;Payment&lt;/code&gt; aggregate's state machine from Section 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using gateway-provided tokens, never touching raw card numbers directly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Card details are tokenized CLIENT-SIDE, by the gateway's own JS SDK — your server NEVER sees the raw card number&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cardElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// only this opaque token is ever sent to YOUR backend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standard, essentially universal pattern: raw card numbers are tokenized directly in the client (browser or mobile app), by the payment gateway's own SDK, before ever reaching your server — your backend only ever handles an opaque token representing the card, never the actual card number itself. This dramatically reduces your own system's PCI compliance burden (Section 11) since sensitive card data structurally never touches your infrastructure at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Payment State Machine
&lt;/h2&gt;

&lt;h3&gt;
  
  
  An explicit, enumerable set of states and legal transitions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initiated → Authorized → Captured → (Refunded | PartiallyRefunded)
     ↓            ↓
   Failed       Failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As covered in Section 2's &lt;code&gt;Payment&lt;/code&gt; aggregate, a payment's lifecycle is a small, explicit state machine — and the aggregate's own methods (&lt;code&gt;Authorize()&lt;/code&gt;, &lt;code&gt;Capture()&lt;/code&gt;) are what enforce that only legal transitions are ever possible, throwing rather than silently succeeding if called out of order (attempting to capture a payment that was never authorized, for instance).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why an explicit state machine matters more here than for most domain objects
&lt;/h3&gt;

&lt;p&gt;Given this guide's emphasis on the cost of a payment-related bug, having every legal and illegal state transition explicitly enumerated and enforced by the aggregate itself — rather than scattered conditional checks across application code — is precisely the kind of rigor this series' DDD guide argues pays for itself most clearly in domains with genuinely complex, high-stakes business rules, and few domains fit that description more clearly than payments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terminal states and their permanence
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;refundAmount&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;Status&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Captured&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PartiallyRefunded&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Cannot refund a payment in status &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... a Refunded/PartiallyRefunded payment can never transition back to Captured&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certain states are genuinely terminal or near-terminal (a &lt;code&gt;Failed&lt;/code&gt; payment doesn't transition anywhere further; a fully &lt;code&gt;Refunded&lt;/code&gt; payment shouldn't be refundable again) — encoding these as hard constraints in the aggregate is what prevents an entire category of "this should never happen but somehow did" production incidents specific to payment state.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Webhooks: Handling Asynchronous Gateway Callbacks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why payment gateways rely on webhooks, not just synchronous API responses
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your system → gateway.charge() → gateway returns "pending" immediately
    ... (minutes later, potentially) ...
Gateway → POST /webhooks/payment-status → your system, asynchronously reporting the FINAL outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many payment flows (particularly certain card types requiring additional authentication, or bank transfers) don't resolve synchronously within the original API call — the gateway instead sends an asynchronous &lt;strong&gt;webhook&lt;/strong&gt; once the final outcome is known, directly connecting to this series' Event-Driven Architecture guide's core theme: your system needs to handle this exactly like consuming an event from an external, asynchronous source, with all the same discipline (idempotency, per Section 4; ordering awareness) that guide covers for internal messaging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying webhook authenticity — this is not optional
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/webhooks/payment-gateway"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleWebhook&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;payload&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;new&lt;/span&gt; &lt;span class="nf"&gt;StreamReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ReadToEndAsync&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;signature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Stripe-Signature"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Verifies the payload genuinely came from the gateway, using a shared secret — per this series'&lt;/span&gt;
    &lt;span class="c1"&gt;// Secret Management and JWT Validation guides' emphasis on never trusting an unverified sender&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;isValid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_gatewaySignatureVerifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_webhookSecret&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;isValid&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;Unauthorized&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;evt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ParseWebhookEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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;ProcessWebhookEventAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&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;Ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A webhook endpoint is a publicly reachable URL, by necessity — without verifying the gateway's cryptographic signature on every incoming webhook (using a shared secret, stored per this series' Secret Management guide), an attacker could submit a forged "payment succeeded" webhook and trick your system into believing a payment completed when it never did. This is a direct, concrete application of this series' OWASP Top 10 guide's broken-authentication and injection categories, applied specifically to a payment system's most externally-exposed surface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook idempotency and out-of-order delivery
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_processedWebhookEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EventId&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;Ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// already processed, safe no-op&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;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastUpdatedAt&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;Ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// an OLDER event arriving late — ignore, don't regress state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As covered in this series' Event-Driven Architecture guide, webhooks are subject to the same at-least-once delivery and potential out-of-order arrival as any other asynchronous message — tracking processed event IDs (idempotency, Section 4 again) and comparing event timestamps against the payment's own last-known state (to avoid a late-arriving, stale webhook incorrectly reverting a payment to an earlier state) are both essential, not optional hardening.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. The Saga: Coordinating Payment Across Multiple Services
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Payment as one step in a larger, cross-service business process
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderSaga (per this series' Event-Driven Architecture guide):
  1. OrderService: create order (pending)
  2. InventoryService: reserve stock — compensating action: release stock
  3. PaymentService: charge payment — compensating action: REFUND
  4. OrderService: confirm order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As covered directly in this series' Event-Driven Architecture and Microservices guides, "place an order" typically spans multiple services with separate databases — payment is one step in that larger saga, and its &lt;strong&gt;compensating action&lt;/strong&gt;, should a later step fail, is a refund, not a database rollback (since, per Section 1, there's no cross-service ACID transaction spanning the order, inventory, and payment services' separate databases).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the compensating action for a payment is itself a genuine, auditable transaction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CompensateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentId&lt;/span&gt; &lt;span class="n"&gt;paymentId&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;payment&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;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paymentId&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;refund&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// a NEW ledger transaction, per Section 3 — never erasing the original charge&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RefundAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GatewayCaptureId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&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;Unlike compensating actions in many other domains (releasing a reserved inventory count, say), a payment's compensation is itself a fully real, ledger-recorded, gateway-executed transaction — this directly reinforces Section 3's append-only ledger principle: a failed downstream step doesn't erase the original charge from history, it records a new, compensating refund transaction alongside it, preserving the complete, honest record of what actually happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Reconciliation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why "the ledger looks right" isn't sufficient — it must be proven against external truth
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your ledger says: $48,392.17 captured today
The payment gateway's own settlement report says: $48,392.17 settled today
→ these must match, EXACTLY, every single day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reconciliation&lt;/strong&gt; is the (often nightly, automated) process of comparing your system's own ledger against the payment gateway's independently-generated settlement reports, and ultimately against your bank's actual statements — this is the concrete, continuously-enforced verification that Section 1's "money must reconcile" property actually holds, not just an assumption resting on your own system's internal consistency checks alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automating reconciliation, and surfacing discrepancies immediately
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ReconcileAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateOnly&lt;/span&gt; &lt;span class="n"&gt;date&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;ourRecords&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;_ledgerRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCapturedPaymentsForDateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&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;gatewayRecords&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;_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSettlementReportAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&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;discrepancies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;FindMismatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ourRecords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gatewayRecords&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;discrepancies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_alerting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RaiseAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Reconciliation discrepancy detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discrepancies&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// per this series'&lt;/span&gt;
                                                                                             &lt;span class="c1"&gt;// Prometheus/Grafana guide's&lt;/span&gt;
                                                                                             &lt;span class="c1"&gt;// alerting discipline&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A discrepancy found during reconciliation — a payment your system believes captured that the gateway's settlement report doesn't show, or vice versa — is a genuinely serious signal, treated with the same urgency as a security incident (per this series' OWASP Top 10 guide) rather than a routine data-quality issue to quietly patch; every discrepancy needs to be understood and explained, not merely corrected and forgotten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reconciliation as a recurring, automated background process
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NightlyReconciliationWorker&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundService&lt;/span&gt; &lt;span class="c1"&gt;// per this series' Background Services guide&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&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;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// scheduled, per this series' Background Services guide's recurring-job patterns, using Hangfire or Quartz.NET&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 directly reuses the scheduled background job patterns covered in this series' Background Services guide — reconciliation is precisely the kind of recurring, automated job those patterns are built for, run nightly (or more frequently) without manual intervention, with its own health monitoring (per this series' Health Checks guide's "last successful run" pattern) to ensure the reconciliation job itself hasn't silently stopped running.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Fraud and Risk Checks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Where fraud checks fit in the payment flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment request → [Risk scoring: velocity checks, device fingerprinting, address verification]
                        ↓
              Score below threshold: proceed to gateway authorization
              Score above threshold: hold for manual review, or decline outright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A production payment system layers fraud/risk assessment before (or alongside) the actual gateway authorization call — checking transaction velocity (has this card/account attempted an unusual number of payments recently), device and IP reputation, and billing/shipping address consistency, often using a specialized third-party risk-scoring service (analogous to how gateways themselves are typically third-party specialists, per Section 1) rather than building fraud detection from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trade-off between fraud prevention and legitimate-customer friction
&lt;/h3&gt;

&lt;p&gt;Every fraud check has a real cost in false positives — a legitimate customer wrongly declined or delayed by an overly aggressive risk check is a genuine, measurable business cost, not a harmless extra precaution; this is a deliberate, ongoing tuning exercise (adjusting risk thresholds based on observed false-positive and fraud-loss rates over time) rather than a "more strict is always better" default.&lt;/p&gt;

&lt;h3&gt;
  
  
  3D Secure and step-up authentication
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Card payment → gateway determines additional authentication is required (3D Secure) →
  customer redirected to their bank's own authentication challenge → returns, payment proceeds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For card payments specifically, &lt;strong&gt;3D Secure&lt;/strong&gt; (the "Verified by Visa"/"Mastercard Identity Check" flow many customers have encountered) shifts liability for certain fraud disputes from the merchant to the card issuer, in exchange for an additional authentication step — most gateways handle the actual challenge flow, but your system's payment flow (and its state machine, per Section 6) needs to accommodate this additional, asynchronous authentication step as a legitimate part of the payment lifecycle, not an edge case.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Data Security and Compliance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PCI DSS: why tokenization (Section 5) is the practical answer, not a checklist to satisfy directly
&lt;/h3&gt;

&lt;p&gt;The Payment Card Industry Data Security Standard (PCI DSS) imposes extensive, genuinely burdensome requirements on any system that stores, processes, or transmits raw card data — the practical, almost universally adopted strategy for a system built on top of a gateway (per Section 5) is to &lt;strong&gt;never let raw card data touch your own infrastructure at all&lt;/strong&gt;, via client-side tokenization, which dramatically narrows your own PCI compliance scope rather than requiring you to build and audit a full PCI-compliant environment yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption and secret management for whatever sensitive data your system does hold
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// API keys for the payment gateway itself are exactly the kind of secret covered in this series'&lt;/span&gt;
&lt;span class="c1"&gt;// Secret Management guide — never in source control, ideally via Managed Identity + Key Vault&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gatewayApiKey&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;_secretClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payment-gateway-api-key"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with card data itself tokenized away, a payment system still holds genuinely sensitive secrets — gateway API keys, webhook signing secrets — and every principle covered in this series' Secret Management guide applies directly and without exception here: no hardcoded credentials, Managed Identity where the platform supports it, and rotation discipline for anything that could grant an attacker the ability to initiate fraudulent charges or forge webhook events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audit logging as a compliance and forensic requirement, not just an operational nicety
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Payment {PaymentId} captured for {Amount} by {ActorId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As covered in this series' Structured Logging and OWASP Top 10 guides, every sensitive action (a payment captured, a refund issued, a risk override applied) needs to be logged with enough context (who, what, when) to support both regulatory audit requirements and forensic investigation after an incident — this is a stricter, more comprehensive logging bar than most systems require, precisely because of Section 1's stakes.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Consistency, Availability, and the CAP Trade-off for Money
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why payment systems generally favor consistency over availability, unlike much of this series' general guidance
&lt;/h3&gt;

&lt;p&gt;As covered in this series' System Design guide's CAP theorem discussion, most systems in this series lean toward availability and eventual consistency where possible — a payment system is one of the clearer, most defensible exceptions: it is generally preferable for a payment attempt to fail cleanly (the customer retries, or sees a clear error) than for the system to accept it under uncertain, potentially-inconsistent conditions and risk a ledger discrepancy that reconciliation (Section 9) later has to painstakingly untangle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where eventual consistency is still acceptable, deliberately scoped
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The LEDGER write (money moved) → strong consistency required, no compromise
A downstream ANALYTICS dashboard showing "today's revenue" → eventual consistency is genuinely fine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every part of a payment system needs the same consistency bar — the core ledger write absolutely does, but downstream, read-only projections (a merchant's revenue dashboard, an analytics pipeline) can and should tolerate the same eventual consistency this series' Event-Driven Architecture and CQRS discussions describe generally, since a dashboard being a few seconds stale carries none of the risk a genuinely inconsistent ledger does.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Scaling the System
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Applying this series' System Design guide's building blocks, with payment-specific emphasis
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read replicas (per this series' SQL Server/PostgreSQL guides): safe for READ-heavy queries
  (transaction history, dashboards) — never route a WRITE that must be immediately consistent to a replica
Caching (per this series' Redis guide): appropriate for relatively static data (merchant configuration,
  fee schedules) — NEVER cache a payment's current status, which must always reflect genuine current state
Queues (per this series' RabbitMQ/Kafka guides): appropriate for the asynchronous parts of the flow
  (webhook processing, sending receipt emails, updating analytics) — NOT for the synchronous
  authorization call itself, which the customer is actively waiting on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every technique from this series' System Design guide applies here, with the caveat that each one needs to be evaluated against this guide's stricter consistency bar (Section 12) before being applied — the general principle "identify the bottleneck, then apply the specific technique" holds, but payments narrow which techniques are safe to apply to which specific part of the flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharding the ledger, and the partition key that actually matters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sharding by account_id (or merchant_id) keeps all of one account's ledger entries together,
  making "what is this account's balance" a single-shard query rather than a cross-shard fan-out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As covered in this series' System Design and Cosmos DB/MongoDB guides, choosing the ledger's partition/shard key deliberately — typically the account or merchant ID, since balance queries are the most common and most latency-sensitive access pattern — avoids the expensive cross-shard fan-out that a poorly chosen key (transaction ID, say) would force on every balance check.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Observability for a Payment System
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Every guide in this series' observability trio, applied with payment-specific stakes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Structured logs (per this series' Structured Logging guide): every payment state transition, logged
  with the payment ID and correlation ID, NEVER logging raw card data or full gateway tokens
Distributed tracing (per this series' Distributed Tracing guide): tracing a single payment's journey
  across the risk check, gateway call, and ledger write — essential for diagnosing where a specific
  slow or failed payment actually got stuck
Metrics (per this series' Prometheus/Grafana guide): payment success rate, gateway latency,
  authorization decline rate — the aggregate health signals a payments team watches continuously
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every technique from this series' observability guides applies directly, with one payment-specific addition worth stating explicitly: logs and traces must never capture raw card numbers, full gateway tokens, or CVV data, even for debugging purposes — this is a hard, non-negotiable line directly extending this series' OWASP Top 10 and Secret Management guides' "never log sensitive data" principle, applied here with genuinely higher stakes than almost any other domain in this series.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alerting on payment-specific symptoms
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Per this series' Prometheus/Grafana guide's symptom-based alerting principle, applied to payments
rate(payment_declined_total[5m]) / rate(payment_attempted_total[5m]) &amp;gt; 0.15  # a sudden decline-rate spike
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sudden spike in the payment decline rate, or in gateway latency, is exactly the kind of user-facing symptom this series' Prometheus/Grafana guide argues alerts should be built around — and for a payment system, the on-call response to such an alert carries unusually direct business consequences (lost revenue, frustrated customers), which is precisely why this category of alert deserves genuinely fast, well-rehearsed incident response.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. Common Pitfalls
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pitfall&lt;/th&gt;
&lt;th&gt;Why it hurts&lt;/th&gt;
&lt;th&gt;Better approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storing money as &lt;code&gt;double&lt;/code&gt; or unvalidated raw decimals&lt;/td&gt;
&lt;td&gt;Real rounding errors, currency-mismatch bugs&lt;/td&gt;
&lt;td&gt;Integer minor units or a currency-aware &lt;code&gt;Money&lt;/code&gt; value object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A mutable balance column instead of an append-only ledger&lt;/td&gt;
&lt;td&gt;No audit trail; a single bad &lt;code&gt;UPDATE&lt;/code&gt; silently corrupts financial history&lt;/td&gt;
&lt;td&gt;Double-entry, append-only ledger entries; balance as a derived &lt;code&gt;SUM&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No idempotency key on payment creation/retry&lt;/td&gt;
&lt;td&gt;A network timeout retry genuinely double-charges the customer&lt;/td&gt;
&lt;td&gt;Idempotency keys enforced at every layer: API, gateway call, and ledger write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trusting an unverified webhook&lt;/td&gt;
&lt;td&gt;An attacker can forge a fake "payment succeeded" event&lt;/td&gt;
&lt;td&gt;Always verify the gateway's cryptographic signature on every webhook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handling raw card numbers on your own servers&lt;/td&gt;
&lt;td&gt;Enormous PCI DSS compliance burden, real breach risk&lt;/td&gt;
&lt;td&gt;Client-side tokenization; never let raw card data touch your infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No reconciliation process, trusting your own ledger's internal consistency alone&lt;/td&gt;
&lt;td&gt;A silent, undetected discrepancy against the gateway's own records&lt;/td&gt;
&lt;td&gt;Automated, daily reconciliation against the gateway's settlement reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching a payment's current status&lt;/td&gt;
&lt;td&gt;Stale cached status shown to a customer or downstream system during an active, changing payment&lt;/td&gt;
&lt;td&gt;Never cache genuinely time-sensitive payment state; cache only static reference data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logging raw card numbers or full tokens for debugging&lt;/td&gt;
&lt;td&gt;A severe compliance and security violation&lt;/td&gt;
&lt;td&gt;Redact/exclude sensitive fields from all logs and traces, without exception&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Quick Reference Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Payment&lt;/code&gt; aggregate + state machine&lt;/td&gt;
&lt;td&gt;Enforces only legal payment state transitions, per this series' DDD guide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Double-entry, append-only ledger&lt;/td&gt;
&lt;td&gt;The provably correct, auditable source of truth for all money movement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency key&lt;/td&gt;
&lt;td&gt;Prevents duplicate charges from retries at every layer of the flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization / capture&lt;/td&gt;
&lt;td&gt;Separates "verify funds available" from "actually move the money"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokenization&lt;/td&gt;
&lt;td&gt;Keeps raw card data off your own infrastructure, narrowing PCI scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook signature verification&lt;/td&gt;
&lt;td&gt;Prevents forged, unauthorized payment-status events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saga + compensation (refund)&lt;/td&gt;
&lt;td&gt;Coordinates payment correctly across a larger, multi-service business process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reconciliation&lt;/td&gt;
&lt;td&gt;Continuously proves the ledger matches the gateway's/bank's own records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fraud/risk scoring&lt;/td&gt;
&lt;td&gt;Balances fraud prevention against legitimate-customer friction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A payment processing system takes every general system design technique covered throughout this series and applies it under a stricter, less forgiving correctness bar — because the cost of a bug here is measured in real money moved incorrectly, not just degraded user experience. The design that actually holds up under that bar rests on a small number of non-negotiable foundations: a double-entry, append-only ledger as the provable source of truth; idempotency enforced at every single layer a payment touches; an explicit, aggregate-enforced state machine governing what transitions are even possible; and continuous, automated reconciliation that treats any discrepancy as a genuine incident rather than a rounding error to quietly absorb.&lt;/p&gt;

&lt;p&gt;Nearly every architectural pattern covered elsewhere in this series shows up here in service of that bar — DDD's aggregates enforcing business rules, Event-Driven Architecture's sagas and idempotent consumers, Secret Management's discipline around gateway credentials, and the full observability trio watching over a system where "we'll notice eventually" is never an acceptable answer. Payments are, in that sense, less a distinct discipline from everything else in this series than the place where its cumulative lessons about correctness, idempotency, and honest reconciliation with reality matter more visibly and more unforgivingly than almost anywhere else.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? Feel free to star the repo, open an issue with corrections, or share the reconciliation discrepancy that turned out to matter far more than a rounding error ever should.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>dotnet</category>
      <category>programming</category>
      <category>learning</category>
    </item>
    <item>
      <title>.NET WebCIL Container Meets WebForms Core 2.1</title>
      <dc:creator>Elanat Framework</dc:creator>
      <pubDate>Wed, 26 Aug 2026 14:02:10 +0000</pubDate>
      <link>https://dev.to/elanatframework/net-webcil-container-meets-webforms-core-21-39f5</link>
      <guid>https://dev.to/elanatframework/net-webcil-container-meets-webforms-core-21-39f5</guid>
      <description>&lt;h2&gt;
  
  
  What is WebForms Core?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://elanat.net/page_content/web_forms_core" rel="noopener noreferrer"&gt;WebForms Core&lt;/a&gt; is a modern multi-platform web technology from &lt;a href="https://elanat.net" rel="noopener noreferrer"&gt;Elanat&lt;/a&gt; designed to build interactive web applications without requiring traditional client-side business logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;WebForms Core uses a server-centric architecture in which the server defines UI behavior through commands, while a lightweight client engine executes those commands in the browser.&lt;/p&gt;

&lt;p&gt;With the upcoming &lt;strong&gt;WebForms Core 2.1 (WFC)&lt;/strong&gt;, this architecture is being extended with another interesting capability: &lt;strong&gt;C# code running inside a .NET WebAssembly environment can use WebForms Core itself to generate UI commands.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This makes it possible to combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;C#&lt;/li&gt;
&lt;li&gt;.NET WebAssembly&lt;/li&gt;
&lt;li&gt;WebForms Core&lt;/li&gt;
&lt;li&gt;WebForms Core's declarative UI commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;inside the same WebAssembly application.&lt;/p&gt;




&lt;h1&gt;
  
  
  .NET WebCIL Container
&lt;/h1&gt;

&lt;p&gt;One of the new possibilities demonstrated with WFC 2.1 is the use of a &lt;strong&gt;.NET WebCIL Container&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of compiling C# into a standalone native-style WASM function such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add(10000, 3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the application can load the .NET WebAssembly runtime and execute exported C# methods through &lt;code&gt;dotnet.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices.JavaScript&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;WebFormsCore&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;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyClass&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSExport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Add&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;a&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;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSExport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;SetData&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;inputPlace&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;text&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;backgroundColor&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;fontSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;WebForms&lt;/span&gt; &lt;span class="n"&gt;form&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;WebForms&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputPlace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBackgroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSExport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetHtml&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;marquee&amp;gt;Tag From Wasm!&amp;lt;/marquee&amp;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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Main&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;The important part is that &lt;strong&gt;&lt;code&gt;WebForms.cs&lt;/code&gt; from WebForms Core is included in the WebAssembly project&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Therefore, C# code running inside the WebAssembly environment can directly create WebForms Core commands.&lt;/p&gt;




&lt;h1&gt;
  
  
  Creating the .NET WebCIL Project
&lt;/h1&gt;

&lt;p&gt;Create a new .NET 10 WebAssembly console project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new wasmconsole &lt;span class="nt"&gt;-n&lt;/span&gt; NativeWasmModule &lt;span class="nt"&gt;-f&lt;/span&gt; net10.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then enter the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;NativeWasmModule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project can use the following configuration:&lt;br&gt;
&lt;/p&gt;

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

  &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net10.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;WasmEnableExceptionHandling&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/WasmEnableExceptionHandling&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;RuntimeIdentifier&amp;gt;&lt;/span&gt;browser-wasm&lt;span class="nt"&gt;&amp;lt;/RuntimeIdentifier&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;OutputType&amp;gt;&lt;/span&gt;Exe&lt;span class="nt"&gt;&amp;lt;/OutputType&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;AllowUnsafeBlocks&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/AllowUnsafeBlocks&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;WasmMainJSPath&amp;gt;&lt;/span&gt;main.mjs&lt;span class="nt"&gt;&amp;lt;/WasmMainJSPath&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The project targets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net10.0&lt;span class="nt"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;RuntimeIdentifier&amp;gt;&lt;/span&gt;browser-wasm&lt;span class="nt"&gt;&amp;lt;/RuntimeIdentifier&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;main.mjs&lt;/code&gt; file is specified as the JavaScript entry point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;WasmMainJSPath&amp;gt;&lt;/span&gt;main.mjs&lt;span class="nt"&gt;&amp;lt;/WasmMainJSPath&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Adding WebForms Core
&lt;/h1&gt;

&lt;p&gt;The &lt;code&gt;WebForms.cs&lt;/code&gt; class from WebForms Core is added to this project.&lt;/p&gt;

&lt;p&gt;This is important because the WebAssembly C# code is now capable of using WebForms Core's API.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&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;WebForms&lt;/span&gt; &lt;span class="n"&gt;form&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;WebForms&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputPlace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBackgroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The C# method doesn't directly manipulate the browser DOM.&lt;/p&gt;

&lt;p&gt;Instead, it generates a &lt;strong&gt;WebForms Core response&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This keeps the same philosophy used throughout WebForms Core.&lt;/p&gt;




&lt;h1&gt;
  
  
  Exporting C# Methods
&lt;/h1&gt;

&lt;p&gt;Methods that should be accessible from JavaScript are marked with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSExport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSExport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;Add&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;a&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;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method can then be accessed through the .NET WebAssembly runtime.&lt;/p&gt;

&lt;p&gt;A method can also return a WebForms Core response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSExport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;SetData&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;inputPlace&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;text&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;backgroundColor&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;fontSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;WebForms&lt;/span&gt; &lt;span class="n"&gt;form&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;WebForms&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputPlace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBackgroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Response&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 where the combination becomes particularly interesting.&lt;/p&gt;

&lt;p&gt;The WASM code isn't merely calculating a value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can generate WebForms Core UI behavior.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Using C# WebAssembly from WebForms Core
&lt;/h1&gt;

&lt;p&gt;A WebForms Core controller can invoke the C# WebAssembly runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;CodeBehind&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;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CsharpMediatorWasmController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CodeBehindController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PageLoad&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;context&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;WasmPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/web-assembly/csharp-publish/_framework/dotnet.js"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;WebForms&lt;/span&gt; &lt;span class="n"&gt;form&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;WebForms&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;b&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Fetch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WasmMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WasmLanguage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSharpMediator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyClass.Add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;

        &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetWasmEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WasmEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HtmlEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmLanguage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSharpMediator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyClass.SetData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"h3Tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Text From Wasm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"lightgreen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"30px"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetWasmEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WasmEventWithOutput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HtmlEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmLanguage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSharpMediator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyClass.GetHtml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="s"&gt;"WasmHtmlOutput"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExportToHtmlComment&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 the method names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MyClass.Add
MyClass.SetData
MyClass.GetHtml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The class name can be specified before the method name.&lt;/p&gt;

&lt;p&gt;This makes it possible to address exported methods belonging to different C# types.&lt;/p&gt;




&lt;h1&gt;
  
  
  HTML
&lt;/h1&gt;

&lt;p&gt;The HTML remains simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@page
@controller CsharpMediatorWasmController
@layout "/layout.aspx"

@{
    ViewData.Add("title", "C# Mediator Wasm");
}

&lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;.NET WebCIL Container&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;C# Mediator WASM Result: &lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"WasmEvent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Wasm Event
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"h3Tag"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Wasm Tag Changing!
&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"WasmEventWithOutput"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Wasm Event With Output
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"WasmHtmlOutput"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Wasm Html Output
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no custom WebAssembly element.&lt;/p&gt;

&lt;p&gt;There is no special DOM component.&lt;/p&gt;

&lt;p&gt;There is no C# component syntax.&lt;/p&gt;

&lt;p&gt;The HTML remains standard HTML.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Interesting Part
&lt;/h1&gt;

&lt;p&gt;Consider 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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetWasmEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WasmEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HtmlEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmLanguage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSharpMediator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyClass.SetData"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"h3Tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Text From Wasm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"lightgreen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"30px"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server declares:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When this button is clicked, execute &lt;code&gt;MyClass.SetData&lt;/code&gt; inside the C# WebAssembly runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The method executes:&lt;br&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;WebForms&lt;/span&gt; &lt;span class="n"&gt;form&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;WebForms&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputPlace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBackgroundColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is then handled by WebForms Core.&lt;/p&gt;

&lt;p&gt;The C# WebAssembly method therefore becomes another source of WebForms Core commands.&lt;/p&gt;




&lt;h1&gt;
  
  
  WASM Event With Output
&lt;/h1&gt;

&lt;p&gt;The same mechanism can return HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSExport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetHtml&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;marquee&amp;gt;Tag From Wasm!&amp;lt;/marquee&amp;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:&lt;br&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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetWasmEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WasmEventWithOutput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HtmlEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmLanguage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSharpMediator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyClass.GetHtml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="s"&gt;"WasmHtmlOutput"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The returned value is placed into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"WasmHtmlOutput"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This demonstrates that the WASM method can be used not only for calculations, but also as a source of dynamic UI output.&lt;/p&gt;




&lt;p&gt;The screenshot below shows the HTML page after clicking the buttons.&lt;/p&gt;

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




&lt;h1&gt;
  
  
  What Makes This Different?
&lt;/h1&gt;

&lt;p&gt;This is not simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Run C# in the browser."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That capability already exists.&lt;/p&gt;

&lt;p&gt;The interesting part is the &lt;strong&gt;combination of C# WebAssembly and WebForms Core&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A C# method running inside WebAssembly can use:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;and produce WebForms Core commands.&lt;/p&gt;

&lt;p&gt;That creates a pipeline 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;C# WebAssembly
      ↓
  WebForms.cs
      ↓
WebForms Core Response
      ↓
WebForms Core Client Engine
      ↓
     DOM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WebAssembly code does not need to know how the browser DOM is implemented.&lt;/p&gt;

&lt;p&gt;It can describe the desired UI behavior through WebForms Core.&lt;/p&gt;




&lt;h1&gt;
  
  
  Native WASM and .NET WebCIL Are Different
&lt;/h1&gt;

&lt;p&gt;WebForms Core can work with different types of WebAssembly environments.&lt;/p&gt;

&lt;p&gt;For example, a native WASM module may expose a function such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and can be loaded directly as a &lt;code&gt;.wasm&lt;/code&gt; module.&lt;/p&gt;

&lt;p&gt;The .NET approach is different.&lt;/p&gt;

&lt;p&gt;With the .NET WebCIL environment, the entry point is:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;and the .NET runtime loads the necessary WebAssembly components.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Native WASM
     ↓
module.wasm
     ↓
WebAssembly.instantiate()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is conceptually different from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.NET WebAssembly
     ↓
dotnet.js
     ↓
.NET runtime
     ↓
C# exported method
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebForms Core can provide an abstraction over these different execution models.&lt;/p&gt;




&lt;h1&gt;
  
  
  No JavaScript Business Logic
&lt;/h1&gt;

&lt;p&gt;One of the most interesting properties of this example is that the application does not require custom JavaScript business logic.&lt;/p&gt;

&lt;p&gt;The C# code contains:&lt;br&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="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;SetData&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;WebForms&lt;/span&gt; &lt;span class="n"&gt;form&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;WebForms&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetText&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetBackgroundColor&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFontSize&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Response&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 HTML contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"WasmEvent"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Wasm Event
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the WebForms Core server code connects them:&lt;br&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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetWasmEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WasmEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HtmlEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmLanguage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSharpMediator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WasmPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"MyClass.SetData"&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 application logic remains in C#.&lt;/p&gt;




&lt;h1&gt;
  
  
  WebForms Core 2.1
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;WebForms Core 2.1, or simply WFC, is coming soon.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This release expands the possibilities of WebForms Core by allowing WebAssembly methods to participate more deeply in the WebForms Core execution model.&lt;/p&gt;

&lt;p&gt;The .NET WebCIL scenario is particularly interesting because developers can write C# methods such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSExport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;SetData&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and use the existing WebForms Core API inside those methods.&lt;/p&gt;

&lt;p&gt;This means that the same WebForms Core command model can be used from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server-side C#&lt;/li&gt;
&lt;li&gt;C# WebAssembly&lt;/li&gt;
&lt;li&gt;Native WebAssembly modules&lt;/li&gt;
&lt;li&gt;Other WASM-capable languages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;while the browser continues to use the WebForms Core client engine (&lt;a href="https://elanat.net/page_content/web_forms_js" rel="noopener noreferrer"&gt;WebFormsJS&lt;/a&gt;).&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Result
&lt;/h1&gt;

&lt;p&gt;This example demonstrates a new direction for WebForms Core:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C# WebAssembly can become an execution layer for WebForms Core rather than simply a replacement for JavaScript.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The architecture can be summarized as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              WebForms Core
                    │
        ┌───────────┴───────────┐
        │                       │
     Server                 WebAssembly
        │                       │
   WebForms.cs               C# / WASM
        │                       │
        └───────────┬───────────┘
                    ↓
          WebForms Core Commands
                    ↓
               Browser DOM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And perhaps the most interesting point is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WebAssembly does not have to replace the WebForms Core model. It can become another execution environment for it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That opens the door to using high-performance compiled languages and .NET WebAssembly together with the declarative command architecture of WebForms Core.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebForms Core 2.1 is coming soon!&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  WebForms Core vs Blazor WebAssembly
&lt;/h1&gt;

&lt;p&gt;Compared with &lt;strong&gt;Blazor WebAssembly&lt;/strong&gt;, this approach in WebForms Core is not intended to run a complete .NET application in the browser. Instead, it can &lt;strong&gt;invoke a specific C# method as a WASM capability&lt;/strong&gt; and return its result to WebForms Core. In the example above, &lt;code&gt;MyClass.Add&lt;/code&gt;, &lt;code&gt;MyClass.SetData&lt;/code&gt;, and &lt;code&gt;MyClass.GetHtml&lt;/code&gt; are executed only when they are actually needed. Therefore, the architecture is much closer to a &lt;strong&gt;WASM Function Runtime&lt;/strong&gt; than to a WASM-based SPA. Furthermore, by using &lt;code&gt;WebForms&lt;/code&gt; within the C# code, even UI modification logic can be defined directly inside the C# method, with its output then passed to the WebForms Core engine.&lt;/p&gt;

&lt;p&gt;In contrast, &lt;strong&gt;Blazor WASM&lt;/strong&gt; provides a complete programming model for building .NET user interfaces in the browser, with a significant portion of the application logic and required runtime running on the client. WebForms Core can take a different approach: &lt;strong&gt;HTML remains standard HTML, WebForms Core manages UI behavior, and WASM is used only when specific capabilities need to be executed.&lt;/strong&gt; Therefore, the two are not necessarily direct competitors. Blazor WASM primarily focuses on running .NET applications in the browser through WebAssembly, whereas WebForms Core is &lt;strong&gt;not limited to WebAssembly and provides a broader architecture for developing and executing web behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Related links
&lt;/h2&gt;

&lt;p&gt;WebForms Core in GitHub:&lt;br&gt;
&lt;a href="https://github.com/webforms-core" rel="noopener noreferrer"&gt;https://github.com/webforms-core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>dotnet</category>
      <category>tutorial</category>
      <category>webformscore</category>
    </item>
    <item>
      <title>redb.Route: Control Bus for .NET routes: stop, restart and react to route events at runtime</title>
      <dc:creator>rinat kozin</dc:creator>
      <pubDate>Wed, 26 Aug 2026 09:31:00 +0000</pubDate>
      <link>https://dev.to/rinat_kozin/redb-control-bus-for-net-routes-stop-restart-and-react-to-route-events-at-runtime-329h</link>
      <guid>https://dev.to/rinat_kozin/redb-control-bus-for-net-routes-stop-restart-and-react-to-route-events-at-runtime-329h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9gomscnav641nziiswl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj9gomscnav641nziiswl.png" alt="redb.Route" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manage routes at runtime by sending a message: start, stop, suspend, restart, plus a consumer that turns route lifecycle events into messages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A running integration is not a static thing. A deploy needs to drain a queue and stop a route before the new build takes over. A downstream service falls over, and the route hammering it should back off instead of piling up retries. An operator needs to suspend one branch of the flow without restarting the whole process. Something fails at 3am, and the on-call dashboard should light up on its own, not because someone was tailing logs.&lt;/p&gt;

&lt;p&gt;Apache Camel solved the operational half of this a long time ago with the &lt;strong&gt;Control Bus&lt;/strong&gt; pattern: you manage routes by sending a message to a special endpoint, the same way you move any other message. redb.Route brings that pattern to .NET, with the Camel command set, and adds one thing Camel does not have: a consumer that turns route lifecycle events back into messages you can route.&lt;/p&gt;

&lt;p&gt;So there are two directions here. Outbound: tell a route to start, stop, suspend, resume or restart. Inbound: subscribe to what routes are doing and react. Both are ordinary steps of a pipeline. Let us look at the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control Bus in one minute
&lt;/h2&gt;

&lt;p&gt;The component is registered out of the box, there is no package to add. Address it by URI or by the fluent DSL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stop a route by id&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controlbus:route?routeId=orders&amp;amp;action=stop"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Same thing, fluent&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ControlBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a producer step. When a message reaches it, the named route is stopped. Everything else is variations on the action and one consumer for events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manage a route by sending a message
&lt;/h2&gt;

&lt;p&gt;The action is the verb. The full set mirrors Camel:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start the route.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop the route (consumer removed, the route stays registered).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Suspend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Suspend the route.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Resume&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resume a stopped or suspended route.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Restart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop, then start after &lt;code&gt;restartDelay&lt;/code&gt; (default 1000 ms).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Report the route's status.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Report the route's statistics.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Fail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stop the route and mark it failed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the fluent form the action is an enum; as a URI it is the &lt;code&gt;action&lt;/code&gt; query parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;redb.Route.ControlBus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ControlBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Suspend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"payments"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ControlBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// fire-and-forget&lt;/span&gt;

&lt;span class="c1"&gt;// URI equivalents&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controlbus:route?routeId=payments&amp;amp;action=suspend"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controlbus:route?routeId=orders&amp;amp;action=restart&amp;amp;async=true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it is a normal route step, the control action can be the tail of any pipeline. A timer that suspends a batch route outside business hours. A webhook that restarts a route on a config change. A choice branch that stops a consumer when a poison message is seen. You are not calling a management API from the outside; you are routing a message, with all the same retries, error handling and observability as the rest of the flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  A route can manage itself
&lt;/h2&gt;

&lt;p&gt;Pass &lt;code&gt;current&lt;/code&gt; as the route id and the action targets the route that is sending the message. This is how a route reacts to its own condition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://orders"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CheckHealth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Choice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"downstreamDown"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ControlBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Suspend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// back off, stop consuming&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A route that detects its downstream is unhealthy suspends itself instead of spinning through failures. Something external, a timer or an operator message, resumes it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part Camel does not have: react to route events
&lt;/h2&gt;

&lt;p&gt;This is the direction that is usually missing. In Camel the Control Bus is producer-only: you send commands, you do not subscribe to what happens. redb.Route adds &lt;code&gt;controlbus:notify&lt;/code&gt;, a &lt;strong&gt;consumer&lt;/strong&gt; that emits route and context lifecycle events as messages. You put it on the &lt;code&gt;From&lt;/code&gt; side and route the events anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controlbus:notify"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusHeaders&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="c1"&gt;// RouteStarted, RouteStopped, RouteErrored, ...&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"kafka://route-events"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The events cover the lifecycle of routes and of the context itself: &lt;code&gt;RouteStarted&lt;/code&gt;, &lt;code&gt;RouteStopped&lt;/code&gt;, &lt;code&gt;RouteSuspending&lt;/code&gt;, &lt;code&gt;RouteErrored&lt;/code&gt;, &lt;code&gt;ContextStarting&lt;/code&gt;, &lt;code&gt;ContextStarted&lt;/code&gt;, &lt;code&gt;ContextStopping&lt;/code&gt;, &lt;code&gt;ContextStopped&lt;/code&gt;, &lt;code&gt;ExchangeTimedOut&lt;/code&gt;. Each message carries the details as headers: the event name, the affected route id, a timestamp, and where relevant the error, the exchange id and the elapsed time.&lt;/p&gt;

&lt;p&gt;Filter to what you care about with the &lt;code&gt;events&lt;/code&gt; and &lt;code&gt;routeId&lt;/code&gt; 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="c1"&gt;// Only failures and stops, only for the orders route&lt;/span&gt;
&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controlbus:notify?events=RouteErrored,RouteStopped&amp;amp;routeId=orders"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"slack://alerts"&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 interesting part is that both directions compose. Events on the way in, commands on the way out, in one route: this is a self-healing loop with no external control plane.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;From&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controlbus:notify?events=RouteErrored"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;In&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RouteId&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ControlBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ControlBusAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// or the captured route id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An errored route emits an event, a route consumes it, waits, and restarts the offender. The supervision logic is a pipeline, visible and testable like any other, not a hardcoded policy buried in the engine.&lt;/p&gt;

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

&lt;p&gt;Without a Control Bus, runtime route management is a pile of one-off plumbing. A custom admin controller that reaches into the engine to stop a route. A &lt;code&gt;BackgroundService&lt;/code&gt; that polls a flag table to know when to pause. A logging appender wired to an alerting SDK so someone hears about a failure. Each is a separate mechanism, with its own lifecycle, its own tests, its own way of being wrong.&lt;/p&gt;

&lt;p&gt;The Control Bus turns all of that into routing. Management commands are messages &lt;code&gt;To&lt;/code&gt; an endpoint. Lifecycle events are messages &lt;code&gt;From&lt;/code&gt; an endpoint. They go through the same DSL, the same error handling, the same OpenTelemetry traces as your business flows, and they land in the same place your team already looks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-downtime deploys&lt;/strong&gt;: suspend and drain a route on a signal, flip, resume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpressure and circuit-breaking at the route level&lt;/strong&gt;: a route suspends itself when its downstream is unhealthy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational events as data&lt;/strong&gt;: pipe &lt;code&gt;RouteErrored&lt;/code&gt; and &lt;code&gt;RouteStopped&lt;/code&gt; into Kafka, Slack, a metrics sink, an incident tool, an audit log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-healing&lt;/strong&gt;: notify in, restart out, in one small route.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The same control from a dashboard: redb.Tsak
&lt;/h2&gt;

&lt;p&gt;Everything above is the programmatic face: a message drives a route's lifecycle. There is an operational face too. &lt;strong&gt;redb.Tsak&lt;/strong&gt;, the runtime that hosts your routes, exposes the same route and context lifecycle in a dashboard, so an operator can start, stop, suspend or resume a route, and manage whole contexts, live, without writing a route or shipping a deploy. One capability, two entry points: a message from inside a flow, or a click from outside it, over the same engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Camel parity, and beyond
&lt;/h2&gt;

&lt;p&gt;If you come from Camel, the command side is familiar: &lt;code&gt;controlbus:route&lt;/code&gt; with &lt;code&gt;routeId&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt;, plus &lt;code&gt;controlbus:language&lt;/code&gt; for expression-based control, producer-only, the standard verbs. redb.Route matches that command set. The &lt;code&gt;controlbus:notify&lt;/code&gt; consumer is the addition: Camel gives you EventNotifier as an SPI you implement in Java and register; redb.Route gives you the same information as a first-class endpoint you route from, no interface to implement, no wiring, just &lt;code&gt;From("controlbus:notify")&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is it a separate package?&lt;/strong&gt; No. Control Bus is part of core &lt;code&gt;redb.Route&lt;/code&gt; and is registered out of the box. Nothing to install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does stopping a route remove it?&lt;/strong&gt; No. &lt;code&gt;Stop&lt;/code&gt; and &lt;code&gt;Suspend&lt;/code&gt; remove the consumer but keep the route registered, so &lt;code&gt;Resume&lt;/code&gt; or &lt;code&gt;Start&lt;/code&gt; brings it back. &lt;code&gt;Fail&lt;/code&gt; stops it and marks it errored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a route control another route, not just itself?&lt;/strong&gt; Yes. Pass the target route id instead of &lt;code&gt;current&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What events are available?&lt;/strong&gt; Route lifecycle (&lt;code&gt;RouteStarted&lt;/code&gt;, &lt;code&gt;RouteStopped&lt;/code&gt;, &lt;code&gt;RouteSuspending&lt;/code&gt;, &lt;code&gt;RouteErrored&lt;/code&gt;), context lifecycle (&lt;code&gt;ContextStarting&lt;/code&gt; through &lt;code&gt;ContextStopped&lt;/code&gt;), and &lt;code&gt;ExchangeTimedOut&lt;/code&gt;. Filter with &lt;code&gt;events=&lt;/code&gt; and &lt;code&gt;routeId=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the command synchronous?&lt;/strong&gt; By default yes; pass &lt;code&gt;async=true&lt;/code&gt; (or &lt;code&gt;async: true&lt;/code&gt; in the DSL) for fire-and-forget, which also avoids a route trying to stop itself synchronously mid-exchange.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it lives
&lt;/h2&gt;

&lt;p&gt;Control Bus ships inside &lt;a href="https://www.nuget.org/packages/redb.Route/" rel="noopener noreferrer"&gt;redb.Route on NuGet&lt;/a&gt;; the DSL and the &lt;code&gt;controlbus:notify&lt;/code&gt; event set are in the &lt;a href="https://github.com/redbase-app/redb-route" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. It is one of the 30+ EIP patterns in the framework, and like the rest it is a step of a route: the same &lt;code&gt;From → … → To&lt;/code&gt;, the same observability. The difference is that the message it carries is a route telling you what it just did, or you telling a route what to do next.&lt;/p&gt;

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




&lt;p&gt;More of my writing: &lt;a href="https://redbase.app/articles" rel="noopener noreferrer"&gt;redbase.app/articles&lt;/a&gt;, and on &lt;a href="https://dev.to/rinat_kozin"&gt;dev.to&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
      <category>esb</category>
    </item>
    <item>
      <title>NET Framework Essentials: Web Development Simplified</title>
      <dc:creator>Sahil Khurana</dc:creator>
      <pubDate>Wed, 26 Aug 2026 09:20:32 +0000</pubDate>
      <link>https://dev.to/sahil_khurana_486f374ecf2/net-framework-essentials-web-development-simplified-5gn1</link>
      <guid>https://dev.to/sahil_khurana_486f374ecf2/net-framework-essentials-web-development-simplified-5gn1</guid>
      <description>&lt;p&gt;Your backend framework will outlive your current team. Choose one that the &lt;em&gt;next&lt;/em&gt; team can still navigate — here's why .NET has been that framework for Netflix, GitHub, and Stack Overflow for over two decades.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Twenty-three years. That's how long .NET has been running in production. Most frameworks from that era got abandoned, forked beyond recognition, or replaced entirely — .NET kept showing up. Netflix still uses it. GitHub uses it. Stack Overflow, which has probably saved more developer careers than any single resource on the internet, runs on ASP.NET. None of these teams are using it out of inertia. They're using it because it works under conditions that expose every weakness in a poorly designed system. This article gets into how .NET actually works, what it gives teams day-to-day, and whether it makes sense for what you're building now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One codebase, five platforms&lt;/strong&gt; — Windows, macOS, Linux, Android, iOS. No rewrites, no platform-specific forks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three languages, one project&lt;/strong&gt; — C#, F#, and Visual Basic coexist without forcing a rewrite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The performance tooling ships with it&lt;/strong&gt; — JIT compiler, AOT compiler, CLR memory management, Garbage Collector. All out of the box.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Is .NET Still Around?
&lt;/h2&gt;

&lt;p&gt;Honestly, this question is worth sitting with for a second — because in software, most things don't survive twenty years. They solve the problem of the moment, get widely adopted before anyone finds the sharp edges, and then get quietly replaced when something newer comes along and the migration pain seems worth it.&lt;/p&gt;

&lt;p&gt;.NET didn't go that way.&lt;/p&gt;

&lt;p&gt;Some of that is Microsoft backing — resources, long-term support commitments, a developer community that doesn't dissolve when priorities shift. But backing alone doesn't explain it. Plenty of well-resourced frameworks have died. What actually kept .NET alive is that the foundational architecture held up. The cross-platform capability wasn't duct-taped on in 2020 because everyone suddenly cared about Linux. It was in the compiler design from early on. When .NET Core went open-source and the licensing fees went away, it wasn't a rebrand — it was the framework catching up to how people already wanted to use it.&lt;/p&gt;

&lt;p&gt;There's a second reason, quieter but just as important. Code gets handed off. The engineers who built version one are almost never the engineers maintaining version four. Comments get stale. Context lives in heads that leave the company. The frameworks that age well are the ones that stay readable, stay consistent, and don't require tribal knowledge to navigate after three years of turnover. .NET handles that transition better than most.&lt;/p&gt;




&lt;h2&gt;
  
  
  How .NET Works — No Documentation Filler
&lt;/h2&gt;

&lt;p&gt;Here's the actual sequence, stripped of the marketing layer.&lt;/p&gt;

&lt;p&gt;Developer opens Visual Studio or VS Code. Writes in C#, F#, or Visual Basic — whichever the team is using. The compiler takes that code and converts it into &lt;strong&gt;Common Intermediate Language (CIL)&lt;/strong&gt;. This isn't Windows-specific code or Linux-specific code. It's a neutral middle format — doesn't care about the OS — and it gets stored in an assembly file.&lt;/p&gt;

&lt;p&gt;User runs the app. The &lt;strong&gt;Common Language Runtime (CLR)&lt;/strong&gt; takes over as the execution engine. It passes the CIL to a &lt;strong&gt;Just-in-Time (JIT) compiler&lt;/strong&gt;, which converts it into actual machine code for whatever OS is running at that exact moment.&lt;/p&gt;

&lt;p&gt;That's the mechanism. Same assembly, different output depending on the environment. No separate Windows build. No Linux-specific rewrite. One file that works across platforms because the conversion happens at runtime, not at compile time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro tip:&lt;/strong&gt; First time you watch a codebase that lived exclusively on Windows servers for years boot cleanly inside a Linux container, the pipeline earns a whole new level of respect.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's also an &lt;strong&gt;Ahead-of-Time (AOT) compiler&lt;/strong&gt; — does the conversion before runtime instead of during. Better startup speed, lower memory footprint at launch. Some deployments need it; others don't. Both compilers are included, no additional configuration required.&lt;/p&gt;

&lt;p&gt;Under all of this, the &lt;strong&gt;Garbage Collector&lt;/strong&gt; runs continuously, clearing unused objects from memory. The CLR manages allocation and deallocation. Developers don't handle it manually. That removes an entire category of bugs — the memory-related ones that tend to show up in production at the worst possible time — not through careful coding, but through the framework just doing it automatically.&lt;/p&gt;

&lt;p&gt;Here's what a minimal .NET 6 web app entry point looks like:&lt;br&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;// Program.cs — minimal hosting model (.NET 6+)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Hello from .NET"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same file. Runs on Windows, Linux, and macOS without a single change.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Developers Actually Get
&lt;/h2&gt;

&lt;p&gt;Skip the bullet-point feature list for a second, because that format lies through omission — it makes everything look equally important, and it isn't.&lt;/p&gt;

&lt;p&gt;The standard library depth is the thing that matters most on a real project. File handling, networking, security, database access — .NET covers it. Teams aren't patching together five community packages, each with independent release cycles and varying levels of maintainer attention. At any real scale, every third-party dependency is a potential vulnerability and a future upgrade headache. Fewer dependencies isn't just cleaner — it's safer.&lt;/p&gt;

&lt;p&gt;ASP.NET for web, MAUI for mobile, Entity Framework for database management — these live in the same ecosystem. They share conventions, share documentation, and work together without needing compatibility shims. When something breaks at the seam between the web layer and the data layer, there's one system to debug. Teams who've spent weeks chasing down breakage between mismatched third-party libraries know exactly why that matters.&lt;/p&gt;

&lt;p&gt;Language flexibility sounds minor until you're three years into a project and inheriting a module written in F# by a contractor who's long gone. .NET lets C# and F# coexist in the same project — no forced rewrite, no language migration before you can touch the code. It's not a feature you use daily. It's a pressure valve you're glad exists when you need it.&lt;/p&gt;

&lt;p&gt;Security defaults aren't opt-in. Authentication, encryption, access control — built in and maintained by Microsoft's security team, not left to per-developer implementation. Combined with CLR memory management, the framework actively closes the kinds of attack surfaces that manual implementations routinely leave cracked open.&lt;/p&gt;

&lt;p&gt;Azure integration is genuinely worth something for enterprise teams already in the Microsoft world. SQL Server, Microsoft 365, Power BI — same vendor, same support chain. It just fits. No custom connectors, no compatibility hacks, no extra configuration layer sitting between the app and the infrastructure.&lt;/p&gt;

&lt;p&gt;.NET Core and .NET 5+ are open-source. Zero licensing fees. Across a team of ten people on a three-year project, that's a real line item — the kind that shows up in engineering budget reviews and doesn't get questioned.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Gets Built on .NET
&lt;/h2&gt;

&lt;p&gt;Enterprise apps are the obvious fit. Complex security requirements, multi-platform deployment needs, long timelines, existing Microsoft infrastructure — .NET was built for exactly that profile and the production record backs it up.&lt;/p&gt;

&lt;p&gt;Web apps and REST APIs through ASP.NET are well-trodden. Stack Overflow processes millions of requests through ASP.NET under load conditions that surface architectural problems fast. It holds.&lt;/p&gt;

&lt;p&gt;Windows desktop software still largely runs on WPF and WinForms. Mobile through MAUI — which replaced Xamarin — lets a single codebase target both Android and iOS. Cloud-first work fits naturally on Azure, though .NET runs on AWS and GCP too. Azure is just the path with the least friction.&lt;/p&gt;

&lt;p&gt;Microservices, internal tooling, cloud-native applications — all reasonable use cases. The framework is broad enough that it's genuinely hard to name a common app type it doesn't handle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Should You Use It?
&lt;/h2&gt;

&lt;p&gt;Depends entirely on the context.&lt;/p&gt;

&lt;p&gt;Enterprise build, long timeline, Microsoft infrastructure already in place, security requirements with actual teeth — yes, probably. .NET was designed for that profile. The fit is real.&lt;/p&gt;

&lt;p&gt;Small team, Python or Node.js fluency across the board, project scope that doesn't need what .NET specifically does well — switching creates friction without proportional benefit. That's not a criticism of the framework. It's just a mismatch.&lt;/p&gt;

&lt;p&gt;The thing worth pushing back on is choosing frameworks based on the demo. Demos are fast, clean, and don't have to survive a team changing twice and a product pivot. The real question is what it looks like to maintain the codebase in three years. On that measure, .NET has a long track record that newer options simply don't have yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts: Is .NET the Right Call for Your Project?
&lt;/h2&gt;

&lt;p&gt;Two decades in production is a real signal. Not a marketing claim, not a vanity metric — actual evidence that the thing holds up when conditions change. New platforms arrived. Deployment models shifted. Team expectations around open-source changed completely. .NET adjusted each time without breaking what was already working.&lt;/p&gt;

&lt;p&gt;What you build on top of it inherits that history. Not glamorous. Probably not the choice that gets applause in a tech talk. But solid, well-supported, and unlikely to become someone else's problem to unwind in two years.&lt;/p&gt;

&lt;p&gt;If you want to talk through whether it fits what you're working on, the &lt;a href="https://innostax.com/blog/everything-you-need-to-know-about-net-framework-for-web-development/" rel="noopener noreferrer"&gt;Innostax team&lt;/a&gt; has been in this space long enough to give you a straight answer.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sahil Khurana&lt;/strong&gt; - Chief Technology Officer at &lt;a href="//Innostax.com"&gt;Innostax&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Innostax is a global software consulting and custom development company helping growth-stage startups, scaleups, and enterprises build reliable, scalable digital products. Founded in 2014, headquartered in Framingham, Massachusetts.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webdev</category>
      <category>backend</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
