<script data-pm-proxy="intercept"></script><?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Distributed Systems with .Net]]></title><description><![CDATA[Distributed Systems with .NET teaches C# engineers how to build production-grade distributed systems using .NET Aspire, Azure Service Bus, Azure Event Hubs, Application Insights, Azure Data Explorer, DAPR, and Bicep — entirely on free Microsoft emulator]]></description><link>https://distributeddotnet.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!ODvG!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7954cfb7-539e-4e23-9be9-40abdc5ac8cd_768x768.png</url><title>Distributed Systems with .Net</title><link>https://distributeddotnet.substack.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 03 Sep 2026 09:48:20 GMT</lastBuildDate><atom:link href="/__u/distributeddotnet.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Distributed Systems with .Net]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[distributeddotnet@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[distributeddotnet@substack.com]]></itunes:email><itunes:name><![CDATA[Distributed Systems with .Net]]></itunes:name></itunes:owner><itunes:author><![CDATA[Distributed Systems with .Net]]></itunes:author><googleplay:owner><![CDATA[distributeddotnet@substack.com]]></googleplay:owner><googleplay:email><![CDATA[distributeddotnet@substack.com]]></googleplay:email><googleplay:author><![CDATA[Distributed Systems with .Net]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Post 28: Dotnet-counters + Flame Graphs: Fix .NET p99 Latency with IMemoryCache | Post 28]]></title><description><![CDATA[Post 28 of 45 &#183; Phase 7: Profiling and Optimisation &#183; 18 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-28-dotnet-counters-flame-graphs</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-28-dotnet-counters-flame-graphs</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Wed, 02 Sep 2026 02:30:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!5vTo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The NBomber report from Post 27 shows p99 latency spiking to 1250ms at 200 RPS. That answers WHERE the ceiling is &#8212; but not WHY. Something in the <code>OrderService</code> request pipeline becomes expensive under load. To find it, you need a profiler: a tool that samples the CPU stack thousands of times per second while the load runs, then aggregates those samples into a flame graph.</p><p>Post 28 covers two tools in the .NET diagnostics toolchain. <code>dotnet-counters</code> streams lightweight runtime metrics to the terminal &#8212; CPU usage, GC heap size, allocation rate, thread pool queue length &#8212; with zero performance overhead. <code>dotnet-trace</code> captures a <code>.nettrace</code> file during a 30-second peak load window. PerfView (Windows) or SpeedScope (cross-platform) opens that file as a flame graph showing exactly which function is consuming CPU at 200 RPS.</p><p>The answer &#8212; in this system &#8212; is <code>TenantConfigStore.GetTenantConfigAsync</code>. Every order creation request hits Azure Table Storage once for the tenant configuration lookup (the <code>TenantAuthorizationHandler</code> from Post 21). At 200 RPS that is 200 Table Storage round trips per second. Adding <code>IMemoryCache</code> with a 30-second TTL reduces that to one round trip every 30 seconds and drops p99 by approximately 40%.</p><p>No new code this post &#8212; only two commits: <code>docs/profiling-notes.md</code> (the profiling workflow) and a <code>MemoryCache</code> addition to <code>TenantConfigStore</code>. <code>verify.ps1</code> grows to 61 checks.</p><blockquote><p><strong>Career skill this post adds:</strong> <code>dotnet-counters</code> + <code>dotnet-trace</code> + PerfView / SpeedScope flame graph analysis &#8212; the four-step .NET performance engineering workflow. Every Azure .NET senior engineer interview that asks &#8220;how did you diagnose a latency regression?&#8221; expects this answer: counters for situation awareness, trace for root cause, flame graph for the hot path, targeted fix, re-run load test. The <code>TenantConfigStore</code> cache optimisation pattern &#8212; per-key TTL cache for a lookup called on every authenticated request &#8212; is a recurring pattern in multi-tenant ASP.NET Core APIs.</p></blockquote><ul><li><p><code>dotnet-counters</code> &#8212; which counters to watch at 200 RPS and what each signals</p></li><li><p><code>dotnet-trace</code> &#8212; capturing a <code>.nettrace</code> file, which providers to enable</p></li><li><p>Flame graph anatomy &#8212; width, height, hot leaves vs wide roots</p></li><li><p>The hot path: <code>TenantConfigStore</code> &#8594; <code>TableClient.GetEntityAsync</code> at 200 RPS</p></li><li><p><code>IMemoryCache</code> addition &#8212; the fix and why 30-second TTL is appropriate</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>The full Phase 1&#8211;6 stack is deployed: 7 services, 4 emulators, Dapr sidecars, 8 integration tests, NBomber load test. Post 28 adds profiling tooling and the first performance optimisation &#8212; a cache in <code>TenantConfigStore</code>. No new NuGet packages beyond what is already in the base library.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/28-perfview</code> <strong>Install once:</strong> <code>dotnet tool install -g dotnet-counters</code> &#183; <code>dotnet tool install -g dotnet-trace</code> Both are official Microsoft .NET diagnostics tools. Cross-platform.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!5vTo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!5vTo!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png 424w, /__u/substackcdn.com/image/fetch/$s_!5vTo!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png 848w, /__u/substackcdn.com/image/fetch/$s_!5vTo!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png 1272w, /__u/substackcdn.com/image/fetch/$s_!5vTo!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!5vTo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png" width="1350" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1350,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:197910,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/212268834?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!5vTo!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png 424w, /__u/substackcdn.com/image/fetch/$s_!5vTo!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png 848w, /__u/substackcdn.com/image/fetch/$s_!5vTo!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png 1272w, /__u/substackcdn.com/image/fetch/$s_!5vTo!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd61c45e-548a-42fc-8404-c68fb5ead3df_1350x720.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-28-dotnet-counters-flame-graphs">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Post 27: Build a Hyperscale .NET Microservices App with Aspire, OpenTelemetry & NBomber Load Testing]]></title><description><![CDATA[Post 27 of 45 &#183; Phase 6: Testing and Load Testing &#183; 19 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-27-build-a-hyperscale-net-microservices</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-27-build-a-hyperscale-net-microservices</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Mon, 31 Aug 2026 02:30:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1eqj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Post 26 added integration tests that verify correctness under normal conditions &#8212; one request, one response, assert the result. Post 27 adds load tests that verify behaviour under sustained throughput &#8212; 50, 100, and 200 requests per second for 30 seconds each, with p50 and p99 latency captured per step.</p><p>The load test reveals something integration tests cannot: where the system degrades. At 50 RPS the p99 is 85ms. At 100 RPS it is 210ms. At 200 RPS it is 1100ms. That non-linear jump between 100 and 200 RPS is the signal &#8212; something saturates around 100 RPS. The HTML report produced by NBomber identifies whether the bottleneck is the Service Bus emulator CPU, the Azurite Table Storage connection pool in <code>TenantAuthorizationHandler</code>, or the JWT validation overhead accumulating under parallel requests.</p><p>NBomber is code-first. The load plan is a C# console application &#8212; not a JMeter <code>.jmx</code> XML file, not a Visual Studio <code>.loadtest</code> XML file. The scenario references the same <code>appsettings.Development.json</code> JWT configuration as the application. The <code>Simulation.Inject</code> model matches the arrival rate pattern of real HTTP traffic: N new requests per second, regardless of system speed.</p><p><code>verify.ps1</code> grows to 59 checks. Two new checks confirm the <code>tests/LoadTests/</code> project exists and contains the NBomber package reference.</p><blockquote><p><strong>Career skill this post adds:</strong> NBomber load testing with stepped load simulations &#8212; <code>Simulation.Inject</code> for constant arrival rate, <code>WithWarmUpDuration</code> for JIT/connection stabilisation, and the interpretation of p50 vs p99 divergence under load. Every Azure .NET performance engineering role asks &#8220;how did you measure the throughput ceiling of the system?&#8221; The answer is a stepped load test with a clear bottleneck identification process.</p></blockquote><ul><li><p><code>Simulation.Inject</code> vs <code>KeepConstant</code> &#8212; arrival rate vs fixed concurrency, when each applies</p></li><li><p>Warm-up duration &#8212; why first-JIT and cold connection overhead contaminates results without it</p></li><li><p>p50 vs p99 divergence &#8212; the signal that a non-linear bottleneck exists</p></li><li><p>Reading the NBomber HTML report &#8212; latency histogram, RPS achieved vs requested, error table</p></li><li><p>Common bottlenecks on local emulators and their production equivalents</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Posts 24&#8211;25 added Dapr. Post 26 added Testcontainers integration tests. Post 27 adds NBomber as a second test type in <code>tests/</code> &#8212; a console application that measures sustained throughput behaviour. The application under test is unchanged; the full Phase 1&#8211;5 stack runs via Aspire.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/27-nbomber</code> <strong>Prerequisite:</strong> Aspire stack must be running (<code>dotnet run --project src/AppHost</code>). The load test acquires a JWT from UserService and then drives <code>POST /orders/create</code> on OrderService.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!1eqj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!1eqj!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png 424w, /__u/substackcdn.com/image/fetch/$s_!1eqj!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png 848w, /__u/substackcdn.com/image/fetch/$s_!1eqj!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png 1272w, /__u/substackcdn.com/image/fetch/$s_!1eqj!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!1eqj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png" width="1456" height="712" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:712,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:908413,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/212258060?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!1eqj!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png 424w, /__u/substackcdn.com/image/fetch/$s_!1eqj!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png 848w, /__u/substackcdn.com/image/fetch/$s_!1eqj!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png 1272w, /__u/substackcdn.com/image/fetch/$s_!1eqj!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F75ab8c4a-bfdc-436d-b825-21ef45decada_5400x2640.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-27-build-a-hyperscale-net-microservices">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Post 26:.NET 8 Integration Testing with Testcontainers & Azurite | Aspire, OpenTelemetry & Azure SDK ]]></title><description><![CDATA[Post 26 of 45 &#183; Phase 6: Testing and Load Testing &#183; 20 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-26net-8-integration-testing</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-26net-8-integration-testing</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Sat, 29 Aug 2026 02:30:46 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!AES4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every post through Phase 5 has a <code>verify.ps1</code> that tests the running system by calling HTTP endpoints and checking log output. That is end-to-end testing &#8212; valuable, but slow: it requires the full Aspire stack, all four Docker emulators, and 8-45 second waits per check. <code>verify.ps1</code> is not a substitute for unit and integration tests that can run in under 10 seconds in CI without a running Aspire Dashboard.</p><p>Post 26 adds integration tests. Not unit tests with mocked Azure SDK clients &#8212; mocking <code>TableServiceClient</code> tests that your code calls <code>GetEntityAsync</code> with specific arguments, not that it returns the right result when the table contains specific data. Integration tests with Testcontainers start a real <code>mcr.microsoft.com/azure-storage/azurite</code> Docker container inside the test process, run <code>TenantConfigStore</code> and <code>OrderBlobStore</code> against it, and confirm behaviour that matters: the path structure is correct, second writes are idempotent, unknown tenants return null.</p><p>Eight tests across two test classes. Each class starts one Azurite container, runs four tests against it, and stops the container. The same Azurite image used in <code>docker-compose.yml</code> and <code>.WithDaprSidecar()</code> runs in the tests. There is no configuration difference between local development and CI.</p><blockquote><p><strong>Career skill this post adds:</strong> Testcontainers integration testing for Azure SDK components &#8212; <code>AzuriteContainer</code> from <code>Testcontainers.Azurite 3.9.0</code>, <code>IAsyncLifetime</code> for container lifecycle, and the principle that integration tests should use the same Docker images as local development. Appears in senior .NET engineer roles that list &#8220;integration testing&#8221; and &#8220;CI/CD&#8221; alongside &#8220;Azure SDK.&#8221; The argument for Testcontainers over mocks &#8212; testing behaviour not implementation &#8212; is a system design interview answer for &#8220;how do you test code that uses external services?&#8221;</p></blockquote><ul><li><p>Why mocks of Azure SDK clients give false confidence &#8212; what they test vs what they don&#8217;t</p></li><li><p><code>Testcontainers.Azurite</code> &#8212; the typed container builder and <code>IAsyncLifetime</code> lifecycle</p></li><li><p><code>AzuriteContainer.GetConnectionString()</code> &#8212; the bridge from container to SDK client</p></li><li><p>Test isolation without container restart &#8212; unique IDs per test</p></li><li><p>What the 8 tests cover: path convention, idempotency, null for unknown tenant, <code>ContentType</code></p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Phase 5 complete (Posts 24&#8211;25): Dapr service invocation and Dapr pub/sub. Phase 6 begins with testing infrastructure. The application code under test (<code>TenantConfigStore</code>, <code>OrderBlobStore</code>) has not changed since Posts 17 and 16. Post 26 adds <code>tests/IntegrationTests/</code> as a new project that references the service projects directly.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/26-testcontainers</code> <strong>Prerequisite:</strong> Docker must be running. Testcontainers pulls the Azurite image and starts containers. <code>dotnet test tests/IntegrationTests</code> must pass all 8 tests before running <code>.\verify.ps1</code>.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!AES4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!AES4!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png 424w, /__u/substackcdn.com/image/fetch/$s_!AES4!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png 848w, /__u/substackcdn.com/image/fetch/$s_!AES4!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png 1272w, /__u/substackcdn.com/image/fetch/$s_!AES4!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!AES4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png" width="1456" height="696" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:696,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1228299,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/212248914?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!AES4!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png 424w, /__u/substackcdn.com/image/fetch/$s_!AES4!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png 848w, /__u/substackcdn.com/image/fetch/$s_!AES4!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png 1272w, /__u/substackcdn.com/image/fetch/$s_!AES4!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffa258623-2fe5-420e-aab5-3aa550b17428_5400x2580.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-26net-8-integration-testing">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 25:.NET Aspire Microservices Demo: Dapr Pub/Sub, Azure Emulators & OpenTelemetry on .NET 8 ]]></title><description><![CDATA[Post 25 of 45 &#183; Phase 5: Dapr and Testcontainers &#183; 18 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-25net-aspire-microservices-demo</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-25net-aspire-microservices-demo</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Thu, 27 Aug 2026 02:30:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!RnyU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Post 24 added Dapr service invocation &#8212; the building block for service-to-service calls. Post 25 adds Dapr pub/sub &#8212; the building block for event publishing. Where Phase 2 used <code>ServiceBusSender.SendMessageAsync</code> to publish <code>OrderCreatedEvent</code> directly to an Azure Service Bus queue, Post 25 adds a parallel path: <code>daprClient.PublishEventAsync("order-pubsub", "orders", event)</code>. The application code is simpler. The backing broker is determined by a YAML file.</p><p>The key property of Dapr pub/sub: the component YAML (<code>config/dapr/components/pubsub.yaml</code>) is the only thing that changes between Redis locally and Azure Service Bus in production. The <code>PublishEventAsync</code> call &#8212; and the <code>[Topic]</code> subscriber attribute &#8212; are identical in both environments. The application code has no <code>namespace.servicebus.windows.net</code>, no <code>SharedAccessKey</code>, no <code>ServiceBusClient</code>. Those are broker configuration, not application code.</p><p><code>verify.ps1</code> grows to 55 checks. The final check calls <code>/orders/create-dapr</code> and verifies the &#8220;Order published via Dapr pub/sub&#8221; log record in the OTel Collector &#8212; confirming the Dapr sidecar received the publish call and routed it to the Redis component.</p><blockquote><p><strong>Career skill this post adds:</strong> Dapr pub/sub with <code>PublishEventAsync</code> and <code>[Topic]</code> subscription &#8212; the broker-agnostic event publishing pattern. The <code>pubsub.yaml</code> component swap (Redis &#8594; Azure Service Bus &#8594; Azure Event Hubs &#8594; RabbitMQ) is the key demonstration: the same application code works with four different brokers. Appears in Azure .NET distributed systems roles that list Dapr alongside &#8220;event-driven architecture.&#8221;</p></blockquote><ul><li><p>Dapr pub/sub vs direct Azure Service Bus SDK &#8212; what changes, what stays the same</p></li><li><p><code>pubsub.yaml</code> component YAML &#8212; the three lines that change for production</p></li><li><p><code>daprClient.PublishEventAsync</code> &#8212; the publish call</p></li><li><p><code>[Topic]</code> attribute / <code>.WithTopic()</code> &#8212; the subscriber binding</p></li><li><p>ACK/NACK via HTTP response codes &#8212; simpler than Service Bus <code>CompleteAsync</code>/<code>AbandonAsync</code></p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Post 24 added Dapr sidecar support (<code>WithDaprSidecar()</code>) to both UserService and OrderService. Post 25 adds the pub/sub building block on top of the same sidecars. The Redis component (<code>dapr init</code> installs a local Redis container automatically) handles message routing.</p><p>The Phase 2 Service Bus pipeline remains active and unchanged. Post 25 adds a parallel Dapr pub/sub path at <code>/orders/create-dapr</code>. Both paths publish <code>OrderCreatedEvent</code>. The goal is comparison, not replacement &#8212; at this stage in the series.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/25-dapr-pubsub</code> Redis must be running (<code>dapr init</code> starts it automatically). Verify: <code>docker ps | grep redis</code>.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!RnyU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!RnyU!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png 424w, /__u/substackcdn.com/image/fetch/$s_!RnyU!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png 848w, /__u/substackcdn.com/image/fetch/$s_!RnyU!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png 1272w, /__u/substackcdn.com/image/fetch/$s_!RnyU!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!RnyU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png" width="1456" height="679" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:679,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1098904,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/212243761?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!RnyU!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png 424w, /__u/substackcdn.com/image/fetch/$s_!RnyU!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png 848w, /__u/substackcdn.com/image/fetch/$s_!RnyU!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png 1272w, /__u/substackcdn.com/image/fetch/$s_!RnyU!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6cce8c74-ab85-4cbf-ad0a-6dbd491a61c1_5400x2520.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-25net-aspire-microservices-demo">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 24 : Dapr Service Invocation with .NET Aspire | Build Microservices Communication in ASP.NET Core]]></title><description><![CDATA[Post 24 of 45 &#183; Phase 5: Dapr and Testcontainers &#183; 21 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-24-dapr-service-invocation-with</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-24-dapr-service-invocation-with</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Tue, 25 Aug 2026 02:30:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!4YL9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><code>OrderService</code> calls <code>UserService</code> in Phase 5 &#8212; not via direct HTTP with <code>HttpClient</code>, but via the Dapr sidecar. The request goes: OrderService &#8594; local Dapr sidecar (localhost:3500) &#8594; UserService Dapr sidecar &#8594; UserService handler. What the sidecar adds for free: automatic retries, circuit breaker, mTLS between services, and distributed trace propagation. What the application code changes: one method call. The handler in <code>UserService</code> does not change at all.</p><p>Post 24 introduces the Dapr Distributed Application Runtime &#8212; the sidecar architecture that moves infrastructure capabilities (service discovery, retries, circuit breakers) out of application code and into a process that runs alongside every service. Two packages, two lines in <code>AppHost/Program.cs</code>, one new endpoint. <code>verify.ps1</code> grows to 53 checks.</p><blockquote><p><strong>Career skill this post adds:</strong> Dapr service invocation with <code>DaprClient.InvokeMethodAsync</code> &#8212; the pattern that replaces <code>HttpClient</code> for service-to-service calls in a Dapr-enabled microservice system. Appears on Azure .NET senior engineer roles listing Dapr or <code>Microsoft.Extensions.Http.Resilience</code>. The sidecar model explanation is a system design interview answer for &#8220;how do you build resilient microservices without coupling resilience to business logic?&#8221;</p></blockquote><ul><li><p>The Dapr sidecar model &#8212; why it exists, what it handles, what it doesn&#8217;t</p></li><li><p><code>WithDaprSidecar()</code> in Aspire AppHost &#8212; the one-line addition per service</p></li><li><p><code>DaprClient.InvokeMethodAsync</code> &#8212; the application-side call</p></li><li><p>Distributed trace propagation through the sidecar &#8212; cross-service span in Aspire Dashboard</p></li><li><p>Prerequisites: <code>dapr init</code> and the daprd binary</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Phase 4 is complete (Posts 20&#8211;23). Phase 5 introduces Dapr as an infrastructure layer that augments the existing Service Bus and Event Hubs messaging with sidecar-managed service-to-service resilience.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/24-dapr-sidecar</code> <strong>Prerequisites:</strong> <code>dapr init</code> must have been run. On Windows: <code>winget install Dapr.CLI</code> then <code>dapr init</code>. Verify: <code>dapr --version</code>.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!4YL9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!4YL9!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png 424w, /__u/substackcdn.com/image/fetch/$s_!4YL9!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png 848w, /__u/substackcdn.com/image/fetch/$s_!4YL9!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png 1272w, /__u/substackcdn.com/image/fetch/$s_!4YL9!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!4YL9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png" width="1456" height="712" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:712,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1130073,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/212106968?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!4YL9!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png 424w, /__u/substackcdn.com/image/fetch/$s_!4YL9!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png 848w, /__u/substackcdn.com/image/fetch/$s_!4YL9!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png 1272w, /__u/substackcdn.com/image/fetch/$s_!4YL9!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffbe9087-e690-4701-95e6-3cd3bb8e8f14_5400x2640.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-24-dapr-service-invocation-with">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Post 23: Build an SRE Dashboard in .NET | Hyperscale Log Monitoring with Azure App Insights & KQL]]></title><description><![CDATA[Post 23 of 45 &#183; Phase 4: Authentication and Security &#183; 18 min read &#183; No Azure subscription required to build &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-23-build-an-sre-dashboard-in</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-23-build-an-sre-dashboard-in</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Mon, 17 Aug 2026 02:30:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/K6tixlBSHv8" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The system has OTel traces in the Aspire Dashboard, telemetry in App Insights, KQL queries in <code>docs/kql-queries.md</code>, and Sentinel detection rules in <code>docs/sentinel-detection-rules.md</code>. What it does not have: a single view that answers the question an on-call engineer asks at 3 AM &#8212; &#8220;Is the system healthy right now?&#8221;</p><p>Post 23 adds two documents that together answer that question. <code>docs/sre-dashboard.json</code> is an Azure Dashboard JSON template with eight tiles, each backed by a KQL query that was already in <code>docs/kql-queries.md</code>. <code>docs/sre-runbook.md</code> is the operational companion &#8212; for each tile, what the threshold is, what the root cause is, and what to do in the first five minutes.</p><p>Both documents are deployable artifacts. The dashboard imports into the Azure Portal via a JSON upload or deploys via <code>az resource create</code>. The runbook is the document that gets linked from the PagerDuty alert body and opened at 3 AM. Both live in <code>docs/</code> and ship with the repository &#8212; the same <code>git pull</code> that deploys new application code also updates the operational documentation.</p><blockquote><p><strong>Career skill this post adds:</strong> SRE dashboard design thinking &#8212; which metrics to show (actionable, alert-triggering) vs which to hide (interesting, review-only). The four golden signals framework from the Google SRE book provides the structure. Every senior Azure SRE and platform engineer interview includes &#8220;walk me through the observability stack you would build for a distributed system.&#8221; The four golden signals + dashboard + runbook is the answer.</p></blockquote><ul><li><p>The four golden signals: latency, traffic, errors, saturation &#8212; definitions and this system&#8217;s equivalents</p></li><li><p>What belongs on an on-call dashboard vs a weekly review dashboard</p></li><li><p>Azure Dashboard JSON structure &#8212; lenses, parts, positions, KQL-backed tiles</p></li><li><p>The runbook structure &#8212; threshold, severity, immediate steps, root causes</p></li><li><p>Deploying the dashboard via the Azure Portal and <code>az</code> CLI</p></li></ul><div><hr></div><h2>The four golden signals</h2><p>Google&#8217;s Site Reliability Engineering book defines four signals that, together, provide a complete picture of system health. Every production distributed system should expose all four. Post 23 maps each signal to the series&#8217; existing telemetry:</p><p><strong>1. Latency &#8212; how long does a request take?</strong></p><ul><li><p>This system: p99 of <code>/orders/create</code> requests in App Insights <code>requests</code> table</p></li><li><p>The right metric: p99, not average. Averages hide slow outliers. A p99 of 2000ms means 1% of users wait 2 seconds &#8212; which is a real problem that an average of 150ms would conceal.</p></li><li><p>Alert threshold: &gt; 1000ms for 5 consecutive minutes</p></li></ul><p><strong>2. Traffic &#8212; how many requests per unit time?</strong></p><ul><li><p>This system: <code>/orders/create</code> count per 5-minute bucket</p></li><li><p>Why traffic matters: a traffic drop is often the first signal of an upstream issue (API gateway, DNS, client app failure). The service appears healthy but orders are not arriving.</p></li><li><p>Alert threshold: &lt; 10% of 7-day rolling average for 15 consecutive minutes</p></li></ul><p><strong>3. Errors &#8212; what fraction of requests fail?</strong></p><ul><li><p>This system: <code>success == false</code> in <code>requests</code> table + exception count in <code>exceptions</code> table</p></li><li><p>Separate from latency: a fast error (50ms 500 response) is an error, not a slow request. Track both independently.</p></li><li><p>Alert threshold: &gt; 1% error rate for 5 consecutive minutes</p></li></ul><p><strong>4. Saturation &#8212; how &#8220;full&#8221; is the service?</strong></p><ul><li><p>This system: Service Bus DLQ depth + Polly retry rate</p></li><li><p>Why these two: the DLQ is the lagging saturation indicator (messages already failed), while Polly retry rate is the leading indicator (messages about to fail). Monitoring both gives early warning.</p></li><li><p>Alert threshold: DLQ &gt; 0 for any 5-minute bucket; Polly retries &gt; 5/min for 10 minutes</p></li></ul><p><strong>Bridge from familiar:</strong> Windows Performance Monitor (PerfMon) custom views on a Windows server show CPU %, memory %, disk I/O, and network throughput &#8212; equivalent to the four golden signals for infrastructure. The counter log is equivalent to the App Insights timechart. The alert threshold in PerfMon is equivalent to the Azure Monitor alert rule that fires when a KQL query returns results. The four golden signals are the application-layer equivalent of the four infrastructure metrics every Windows administrator has been monitoring since NT 4.0.</p><blockquote><p><strong>Design dashboards for the person who opens them at 3 AM.</strong> That person is usually not the architect who knows the system intimately. They need: one tile per signal (not 30 tiles of interesting metrics), clear threshold labels, and a link to the runbook. &#8220;DLQ Depth&#8221; with no axis label or threshold annotation is useless. &#8220;DLQ Depth &#8212; alert if &gt; 0 (see runbook)&#8221; is actionable. The eight tiles in <code>docs/sre-dashboard.json</code> each have a <code>_comment</code> field with the alert threshold.</p></blockquote><div><hr></div><h2>Core concept: On-call dashboard vs weekly review dashboard</h2><p><strong>The wrong approach:</strong> put every interesting metric on one dashboard. Engineers who build the system know what every metric means. The on-call dashboard for someone unfamiliar with the system becomes noise &#8212; 30 tiles, all green, no idea which one would turn red first.</p><p><strong>The right approach:</strong> two dashboards with different audiences.</p><p><strong>On-call dashboard (this post):</strong> Eight tiles. Four golden signals only. Each tile has a threshold. Opening this dashboard takes 10 seconds to scan. If all eight tiles are green, the system is healthy. If any tile is red, the runbook tells you what to do.</p><p><strong>Weekly review dashboard (not in this series, but the design principle):</strong> Tenant order volume trends, SLA percentile over the last 30 days, cost-per-1000-orders, exception rate comparison week-over-week. These are the metrics that inform architectural decisions and capacity planning. They do not belong on the on-call dashboard.</p><p><strong>The distinction is temporal resolution:</strong> on-call dashboards show the last 1 hour with 5-minute resolution. Weekly review dashboards show the last 30 days with 1-day resolution. The same metric &#8212; error rate &#8212; appears in both, but the time window and alert purpose are different.</p><p><strong>Bridge from familiar:</strong> A Windows Server farm administrator has two views: the System Center Operations Manager (SCOM) alert console (on-call) and the performance reporting dashboard (weekly review). The SCOM console shows only active alerts &#8212; the equivalent of the on-call KQL dashboard. The performance reports show capacity trends &#8212; the equivalent of the weekly review dashboard.</p><div><hr></div><h2>Core concept: Azure Dashboard JSON structure</h2><h3>Github Link:</h3><h4>https://github.com/sysdr/dotnet-system-design-p/tree/main/post-23-v3-source-code<br></h4><p><strong>The JSON schema:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;0f0f3c2e-af70-46a8-b4f0-95fae939aed0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "type": "Microsoft.Portal/dashboards",
  "properties": {
    "lenses": {
      "0": {                          // first row of tiles
        "parts": {
          "0": {                      // first tile
            "position": { "x": 0, "y": 0, "colSpan": 6, "rowSpan": 4 },
            "metadata": {
              "type": "Extension/AppInsightsExtension/PartType/AnalyticsPart",
              "inputs": [
                { "name": "Query", "value": "requests | where ..." },
                { "name": "Title", "value": "p99 Latency" }
              ]
            }
          }
        }
      }
    }
  }
}
</code></pre></div><p><strong>Position system:</strong> The dashboard grid is 12 columns wide. <code>colSpan: 6</code> makes a tile half-width. <code>colSpan: 12</code> makes a tile full-width. <code>rowSpan: 4</code> is the standard chart height. Tiles are positioned with <code>x</code> (column) and <code>y</code> (row) &#8212; a <code>colSpan: 6</code> tile at <code>x: 0</code> and another at <code>x: 6</code> are side by side.</p><p><strong>Part type for KQL tiles:</strong> <code>Extension/AppInsightsExtension/PartType/AnalyticsPart</code> &#8212; this is the App Insights Logs tile type. The <code>ComponentId</code> input is the full Azure resource ID of the App Insights instance. The <code>Query</code> input is the KQL string.</p><p><strong>Tokens requiring replacement before deployment:</strong></p><ul><li><p><code>SUBSCRIPTION_ID</code> &#8212; your Azure subscription GUID</p></li><li><p><code>YOUR_RG</code> &#8212; the resource group containing App Insights</p></li><li><p><code>YOUR_APP_INSIGHTS</code> &#8212; the App Insights resource name</p></li></ul><pre><code><code># Replace tokens in PowerShell before deployment
$dashboard = Get-Content docs/sre-dashboard.json -Raw
$dashboard = $dashboard.Replace("SUBSCRIPTION_ID", $env:AZURE_SUBSCRIPTION_ID)
$dashboard = $dashboard.Replace("YOUR_RG",         $env:AZURE_RESOURCE_GROUP)
$dashboard = $dashboard.Replace("YOUR_APP_INSIGHTS", "hyperscale-appinsights")
$dashboard | Set-Content docs/sre-dashboard-deployed.json
</code></code></pre><p><strong>Deployment:</strong></p><pre><code><code>az resource create \
  --resource-group YOUR_RG \
  --resource-type Microsoft.Portal/dashboards \
  --name hyperscale-sre-dashboard \
  --properties @docs/sre-dashboard-deployed.json
</code></code></pre><h3>Working Demo Link:</h3><div id="youtube2-K6tixlBSHv8" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;K6tixlBSHv8&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/K6tixlBSHv8?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h3>Verify</h3><pre><code><code>.\verify.ps1
</code></code></pre><pre><code><code>-- Post 23 checks (new &#8212; SRE dashboard) ----------------
  docs/sre-dashboard.json exists ... PASS
  docs/sre-runbook.md exists      ... PASS
========================================================
 All 51 checks passed. Post 23 complete.
</code></code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!eUoR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!eUoR!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png 424w, /__u/substackcdn.com/image/fetch/$s_!eUoR!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png 848w, /__u/substackcdn.com/image/fetch/$s_!eUoR!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png 1272w, /__u/substackcdn.com/image/fetch/$s_!eUoR!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!eUoR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png" width="1456" height="809" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/deb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:809,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:576311,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/211490159?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!eUoR!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png 424w, /__u/substackcdn.com/image/fetch/$s_!eUoR!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png 848w, /__u/substackcdn.com/image/fetch/$s_!eUoR!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png 1272w, /__u/substackcdn.com/image/fetch/$s_!eUoR!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdeb5c2e8-6a02-4d03-992a-6b236dff0ff4_5400x3000.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-23-build-an-sre-dashboard-in">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 22: .NET Aspire Microservices with OpenTelemetry, Serilog & Observability Dashboard]]></title><description><![CDATA[Post 22 of 45 &#183; Phase 4: Authentication and Security &#183; 19 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-22-net-aspire-microservices</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-22-net-aspire-microservices</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Sat, 15 Aug 2026 02:31:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gDW2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><code>AppHost/Program.cs</code> has existed since Post 1. It has grown from two <code>AddProject</code> calls to seven services, four emulator containers, and JWT configuration that must be consistent across two services. Post 22 explains what it actually is: infrastructure-as-code in C#.</p><p>The claim is not marketing. <code>AppHost/Program.cs</code> is a dependency graph. The graph says: SQL Edge must be healthy before the Service Bus emulator starts. The Service Bus emulator must be healthy before <code>OrderService</code> starts. <code>UserService</code> and <code>OrderService</code> must share the same JWT signing key. <code>OrderService</code> must know <code>UserService</code>&#8216;s URL for Phase 5 Dapr integration. Every one of those relationships is now expressed as a compile-time-checkable C# declaration, not as a YAML string that fails at runtime.</p><p><code>WaitFor()</code> is the first new API. It replaces the 35-second <code>Start-Sleep</code> that has been in every <code>verify.ps1</code> since Post 9. <code>WithEnvironment("Jwt__SigningKey", key)</code> is the second &#8212; it injects the shared JWT key from the AppHost into both services, so a key change requires one edit in one place. <code>WithReference(userService)</code> is the third &#8212; it injects <code>UserService</code>&#8216;s URL into <code>OrderService</code> via Aspire&#8217;s service discovery conventions.</p><p>The Aspire manifest &#8212; generated by <code>dotnet run --project src/AppHost --publisher manifest</code> &#8212; is JSON that describes the same graph for deployment tools: <code>azd</code> for Azure Container Apps, Aspirate for Kubernetes Helm charts, and the Bicep generator that Post 35 uses.</p><blockquote><p><strong>Career skill this post adds:</strong> .NET Aspire <code>AppHost</code> IaC patterns &#8212; <code>WaitFor</code>, <code>WithReference</code>, <code>WithEnvironment</code>, and the manifest. Appears on every Azure .NET role that lists <code>.NET Aspire</code> in its stack requirements. The <code>AppHost</code> pattern is the mechanism that makes microservice local development tractable: one <code>dotnet run</code> command for the entire system, with correct startup ordering and environment configuration, without manual port management or YAML fragility.</p></blockquote><ul><li><p><code>WaitFor(resource)</code> &#8212; explicit startup ordering vs <code>Start-Sleep</code> polling</p></li><li><p><code>WithEnvironment("Section__Key", value)</code> &#8212; double-underscore nested config injection</p></li><li><p><code>WithReference(service)</code> &#8212; URL injection for service-to-service discovery</p></li><li><p>The Aspire manifest &#8212; the deployment artifact generated from the same graph</p></li><li><p>Docker Compose comparison table &#8212; <code>depends_on</code>, <code>environment</code>, <code>networks</code> equivalents</p></li></ul><div><hr></div><h2>System state: where we are</h2><p><code>AppHost/Program.cs</code> is rewritten in Post 22. All functionality is identical &#8212; the services start in the same order, the same configuration is applied. What changes: the startup ordering is now explicit (<code>WaitFor</code>) rather than implicit (35-second sleep), the JWT key is injected by the AppHost rather than read only from <code>appsettings.Development.json</code>, and the service dependency graph is type-safe (renaming a resource breaks compilation, not runtime).</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/22-aspire-iac</code> Run <code>.\verify.ps1</code> &#8212; all 47 Post 1&#8211;21 checks must pass. The 49th check verifies <code>WaitFor()</code> is in <code>AppHost/Program.cs</code>; the 48th verifies <code>Jwt__SigningKey</code> environment injection.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!gDW2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!gDW2!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png 424w, /__u/substackcdn.com/image/fetch/$s_!gDW2!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png 848w, /__u/substackcdn.com/image/fetch/$s_!gDW2!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png 1272w, /__u/substackcdn.com/image/fetch/$s_!gDW2!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!gDW2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png" width="1456" height="841" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:841,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1096062,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/209909697?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!gDW2!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png 424w, /__u/substackcdn.com/image/fetch/$s_!gDW2!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png 848w, /__u/substackcdn.com/image/fetch/$s_!gDW2!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png 1272w, /__u/substackcdn.com/image/fetch/$s_!gDW2!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc80ea2ac-2adb-4d04-84ba-fd956b89a50e_5400x3120.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-22-net-aspire-microservices">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 21: .NET Aspire Dashboard: OpenTelemetry Microservices Logging & JWT Auth ]]></title><description><![CDATA[Post 21 of 45 &#183; Phase 4: Authentication and Security &#183; 22 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-21-net-aspire-dashboard-opentelemetry</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-21-net-aspire-dashboard-opentelemetry</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Thu, 13 Aug 2026 02:31:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!DR_I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Post 20 added a valid JWT requirement to <code>/orders/create</code>. That stopped unauthenticated calls. But it did not stop valid calls from disabled tenants. A <code>tenant_id = "tenant-99"</code> claim in a perfectly valid JWT would create orders for a tenant that does not exist in <code>TenantConfigStore</code>.</p><p>Post 21 closes that gap with a custom authorization policy. The <code>RequireEnabledTenant</code> policy combines the Post 20 JWT validation with a runtime lookup in the Post 17 <code>TenantConfigStore</code>: does this tenant exist, and is it enabled? If the Table Storage entry says <code>IsEnabled = false</code>, the request returns 403 Forbidden &#8212; even with a fully valid JWT.</p><p>This post also covers the production upgrade from the local symmetric key to <code>Microsoft.Identity.Web</code> &#8212; the NuGet package that wraps <code>JwtBearer</code> for Azure AD / Entra ID, eliminating the shared secret and replacing it with OIDC discovery. <code>DefaultAzureCredential</code>, the credential chain used for managed identities, is covered here because it is the same pattern that connects <code>Microsoft.Identity.Web</code> in AKS to <code>TenantConfigStore</code> in Azure Table Storage without any stored credentials.</p><blockquote><p><strong>Career skill this post adds:</strong> ASP.NET Core custom authorization policies &#8212; <code>IAuthorizationRequirement</code> + <code>AuthorizationHandler&lt;T&gt;</code> + <code>AddAuthorization(options =&gt; options.AddPolicy(...))</code>. The three-type pattern is the correct approach when authorization logic requires DI-injected services. It separates the <em>what</em> (requirement) from the <em>how</em> (handler) from the <em>where</em> (policy registration). Appears in every enterprise ASP.NET Core codebase that has multi-tenant or role-based access control beyond simple <code>[Authorize(Roles = "Admin")]</code>.</p></blockquote><ul><li><p>Three authorization policy types: role-based, claims-based, resource-based &#8212; when each applies</p></li><li><p><code>IAuthorizationRequirement</code> + <code>AuthorizationHandler&lt;T&gt;</code> &#8212; the two types and why both are needed</p></li><li><p><code>context.Succeed(requirement)</code> vs <code>context.Fail()</code> &#8212; the explicit contract of authorization handlers</p></li><li><p><code>Microsoft.Identity.Web</code> &#8212; what changes and what stays the same when upgrading from local JWT</p></li><li><p><code>DefaultAzureCredential</code> &#8212; the credential chain for managed identities</p></li></ul><div><hr></div><h2>System state: where we are</h2><p><code>OrderService</code> has JWT bearer validation (Post 20) and <code>TenantConfigStore</code> (Post 17). Post 21 connects them: the JWT claim <code>tenant_id</code> is looked up in <code>TenantConfigStore</code> at authorization time. If the tenant is not found or <code>IsEnabled = false</code>, the request is denied before the handler runs.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/21-custom-auth-policy</code> Run <code>.\verify.ps1</code> &#8212; all 45 Post 1&#8211;20 checks must pass. Checks that call <code>/orders/create</code> use the <code>Get-Token</code> helper function that calls <code>/auth/token</code> with the tenant-01 defaults.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!DR_I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!DR_I!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png 424w, /__u/substackcdn.com/image/fetch/$s_!DR_I!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png 848w, /__u/substackcdn.com/image/fetch/$s_!DR_I!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png 1272w, /__u/substackcdn.com/image/fetch/$s_!DR_I!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!DR_I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png" width="1456" height="777" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:777,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1099396,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/209357819?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!DR_I!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png 424w, /__u/substackcdn.com/image/fetch/$s_!DR_I!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png 848w, /__u/substackcdn.com/image/fetch/$s_!DR_I!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png 1272w, /__u/substackcdn.com/image/fetch/$s_!DR_I!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8f95876-8e2d-43f0-a850-8e5e0a2d2d33_5400x2880.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-21-net-aspire-dashboard-opentelemetry">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 20: JWT Authentication in .NET Aspire Microservices | Secure APIs with JwtBearer]]></title><description><![CDATA[Post 20 of 45 &#183; Phase 4: Authentication and Security &#183; 21 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-20-wt-authentication-in-net</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-20-wt-authentication-in-net</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Tue, 11 Aug 2026 02:31:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!jBU7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><code>OrderService</code> has been open since Post 1. Any process with network access can POST to <code>/orders/create</code>. On a developer&#8217;s laptop, that is acceptable. In a staging environment, it is not &#8212; and in production, it is a critical security gap. Phase 4 closes it.</p><p>Post 20 adds JWT bearer authentication to <code>OrderService</code>. Unprotected POST to <code>/orders/create</code> &#8594; 401 Unauthorized. POST with a valid <code>Authorization: Bearer {token}</code> header &#8594; 200, message published to Service Bus. The token is issued by a new <code>/auth/token</code> test endpoint in <code>UserService</code> &#8212; a stand-in for the Azure AD token issuance that happens in production.</p><p>Two packages, one configuration change, two source files. The local development path uses a symmetric HMAC-SHA256 signing key in <code>appsettings.Development.json</code>. The production path replaces four lines with <code>Microsoft.Identity.Web</code> and an <code>AzureAd</code> configuration section &#8212; no code change, no new package, no subscription required to build and test it locally.</p><blockquote><p><strong>Career skill this post adds:</strong> JWT bearer authentication in ASP.NET Core 8 Minimal APIs &#8212; <code>builder.Services.AddAuthentication().AddJwtBearer()</code>, <code>app.UseAuthentication()</code>, <code>.RequireAuthorization()</code>. The OTel event hooks that propagate claim values to the Aspire Dashboard trace are the production debugging pattern that turns a 401 from &#8220;unknown failure&#8221; to &#8220;specific validation error in the span.&#8221; Appears on every Azure .NET senior role that lists API security alongside observability.</p></blockquote><ul><li><p>JWT structure &#8212; header, claims payload, signature &#8212; and what <code>[Authorize]</code> verifies</p></li><li><p><code>AddAuthentication().AddJwtBearer()</code> &#8212; the three-line middleware registration</p></li><li><p><code>TokenValidationParameters</code> &#8212; the six properties and what each does</p></li><li><p>Local symmetric key &#8594; production OIDC discovery &#8212; the one-section configuration swap</p></li><li><p>OTel event hooks on <code>OnAuthenticationFailed</code> and <code>OnTokenValidated</code></p></li><li><p><code>.RequireAuthorization()</code> vs <code>.AllowAnonymous()</code> &#8212; selective endpoint protection</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Phase 3 is complete. <code>OrderService</code> has Service Bus publishing, Event Hubs dual-streaming, Blob Storage writes, Table Storage config lookups, full App Insights telemetry, and zero authentication. Post 20 adds the security layer without changing any of the messaging or storage code.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/20-jwt-auth</code> Run <code>.\verify.ps1</code> &#8212; all 43 Phase 1&#8211;3 checks must pass. <strong>Important:</strong> <code>verify.ps1</code> checks 17&#8211;29 now fetch a token from <code>/auth/token</code> before calling <code>/orders/create</code>. If <code>/auth/token</code> fails, those checks will fail. Confirm <code>UserService</code> is healthy before running the full suite.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!jBU7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!jBU7!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png 424w, /__u/substackcdn.com/image/fetch/$s_!jBU7!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png 848w, /__u/substackcdn.com/image/fetch/$s_!jBU7!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png 1272w, /__u/substackcdn.com/image/fetch/$s_!jBU7!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!jBU7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png" width="1456" height="809" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:809,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1192617,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/209348703?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!jBU7!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png 424w, /__u/substackcdn.com/image/fetch/$s_!jBU7!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png 848w, /__u/substackcdn.com/image/fetch/$s_!jBU7!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png 1272w, /__u/substackcdn.com/image/fetch/$s_!jBU7!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa12d0660-9015-4163-960a-05969eec6d0c_5400x3000.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-20-wt-authentication-in-net">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 19: Microsoft Sentinel KQL Detection Rules for .NET — Brute Force, Anomaly Detection & App Insights]]></title><description><![CDATA[Post 19 of 45 &#183; Phase 3: Storage and Observability (final post) &#183; 23 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-19-microsoft-sentinel-kql-detection</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-19-microsoft-sentinel-kql-detection</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Sun, 09 Aug 2026 02:30:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!zNAo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Post 18 covered the universal KQL operators &#8212; the pipe chain, time functions, aggregation, and rendering &#8212; that work identically in Application Insights, Azure Monitor, ADX, and Sentinel.</p><p>Post 19 covers what makes KQL unique: the operators with no SQL equivalent. <code>make-series</code> converts a tabular result into a time-series array. <code>series_decompose_anomalies</code> runs a machine-learning decomposition against that array and returns which points are statistically anomalous. <code>mv-expand</code> explodes array columns back into rows so you can filter on the anomaly scores. And <code>workspace()</code> lets a single query span multiple Log Analytics workspaces &#8212; the multi-region observability join.</p><p>Post 19 also covers Sentinel detection rules &#8212; the KQL queries that run on a schedule and create Incidents when their results are non-empty. The five rules in <code>docs/sentinel-detection-rules.md</code> cover brute-force login detection, anomalous order volume, DLQ accumulation, elevated <code>DeliveryCount</code>, and Polly retry storm proximity. All five have App Insights equivalents testable against real series telemetry without a Sentinel licence.</p><p><code>/users/login-failed</code> is added to <code>UserService</code> &#8212; a new endpoint that generates failed login records for the brute-force detection exercise. Call it 10 times in rapid succession; the KQL rule returns the IP and user count that triggered the threshold.</p><blockquote><p><strong>Career skill this post adds:</strong> <code>make-series</code> + <code>series_decompose_anomalies</code> &#8212; the KQL pattern that appears in advanced Azure Monitor alert rules and Sentinel hunting queries. Sentinel SIEM KQL authoring appears on Azure security engineer and senior architect job specifications. The detection rule pattern &#8212; &#8220;query on a schedule, alert if rows returned&#8221; &#8212; is the same structure used in every enterprise SIEM including Splunk, Elastic SIEM, and IBM QRadar. Learning it in Sentinel transfers across products.</p></blockquote><ul><li><p><code>make-series</code> &#8212; converting tabular counts into a time-series array</p></li><li><p><code>series_decompose_anomalies</code> &#8212; ML anomaly detection in one function call</p></li><li><p><code>mv-expand</code> &#8212; exploding array results back into filterable rows</p></li><li><p><code>workspace()</code> and <code>cluster()</code> &#8212; cross-workspace and cross-cluster queries</p></li><li><p>Sentinel detection rules &#8212; the &#8220;query on schedule &#8594; alert if non-empty&#8221; pattern</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Phase 3 is complete after Post 19. The documentation set (<code>docs/</code>) now contains:</p><ul><li><p><code>telemetry-schema.md</code> &#8212; the OTel attribute contract</p></li><li><p><code>storage-decision.md</code> &#8212; 5-service decision framework</p></li><li><p><code>blob-lifecycle-policy.json</code> &#8212; 4 lifecycle rules for Azurite containers</p></li><li><p><code>kql-queries.md</code> &#8212; 12+ production App Insights queries</p></li><li><p><code>kql-operators-reference.md</code> &#8212; operator quick reference</p></li><li><p><code>sentinel-detection-rules.md</code> &#8212; 5 Sentinel/App Insights detection rules &#8592; Post 19</p></li></ul><blockquote><p><strong>Starting point:</strong> <code>git checkout post/19-kql-advanced</code> Run <code>.\verify.ps1</code> &#8212; all 41 Post 1&#8211;18 checks must pass. Run <code>/users/login-failed</code> 10 times to generate exercise data before starting the brute-force rule exercise</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!zNAo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!zNAo!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg 424w, /__u/substackcdn.com/image/fetch/$s_!zNAo!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg 848w, /__u/substackcdn.com/image/fetch/$s_!zNAo!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg 1272w, /__u/substackcdn.com/image/fetch/$s_!zNAo!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!zNAo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg" width="1456" height="874" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:874,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9174,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/svg+xml&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/209341971?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!zNAo!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg 424w, /__u/substackcdn.com/image/fetch/$s_!zNAo!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg 848w, /__u/substackcdn.com/image/fetch/$s_!zNAo!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg 1272w, /__u/substackcdn.com/image/fetch/$s_!zNAo!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60fcea2c-1c36-4083-a77a-d80fdef55fcf_900x540.svg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-19-microsoft-sentinel-kql-detection">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Post 18: KQL Fundamentals for .NET Aspire | Query OpenTelemetry Traces in Application Insights]]></title><description><![CDATA[Post 18 of 45 &#183; Phase 3: Storage and Observability &#183; 22 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-18-kql-fundamentals-for-net</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-18-kql-fundamentals-for-net</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Fri, 07 Aug 2026 02:30:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CxO_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You have been writing KQL since Post 2. <code>ExcludedTypes = "Exception"</code> in <code>appsettings.json</code> tells the Application Insights sampling processor which telemetry types to exclude &#8212; that configuration decision is documented in the App Insights KQL tables as <code>severityLevel</code>. The <code>SetTag(TelemetryConstants.AttrTenantId, tenantId)</code> calls in Posts 3&#8211;5 put <code>TenantId</code> into <code>customDimensions</code>. The <code>LogWarning("DLQ non-empty. {DeadLetterCount}...")</code> in Post 11 created <code>traces</code> records with <code>severityLevel = 2</code>. Post 15 showed you the five tables those actions write to.</p><p>Post 18 teaches you the query language that reads them.</p><p>KQL is a read-only query language designed for time-series telemetry. The same pipe syntax &#8212; <code>table | operator | operator | operator</code> &#8212; works in Application Insights, Azure Monitor Log Analytics, Azure Data Explorer, and Microsoft Sentinel. The table names change. The operators do not.</p><p>Post 18 adds <code>docs/kql-operators-reference.md</code> (the operator quick reference added to the repository alongside the production queries in <code>docs/kql-queries.md</code>) and <code>/orders/metrics</code> (a new endpoint that generates the <code>Tenant stats</code> log records the lesson&#8217;s exercises are built around). Three concept sections cover the pipe operator, time-based queries, and aggregation with rendering. The exercises build five queries from scratch.</p><blockquote><p><strong>Career skill this post adds:</strong> KQL query writing &#8212; the skill that separates engineers who use Application Insights from engineers who understand it. Post-incident reviews, capacity planning, cost analysis, and alert rule authoring all require KQL. It appears on Azure senior engineer job specifications alongside &#8220;Azure Monitor&#8221; and &#8220;Application Insights&#8221; as a required skill, not a nice-to-have.</p></blockquote><ul><li><p>The pipe operator and execution order &#8212; why the order of <code>where | project | summarize</code> matters</p></li><li><p><code>ago()</code>, <code>bin()</code>, <code>datetime_diff()</code> &#8212; the time functions that make time-series queries possible</p></li><li><p><code>summarize count() by bin(timestamp, 1h)</code> &#8212; the canonical volume-over-time query</p></li><li><p><code>percentile(duration, 99)</code> &#8212; p99 latency without downloading raw data</p></li><li><p><code>render timechart</code> &#8212; one line that turns a KQL result into a chart</p></li><li><p><code>let</code> statements &#8212; named subqueries that make complex queries readable</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Post 15 mapped the five App Insights tables. Post 18 teaches the queries. The system has been generating real telemetry since Post 1 &#8212; every order created, every DLQ entry, every Polly retry, every EH checkpoint update is in App Insights. The exercises in this post are written against that real data.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/18-kql-fundamentals</code> Run <code>.\verify.ps1</code> &#8212; all 39 Post 1&#8211;17 checks must pass. Then call <code>/orders/metrics</code> a few times to generate the <code>Tenant stats</code> telemetry the exercises use.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!CxO_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!CxO_!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png 424w, /__u/substackcdn.com/image/fetch/$s_!CxO_!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png 848w, /__u/substackcdn.com/image/fetch/$s_!CxO_!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png 1272w, /__u/substackcdn.com/image/fetch/$s_!CxO_!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!CxO_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png" width="1456" height="777" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:777,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:948434,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/209069426?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!CxO_!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png 424w, /__u/substackcdn.com/image/fetch/$s_!CxO_!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png 848w, /__u/substackcdn.com/image/fetch/$s_!CxO_!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png 1272w, /__u/substackcdn.com/image/fetch/$s_!CxO_!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffe5eaa40-1a80-46fd-88c4-4659600bfedc_5400x2880.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-18-kql-fundamentals-for-net">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Post 17: .NET Aspire Microservices with OpenTelemetry: User Service, Order Service & Log Monitoring Demo]]></title><description><![CDATA[Post 17 of 45 &#183; Phase 3: Storage and Observability &#183; 21 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-17-net-aspire-microservices</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-17-net-aspire-microservices</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Wed, 05 Aug 2026 02:31:01 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!usA5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Post 16 wrote processed order records to Azurite with a path structure designed for two things: year/month prefix for lifecycle tiering, and tenantId segment for per-tenant deletion. Post 17 adds what makes that design meaningful &#8212; the lifecycle policy that automates the hot &#8594; cool &#8594; archive &#8594; delete transitions automatically, and Azure Table Storage for the per-tenant configuration data that determines how many orders each tenant can place per hour.</p><p>Two deliverables, two storage concerns. The lifecycle policy (<code>docs/blob-lifecycle-policy.json</code>) is four JSON rules that run in Azure Storage without any application code &#8212; a cron job built into the storage layer itself. The tenant configuration store (<code>TenantConfigStore</code>) uses Azurite&#8217;s Table endpoint (port 10002, already running) to store per-tenant rate limits and feature flags as Table entities with O(1) point lookup.</p><p>Post 17 closes out Phase 3&#8217;s storage layer. After Post 17, the system has a storage service for every data type in <code>docs/storage-decision.md</code>: Service Bus for commands (Phase 2), Event Hubs for events (Phase 2), Blob Storage for objects (Post 16), Table Storage for key-value (Post 17). Phase 8 adds ADX for time-series. Every service has a local Docker emulator and a one-line connection string swap for production.</p><blockquote><p><strong>Career skill this post adds:</strong> Azure Blob lifecycle policies and <code>Azure.Data.Tables</code> SDK &#8212; <code>TableServiceClient</code>, <code>TableClient</code>, <code>ITableEntity</code>, <code>GetEntityAsync</code>, <code>QueryAsync</code>, <code>UpsertEntityAsync</code>. The <code>PartitionKey</code> design question (&#8221;how to distribute data across storage nodes while enabling efficient queries&#8221;) is asked in every Azure senior engineer interview that mentions Table Storage. The lifecycle policy JSON structure appears in infrastructure-as-code reviews whenever Blob Storage has more than one tier.</p></blockquote><ul><li><p>Blob lifecycle policy rules &#8212; <code>prefixMatch</code>, <code>tierToCool</code>, <code>tierToArchive</code>, <code>delete</code>, <code>daysAfterModificationGreaterThan</code></p></li><li><p><code>Azure.Data.Tables</code> hierarchy &#8212; <code>TableServiceClient &#8594; TableClient &#8594; TableEntity</code></p></li><li><p><code>PartitionKey</code> design &#8212; co-location vs isolation trade-off</p></li><li><p><code>GetEntityAsync</code> vs <code>QueryAsync</code> &#8212; O(1) lookup vs partition scan</p></li><li><p><code>UpsertEntityAsync</code> with <code>Replace</code> vs <code>Merge</code> &#8212; full replace vs field-level update</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Azurite has been running since Post 13 (checkpoint store). Post 16 added the <code>processed-orders</code> Blob container. Post 17 adds:</p><ol><li><p><code>docs/blob-lifecycle-policy.json</code> &#8212; four lifecycle rules applied in Azure Storage</p></li><li><p><code>TenantConfigStore</code> &#8212; reads/writes Table entities in Azurite&#8217;s Table endpoint (port 10002)</p></li><li><p><code>Azure.Data.Tables 12.8.3</code> in <code>OrderService.csproj</code></p></li></ol><blockquote><p><strong>Starting point:</strong> <code>git checkout post/17-table-storage</code> Run <code>.\verify.ps1</code> &#8212; all 37 Post 1&#8211;16 checks must pass.</p></blockquote><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!usA5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!usA5!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png 424w, /__u/substackcdn.com/image/fetch/$s_!usA5!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png 848w, /__u/substackcdn.com/image/fetch/$s_!usA5!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png 1272w, /__u/substackcdn.com/image/fetch/$s_!usA5!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!usA5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png" width="1456" height="906" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:906,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1338942,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/208952075?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!usA5!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png 424w, /__u/substackcdn.com/image/fetch/$s_!usA5!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png 848w, /__u/substackcdn.com/image/fetch/$s_!usA5!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png 1272w, /__u/substackcdn.com/image/fetch/$s_!usA5!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff115b3c0-504e-410a-9fc9-bf3934cd0eb7_5400x3360.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-17-net-aspire-microservices">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Post 16 : Persist Every Processed Order: Azurite Blob Storage in a .NET Aspire Pipeline]]></title><description><![CDATA[Post 16 of 45 &#183; Phase 3: Storage and Observability &#183; 20 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-16-persist-every-processed-order</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-16-persist-every-processed-order</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Mon, 03 Aug 2026 02:30:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!LE_2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After <code>CompleteMessageAsync</code>, the Service Bus message is gone. The order was processed. The event is in Event Hubs. But where is the order record?</p><blockquote><p>Service Bus is not a database. It delivers commands and discards them on success. Event Hubs retains events for 90 days and then discards them too. Neither service was designed to answer the question: &#8220;Show me the processed state of order <code>abc123</code> from last Tuesday.&#8221;</p></blockquote><blockquote><p>Post 16 answers that question with Azure Blob Storage. When <code>OrderProcessorWorker</code> completes a message, it writes a processed order record to Azurite as a JSON blob at <code>processed-orders/{year}/{month}/{tenantId}/{orderId}.json</code>. The record persists indefinitely. The path structure enables lifecycle policy tiering (year/month prefix) and per-tenant GDPR deletion (tenantId segment) &#8212; both patterns run against Azurite locally before touching Azure.</p></blockquote><blockquote><p><code>Azure.Storage.Blobs 12.21.2</code> is the only new NuGet package. The blob client hierarchy, the <code>CreateIfNotExistsAsync</code> startup pattern, blob metadata, and the &#8220;Complete first, write second&#8221; ordering rule &#8212; these are the four concepts Post 16 teaches. All four are verifiable in Azurite with no Azure subscription.</p></blockquote><blockquote><p><strong>Career skill this post adds:</strong> <code>Azure.Storage.Blobs</code> SDK &#8212; <code>BlobServiceClient</code>, <code>BlobContainerClient</code>, <code>BlobClient</code>. Appears on every Azure .NET role that mentions blob storage, export pipelines, or document storage. The <code>CreateIfNotExistsAsync</code> idempotency pattern and the <code>overwrite: false</code> duplicate protection are the two most common interview questions about Blob Storage in a distributed system.</p></blockquote><ul><li><p>The <code>BlobServiceClient</code> &#8594; <code>BlobContainerClient</code> &#8594; <code>BlobClient</code> hierarchy</p></li><li><p><code>CreateIfNotExistsAsync</code> &#8212; idempotent container setup at startup</p></li><li><p>Blob path structure as a query and lifecycle surface (<code>{year}/{month}/{tenantId}/{orderId}</code>)</p></li><li><p>Blob metadata &#8212; the properties you can filter on without downloading the blob</p></li><li><p>&#8220;Complete first, write second&#8221; &#8212; the ordering rule for Service Bus + Blob integration</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Post 13 already added Azurite to <code>docker-compose.yml</code> for Event Hubs checkpoint storage. The <code>eh-checkpoints</code> container is there. Post 16 adds a second container &#8212; <code>processed-orders</code> &#8212; for business data. Same Azurite instance, same connection string (<code>UseDevelopmentStorage=true</code>), second purpose.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/16-azurite-blob</code> Run <code>.\verify.ps1</code> &#8212; all 35 Post 1&#8211;15 checks must pass before adding Post 16 components.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!LE_2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!LE_2!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png 424w, /__u/substackcdn.com/image/fetch/$s_!LE_2!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png 848w, /__u/substackcdn.com/image/fetch/$s_!LE_2!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png 1272w, /__u/substackcdn.com/image/fetch/$s_!LE_2!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!LE_2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png" width="664" height="487.05494505494505" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1068,&quot;width&quot;:1456,&quot;resizeWidth&quot;:664,&quot;bytes&quot;:1143934,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/208423393?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!LE_2!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png 424w, /__u/substackcdn.com/image/fetch/$s_!LE_2!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png 848w, /__u/substackcdn.com/image/fetch/$s_!LE_2!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png 1272w, /__u/substackcdn.com/image/fetch/$s_!LE_2!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F039fbde2-681e-4468-9da8-1acd77c804c2_4500x3300.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-16-persist-every-processed-order">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Post 15 : Deep Dive into Application Insights for Distributed .NET Systems]]></title><description><![CDATA[Post 15 of 45 &#183; Phase 3: Storage and Observability &#183; 22 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-15-deep-dive-into-application</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-15-deep-dive-into-application</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Sat, 01 Aug 2026 02:31:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!WFKB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>Since Post 2, every request to <code>UserService</code> and <code>OrderService</code> has been landing in Application Insights. Every exception, every dependency call, every log record &#8212; all flowing via <code>UseAzureMonitor()</code> to an App Insights workspace. Fourteen posts of data. Most engineers check App Insights when something is broken. Post 15 covers what is in those tables all the time, and the KQL queries that surface problems before they become incidents.</p><p>App Insights organises incoming telemetry into five tables: <code>requests</code>, <code>dependencies</code>, <code>exceptions</code>, <code>traces</code>, and <code>customEvents</code>. Post 15 maps each table to the code that populates it, shows the KQL queries that answer the most common production questions for each, and demonstrates how <code>operation_Id</code> &#8212; the correlation key the OTel SDK writes automatically &#8212; links all five tables back to a single trace visible in the Aspire Dashboard.</p><p>Post 15 also adds an <code>ActivityEvent</code> to <code>OrderProcessorWorker</code>: a <code>customEvent</code> called <code>OrderProcessed</code> that appears in the App Insights <code>customEvents</code> table and carries business properties &#8212; <code>OrderId</code>, <code>TenantId</code>, and <code>DeliveryCount</code> &#8212; as structured dimensions queryable in KQL.</p></blockquote><blockquote><p><strong>Career skill this post adds:</strong> Application Insights KQL querying &#8212; the first skill most Azure teams ask about in observability interviews. &#8220;Show me how you would find all failed requests for a specific tenant in the last hour&#8221; is a question with a specific KQL answer, and engineers who can write it in 30 seconds stand out from those who can only click through the portal UI.</p></blockquote><ul><li><p>The five App Insights tables and what each receives from the series&#8217; OTel setup</p></li><li><p><code>operation_Id</code> &#8212; how one field links requests, dependencies, exceptions, traces, and custom events</p></li><li><p>KQL patterns for each table: latency percentiles, error regression detection, cross-service exception grouping</p></li><li><p><code>ActivityEvent</code> as the App Insights <code>customEvents</code> primitive</p></li><li><p>Adaptive sampling and the <code>itemCount</code> field &#8212; how to detect what is being excluded</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Phase 3 has started. Post 14 added the storage decision framework. Post 15 goes deep on the storage service that has been running since Post 2: Application Insights. No new infrastructure is added &#8212; the App Insights workspace and OTel instrumentation are already in place.</p><p>What Post 15 adds:</p><ul><li><p><code>activity.AddEvent("OrderProcessed")</code> in <code>OrderProcessorWorker</code> &#8212; a <code>customEvent</code> with business dimensions</p></li><li><p><code>docs/appinsights-queries.md</code> &#8212; 20+ production KQL queries across all five tables</p></li></ul><blockquote><p><strong>Starting point:</strong> <code>git checkout post/15-appinsights-deep-dive</code> Run <code>.\verify.ps1</code> &#8212; all 33 Post 1&#8211;14 checks must pass before adding Post 15 components.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!WFKB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!WFKB!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png 424w, /__u/substackcdn.com/image/fetch/$s_!WFKB!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png 848w, /__u/substackcdn.com/image/fetch/$s_!WFKB!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png 1272w, /__u/substackcdn.com/image/fetch/$s_!WFKB!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!WFKB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png" width="676" height="420.64285714285717" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:906,&quot;width&quot;:1456,&quot;resizeWidth&quot;:676,&quot;bytes&quot;:1137155,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/208430099?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!WFKB!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png 424w, /__u/substackcdn.com/image/fetch/$s_!WFKB!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png 848w, /__u/substackcdn.com/image/fetch/$s_!WFKB!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png 1272w, /__u/substackcdn.com/image/fetch/$s_!WFKB!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4121d93-4d40-4b6b-9c1b-fdd1c7233ba0_4500x2800.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-15-deep-dive-into-application">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 14 : Hyperscale Log Monitoring: Choosing the Right Azure Storage for Each Data Flow]]></title><description><![CDATA[Post 14 of 45 &#183; Phase 3: Storage and Observability &#183; 19 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-14-hyperscale-log-monitoring</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-14-hyperscale-log-monitoring</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Thu, 30 Jul 2026 02:31:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!e16E!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Phase 2 built the messaging layer. After Phase 2, a system can:</p><ul><li><p>Accept order commands via Service Bus and process them exactly once</p></li><li><p>Stream order events to Event Hubs for fan-out consumption</p></li><li><p>Monitor queue depth via <code>sb.orders.deadletter_messages</code> OTel gauges</p></li><li><p>Resume after restarts at the last Azurite checkpoint position</p></li></ul><blockquote><p>What it cannot yet do: persist anything durably. The order is processed. The event is consumed. But where does the order record live? Where does the processed telemetry go for long-term analysis? Where are the audit log entries stored?</p><p>Phase 3 answers those questions across six posts. Post 14 maps the terrain before building in it: five Azure storage services, what each is designed for, when to choose each, and which emulators are already running.</p></blockquote><blockquote><p><strong>Career skill this post adds:</strong> Azure storage service selection &#8212; the most common architecture design interview question for senior Azure .NET roles. &#8220;Which storage service would you use for X?&#8221; appears in every Azure architecture assessment. The decision tree in this post is the framework that turns that question from guesswork into a structured answer.</p></blockquote><ul><li><p>Azure Service Bus vs Event Hubs vs Blob vs Table vs ADX &#8212; the data-type taxonomy</p></li><li><p>The emulator landscape: what runs locally, what the connection strings look like</p></li><li><p><code>docs/storage-decision.md</code> &#8212; the living reference added to the repository</p></li><li><p>The series progression: which posts cover which services</p></li><li><p>The one question to ask first before choosing any storage service</p></li></ul><div><hr></div><h2>System state: where we are</h2><blockquote><p>Phase 2 is complete. Two local emulators are running: the Service Bus Emulator (port 5672) and the Event Hubs Emulator (port 5673). Azurite is already running from Post 13&#8217;s checkpoint store (port 10000). The Kusto Emulator runs in Posts 40&#8211;43. Post 14&#8217;s code addition is <code>docs/storage-decision.md</code> &#8212; a reference that accumulates as each service is added.</p></blockquote><blockquote><p><strong>Starting point:</strong> <code>git checkout post/14-storage-decision</code> Run <code>.\verify.ps1</code> &#8212; all 31 Post 1&#8211;13 checks must pass before adding Post 14 components.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!e16E!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!e16E!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png 424w, /__u/substackcdn.com/image/fetch/$s_!e16E!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png 848w, /__u/substackcdn.com/image/fetch/$s_!e16E!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png 1272w, /__u/substackcdn.com/image/fetch/$s_!e16E!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!e16E!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png" width="636" height="579.6510989010989" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1327,&quot;width&quot;:1456,&quot;resizeWidth&quot;:636,&quot;bytes&quot;:1187775,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/207886765?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!e16E!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png 424w, /__u/substackcdn.com/image/fetch/$s_!e16E!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png 848w, /__u/substackcdn.com/image/fetch/$s_!e16E!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png 1272w, /__u/substackcdn.com/image/fetch/$s_!e16E!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6585643-5177-4b95-9c04-08f2aaa3002d_4500x4100.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-14-hyperscale-log-monitoring">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Post 13 : Service Bus for Commands, Event Hubs for Streams — and Why Checkpoints Save You]]></title><description><![CDATA[Post 13 of 45 &#183; Phase 2: Azure Messaging (final post) &#183; 22 min read &#183; No Azure subscription required &#183; Paid early access]]></description><link>https://distributeddotnet.substack.com/p/post-13-service-bus-for-commands</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-13-service-bus-for-commands</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Tue, 28 Jul 2026 02:31:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8lBA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Post 12 left the checkpoint problem named and unresolved. <code>EventHubConsumerClient.ReadEventsAsync()</code> has no checkpoint store. On every restart &#8212; whether from a deployment, a crash, or the partition rebalance in War Story #3 &#8212; it reads from <code>EventPosition.Earliest</code>. For a partition accumulating 10,000 events/second during a 30-second rebalance, that is 300,000 events replayed before the consumer reaches live position.</p><p>Post 13 fixes it. <code>EventProcessorClient</code> replaces <code>EventHubConsumerClient</code>. Azurite provides the checkpoint store &#8212; a local Azure Blob Storage emulator that stores one small blob per partition: the last successfully processed <code>Offset</code> and <code>SequenceNumber</code>. On restart, <code>EventProcessorClient</code> reads those blobs and calls <code>ProcessEventAsync</code> starting from <code>Offset + 1</code>. Replay cost: at most one event &#8212; the event being processed when the crash or restart occurred.</p><p>Post 13 also formalises the TenantId partitioning that Post 12 started. <code>PartitionKey = tenantId</code> in the producer routes all events for a tenant to the same partition. <code>PartitionInitializingAsync</code> logs which partition each processor instance claims. In a multi-instance deployment (Post 37 with KEDA), four processor instances each own one of the four partitions &#8212; each instance processes exactly one tenant shard in order, without coordination.</p><blockquote><p><strong>Career skill this post adds:</strong> <code>EventProcessorClient</code> + checkpoint store integration &#8212; the production Event Hubs consumer pattern. <code>EventProcessorClient</code> with Azure Blob Storage checkpointing is the standard for any .NET team processing Event Hubs at scale. It is the counterpart to Service Bus <code>AutoCompleteMessages = false</code>: the explicit acknowledgement pattern for streaming consumers. Appears on senior Azure .NET engineer role specifications alongside <code>ServiceBusProcessor</code>.</p></blockquote><ul><li><p><code>EventProcessorClient</code> vs <code>EventHubConsumerClient</code> &#8212; when to use each</p></li><li><p><code>UpdateCheckpointAsync</code> &#8212; per-event vs per-N-events, the cost trade-off</p></li><li><p>Azurite as the checkpoint store &#8212; one blob per partition, same SDK as production</p></li><li><p><code>PartitionInitializingAsync</code> and <code>PartitionClosingAsync</code> &#8212; the partition lifecycle callbacks</p></li><li><p>Consumer group <code>order-processor</code> &#8212; dedicated, independent from the <code>$Default</code> group Post 12 used</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Post 12 added Event Hubs dual-publish and a basic <code>EventHubConsumerClient</code> consumer. Post 13 replaces that consumer with the production-grade <code>EventProcessorClient</code>, adds Azurite as the checkpoint store, and uses the <code>order-processor</code> consumer group (separate from <code>$Default</code>).</p><p>After Post 13, Phase 2 is complete. Both messaging pipelines are production-patterned:</p><ul><li><p><strong>Service Bus:</strong> <code>ServiceBusProcessor</code> + <code>AutoCompleteMessages = false</code> + Polly + <code>MaxDeliveryCount: 3</code> + DLQ monitoring</p></li><li><p><strong>Event Hubs:</strong> <code>EventProcessorClient</code> + Azurite per-event checkpoint + <code>order-processor</code> consumer group + <code>PartitionKey = TenantId</code></p></li></ul><blockquote><p><strong>Starting point:</strong> <code>git checkout post/13-tenantid-partitioning</code> Run <code>.\verify.ps1</code> &#8212; all 29 Post 1&#8211;12 checks must pass before adding Post 13 components.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!8lBA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!8lBA!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png 424w, /__u/substackcdn.com/image/fetch/$s_!8lBA!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png 848w, /__u/substackcdn.com/image/fetch/$s_!8lBA!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png 1272w, /__u/substackcdn.com/image/fetch/$s_!8lBA!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!8lBA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png" width="1456" height="1301" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/21f068ea-7964-489b-9122-accc157daad5_4700x4200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1301,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1620917,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/207743093?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!8lBA!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png 424w, /__u/substackcdn.com/image/fetch/$s_!8lBA!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png 848w, /__u/substackcdn.com/image/fetch/$s_!8lBA!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png 1272w, /__u/substackcdn.com/image/fetch/$s_!8lBA!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21f068ea-7964-489b-9122-accc157daad5_4700x4200.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-13-service-bus-for-commands">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Why AutoCompleteMessages = false is not optional]]></title><description><![CDATA[With AutoCompleteMessages = true, your handler can throw and the message is gone. Silent data loss with no warning.]]></description><link>https://distributeddotnet.substack.com/p/why-autocompletemessages-false-is</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/why-autocompletemessages-false-is</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Sun, 26 Jul 2026 03:30:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ODvG!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7954cfb7-539e-4e23-9be9-40abdc5ac8cd_768x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Why AutoCompleteMessages = false is not optional</p><p><code>AutoCompleteMessages = false</code> in <code>ServiceBusProcessorOptions</code> is not a best practice. It is the only setting that gives your handler meaningful control over what happens to a message when something goes wrong.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://distributeddotnet.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">This Substack is reader-supported. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>With <code>AutoCompleteMessages = true</code> &#8212; the default &#8212; the SDK calls <code>CompleteMessageAsync</code> immediately after <code>ProcessMessageAsync</code> returns, regardless of whether an exception was thrown inside the handler and caught by a try/catch you added. The message is gone. Nothing in the Service Bus logs, nothing in Application Insights, nothing in the DLQ. The order was lost silently.</p><p>The scenario:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:&quot;8cf89bf6-45ed-4082-92a2-a7bbe226076a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">// AutoCompleteMessages = true (the dangerous default)
processor.ProcessMessageAsync += async args =&gt;
{
    try
    {
        await SaveToDatabase(order);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "Database save failed. {OrderId}", order.OrderId);
        // You think: "I handled this."
        // The SDK thinks: "ProcessMessageAsync returned. Complete the message."
        // Result: message completed, order lost, no DLQ entry, no retry.
    }
};</code></pre></div><p>With <code>AutoCompleteMessages = false</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;csharp&quot;,&quot;nodeId&quot;:&quot;e3ba8cfa-5e74-49b6-b3a6-49b9a8f738d2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-csharp">// AutoCompleteMessages = false (correct)
processor.ProcessMessageAsync += async args =&gt;
{
    try
    {
        await SaveToDatabase(order);
        await args.CompleteMessageAsync(args.Message); // only called on success
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "Database save failed. {OrderId}", order.OrderId);
        // ProcessMessageAsync throws &#8594; SDK calls AbandonMessageAsync
        // DeliveryCount increments &#8594; Polly retries (Post 10)
        // After MaxDeliveryCount: DLQ entry with DeadLetterReason
        throw;
    }
};</code></pre></div><p>The difference: one setting. The consequence: the difference between silent data loss and a recoverable failure with full audit trail.</p><p>Post 8 of the series sets <code>AutoCompleteMessages = false</code> from the start and never changes it. Post 10 adds Polly so that <code>AbandonMessageAsync</code> is only called after retries are exhausted. The combination &#8212; manual complete + Polly retry &#8212; is the correct pattern for any production Service Bus consumer.</p><p style="text-align: center;">&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; THREE THINGS WORTH YOUR TIME THIS WEEK &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</p><ol><li><p>Azure Service Bus message settlement &#8212; the full settlement model <a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-transfers-locks-settlement#settlement-of-transfers">https://learn.microsoft.com/en-us/azure/service-bus-messaging/message-transfers-locks-settlement#settlement-of-transfers</a> Covers all four settlement operations (Complete, Abandon, Defer, DeadLetter) and exactly what each does to the message&#8217;s position in the queue. The <code>Defer</code> operation is one the series has not covered yet &#8212; it moves the message off the main queue into a deferred state, addressable only by sequence number. Useful for out-of-order processing.</p></li><li><p>Polly v8 breaking changes from v7 <a href="https://www.pollydocs.org/migration/v8">https://www.pollydocs.org/migration/v8</a> If your team is upgrading from Polly v7 to v8 (which <code>Microsoft.Extensions. Http.Resilience</code> uses), this page covers the renamed types and the new <code>ResiliencePipelineBuilder</code> pattern. The most common breakage: <code>Policy.Handle</code> is now <code>new PredicateBuilder().Handle</code>. The mental model is the same; the syntax changed.</p></li><li><p>What&#8217;s new in .NET Aspire 8.3.0 <a href="https://learn.microsoft.com/en-us/dotnet/aspire/whats-new/aspire-sdk-8-3">https://learn.microsoft.com/en-us/dotnet/aspire/whats-new/aspire-sdk-8-3</a> The series uses Aspire 8.3.0 throughout. The release notes cover the new Azure Service Bus integration (Aspire.Azure.Messaging.ServiceBus) that Post 7 does not use yet &#8212; it handles connection string injection automatically without the manual configuration in appsettings.Development.json. Post 11 adopts this integration.</p></li></ol><p style="text-align: center;">&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;ONE QUESTION &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</p><p>Phase 3 starts after Phase 2 wraps. Phase 3 is storage and observability: Application Insights deep dive, Azurite (local Blob Storage), and KQL &#8212; the query language that works in Application Insights, Azure Monitor, Azure Data Explorer, and Microsoft Sentinel.</p><p>The KQL post is the one I get asked about most.</p><p>Before I write it: what do you actually use KQL for right now?</p><p>A) Debugging &#8212; finding a specific request or exception B) Alerting &#8212; writing detection rules for Azure Monitor C) Dashboards &#8212; building workbooks in App Insights D) Reporting &#8212; querying custom events for business metrics E) Nothing yet &#8212; KQL is new to me</p><p>Hit reply. The answer shapes how I write the KQL posts (Posts 18&#8211;19 in the series).</p><p>&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</p><p>See you next Sunday,</p><p>Distributed Systems with <a href="/__u/distributeddotnet.substack.com/"><span data-color="#0000ff" style="color: rgb(0, 0, 255);">.NET distrib-dotnet.substack.com</span></a></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://distributeddotnet.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">This Substack is reader-supported. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Post 12 : Building Observable .NET Microservices with Aspire, Service Bus & Event Hubs]]></title><description><![CDATA[Post 12 of 45 &#183; Phase 2: Azure Messaging &#183; 21 min read &#183; No Azure subscription required &#183; Paid early access &#8212; free to all on Day 30]]></description><link>https://distributeddotnet.substack.com/p/post-12-building-observable-net-microservices</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-12-building-observable-net-microservices</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Sun, 26 Jul 2026 02:30:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8bhU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>War Story #3 was about checkpointing. After a partition rebalance, the consumer was 20 minutes behind &#8212; not because it was slow, but because it started reading from a 60-second-old checkpoint. The event hub had accumulated 20 minutes of events during the rebalance. The consumer had no way to know how far behind it was.</p><p>Post 12 adds the Azure Event Hubs Emulator: a free Microsoft Docker container that runs a full Event Hubs namespace locally. By the end, <code>/orders/create</code> dual-publishes to both the Service Bus <code>orders</code> queue (command, exactly-once delivery) and the Event Hubs <code>order-events</code> hub (event, fan-out to multiple consumer groups, retained for replay). The <code>OrderEventHubConsumer</code> reads from the <code>$Default</code> consumer group and logs each event with its <code>PartitionId</code>, <code>Offset</code>, and <code>SequenceNumber</code> &#8212; the three values that define where a consumer is in a partition&#8217;s stream.</p></blockquote><p>Post 13 adds <code>EventProcessorClient</code> with Azurite checkpoint storage and TenantId-based partition routing. Post 12 establishes the foundation.</p><blockquote><p><strong>Career skill this post adds:</strong> Azure Event Hubs SDK &#8212; <code>EventHubProducerClient</code>, <code>EventHubConsumerClient</code>, partition keys, consumer groups, and the difference between offset-based and time-based checkpointing. Event Hubs appears on senior Azure .NET engineer job specifications alongside Service Bus, and the &#8220;when to use which&#8221; decision (covered in this post) is a common interview question.</p></blockquote><ul><li><p>Event Hubs vs Service Bus &#8212; the data model difference, not just a feature comparison</p></li><li><p>The append-only partition &#8212; why <code>Offset</code> and <code>SequenceNumber</code> are not the same thing</p></li><li><p><code>EventHubProducerClient</code> with <code>CreateBatchAsync</code> and <code>PartitionKey</code></p></li><li><p><code>EventHubConsumerClient.ReadEventsAsync()</code> &#8212; all partitions, one stream</p></li><li><p>Per-event vs time-based checkpointing &#8212; the War Story #3 fix</p></li><li><p>Dual-publish: Service Bus for commands, Event Hubs for events</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Phase 2 Service Bus pipeline is complete (Posts 7&#8211;11). Post 12 adds the Event Hubs pipeline in parallel. Both pipelines share the same <code>OrderCreatedEvent</code> contract from <code>src/Contracts/</code>. Both use <code>BinaryData.FromObjectAsJson()</code> with the source generator. The event serialisation knowledge from Post 7 carries forward without change.</p><p>After Post 12 the system has two independent publish paths from <code>OrderService</code>:</p><ol><li><p><strong>Service Bus </strong><code>orders</code><strong> queue</strong> &#8594; <code>OrderProcessorWorker</code> &#8212; transactional, exactly-once, DLQ on failure</p></li><li><p><strong>Event Hubs </strong><code>order-events</code><strong> hub</strong> &#8594; <code>OrderEventHubConsumer</code> &#8212; streaming, fan-out, retained</p></li></ol><blockquote><p><strong>Starting point:</strong> <code>git checkout post/12-eventhubs-emulator</code> Run <code>.\verify.ps1</code> &#8212; all 27 Post 1&#8211;11 checks must pass before adding Post 12 components.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!8bhU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!8bhU!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png 424w, /__u/substackcdn.com/image/fetch/$s_!8bhU!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png 848w, /__u/substackcdn.com/image/fetch/$s_!8bhU!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png 1272w, /__u/substackcdn.com/image/fetch/$s_!8bhU!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!8bhU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png" width="1456" height="1171" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1171,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1264340,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/207734463?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!8bhU!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png 424w, /__u/substackcdn.com/image/fetch/$s_!8bhU!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png 848w, /__u/substackcdn.com/image/fetch/$s_!8bhU!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png 1272w, /__u/substackcdn.com/image/fetch/$s_!8bhU!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac0dbe66-51d1-4e3b-b2f5-2b943956e995_4600x3700.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-12-building-observable-net-microservices">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 10 : .NET Order Pipeline: Aspire, Service Bus Emulator, and Resilience That Actually Works]]></title><description><![CDATA[Post 10 of 45 &#183; Phase 2: Azure Messaging &#183; 21 min read &#183; No Azure subscription required &#183; Paid early access &#8212; free to all on Day 26]]></description><link>https://distributeddotnet.substack.com/p/post-10-net-order-pipeline-aspire</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-10-net-order-pipeline-aspire</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Wed, 22 Jul 2026 02:30:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!w-mQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p>The downstream payment API returned HTTP 503 for 2 seconds. Not a crash &#8212; a blip. The infrastructure team confirmed later: it was a rolling restart that lasted 1.8 seconds.</p></blockquote><blockquote><p>Without Polly, <code>OrderProcessorWorker</code> threw <code>HttpRequestException</code>. The SDK called <code>AbandonMessageAsync</code>. <code>DeliveryCount</code> became 2. One more transient failure and the message would land in the DLQ with <code>MaxDeliveryCountExceeded</code> &#8212; for a fault that had nothing to do with the message.</p><p>With Polly &#8212; one line of configuration &#8212; the handler retried internally, waited 1 second, tried again, succeeded. The SDK called <code>CompleteMessageAsync</code>. <code>DeliveryCount</code> stayed at 1. The 2-second payment API blip was invisible to Service Bus.</p></blockquote><blockquote><p>The distinction matters because <code>MaxDeliveryCount</code> is your last-resort protection against poison messages &#8212; messages that will never succeed regardless of retries. If transient failures eat that budget, <code>MaxDeliveryCount: 3</code> means three transient blips send a message to the DLQ. Polly reserves the <code>DeliveryCount</code> budget for genuine failures.</p><p>Post 10 adds a <code>ResiliencePipeline</code> to <code>OrderProcessorWorker</code>: exponential backoff with jitter for transient HTTP failures, a 25-second timeout per attempt (shorter than the 30-second <code>LockDuration</code>), and the <code>TRANSIENT_</code> prefix test endpoint that shows &#8212; with log records in the Aspire Dashboard &#8212; exactly when Polly retries and when the SDK&#8217;s <code>DeliveryCount</code> changes.</p></blockquote><blockquote><p><strong>Career skill this post adds:</strong> <code>Microsoft.Extensions.Http.Resilience</code> and <code>ResiliencePipelineBuilder</code> &#8212; the Polly integration shipped with .NET 8. These appear on every Azure .NET senior engineer job specification that mentions Service Bus, Event Hubs, or any downstream HTTP dependency. The <code>MaxConcurrentCalls</code> &#215; Polly retry interaction is a common interview topic.</p></blockquote><ul><li><p>The resilience pipeline &#8212; retry, timeout, and the order they execute in</p></li><li><p><code>DeliveryCount</code> vs Polly retries &#8212; the exact accounting of who increments what</p></li><li><p><code>UseJitter = true</code> &#8212; why randomised delays prevent the thundering herd problem</p></li><li><p><code>AddTimeout(TimeSpan.FromSeconds(25))</code> &#8212; why the timeout must be less than <code>LockDuration</code></p></li><li><p><code>ShouldHandle</code> &#8212; which exceptions Polly retries vs which propagate immediately</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Posts 7&#8211;9 built the Service Bus messaging loop: publish (Post 7), consume (Post 8), and DLQ forensics (Post 9). Post 10 adds the resilience layer &#8212; the in-handler policy that separates transient failures (retry automatically) from permanent failures (propagate to the SDK, let <code>DeliveryCount</code> increment).</p><p>After Post 10, the error handling hierarchy is:</p><ol><li><p><strong>Polly catches:</strong> transient <code>HttpRequestException</code>, <code>TimeoutRejectedException</code> &#8212; retried internally, <code>DeliveryCount</code> unchanged</p></li><li><p><strong>SDK catches:</strong> unhandled exceptions after Polly retries exhausted &#8212; <code>DeliveryCount</code> increments</p></li><li><p><strong>Explicit dead-letter:</strong> deserialisation failures, poison messages &#8212; <code>DeadLetterMessageAsync</code> called directly, <code>DeliveryCount</code> irrelevant</p></li></ol><blockquote><p><strong>Starting point:</strong> <code>git checkout post/10-resilience</code> Run <code>.\verify.ps1</code> &#8212; all 23 Post 1&#8211;9 checks must pass before adding Post 10 components.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!w-mQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!w-mQ!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png 424w, /__u/substackcdn.com/image/fetch/$s_!w-mQ!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png 848w, /__u/substackcdn.com/image/fetch/$s_!w-mQ!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png 1272w, /__u/substackcdn.com/image/fetch/$s_!w-mQ!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!w-mQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png" width="1456" height="1153" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1153,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1321735,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/207513485?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!w-mQ!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png 424w, /__u/substackcdn.com/image/fetch/$s_!w-mQ!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png 848w, /__u/substackcdn.com/image/fetch/$s_!w-mQ!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png 1272w, /__u/substackcdn.com/image/fetch/$s_!w-mQ!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24131e3d-a6d4-4453-acaf-df5bbfd69703_4800x3800.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-10-net-order-pipeline-aspire">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[POST 9 : Azure Service Bus Dead-Letter Queues: Finding, Inspecting, and Replaying Failed Messages]]></title><description><![CDATA[Post 9 of 45 &#183; Phase 2: Azure Messaging &#183; 23 min read &#183; No Azure subscription required &#183; Paid early access &#8212; free to all on Day 33]]></description><link>https://distributeddotnet.substack.com/p/post-9-azure-service-bus-dead-letter</link><guid isPermaLink="false">https://distributeddotnet.substack.com/p/post-9-azure-service-bus-dead-letter</guid><dc:creator><![CDATA[Distributed Systems with .Net]]></dc:creator><pubDate>Mon, 20 Jul 2026 02:30:21 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!7YAc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>War Story #2 &#8212; 40,000 messages in the dead-letter queue &#8212; started with one bad deployment. But the damage took six hours to discover because nobody was watching the DLQ. The queue&#8217;s <code>ActiveMessageCount</code> looked healthy: messages were moving, just not to where anyone expected.</p><p>The DLQ is not a failure mode. It is a feature. Service Bus created it to protect you from permanently losing messages you cannot process. The failure mode is leaving the DLQ unmonitored.</p><p>Post 9 is War Story #2 run locally, step by step. You send a deliberately poison message &#8212; an order with an <code>OrderId</code> prefixed <code>POISON_</code> that the handler throws on every attempt. You watch <code>DeliveryCount</code> climb from 1 to 2 to 3. After <code>MaxDeliveryCount</code> (3) exhaustions, Service Bus moves the message to the DLQ. You inspect the DLQ using <code>ServiceBusReceiver</code> in dead-letter mode, read the <code>DeadLetterReason</code> that explains why the message was dead-lettered, and replay it to the main queue using a PowerShell script &#8212; the same script that cleared War Story #2&#8217;s 40,000 messages in production.</p><p>This post also introduces Service Bus topics and subscriptions &#8212; the fan-out pattern that lets one published message reach multiple subscribers with SQL filter routing.</p><blockquote><p><strong>Career skill this post adds:</strong> Service Bus DLQ forensics &#8212; inspecting dead-letter queues, reading <code>DeadLetterReason</code> and <code>DeadLetterErrorDescription</code>, and replaying DLQ messages safely. This is a common interview question and a production skill for any .NET engineer operating Azure Service Bus.</p></blockquote><ul><li><p><code>DeliveryCount</code> &#8212; what it counts, how it increments, and why <code>MaxDeliveryCount: 3</code> is safer than the default of 10</p></li><li><p><code>ServiceBusReceiver</code> in dead-letter mode &#8212; same API as a normal receiver, different constructor argument</p></li><li><p>Topics vs queues &#8212; when to use each; the SQL filter subscription pattern</p></li><li><p>The replay script &#8212; moving dead-lettered messages back to the main queue</p></li><li><p><code>RequiresDuplicateDetection</code> &#8212; how to prevent the same message being processed twice during replay</p></li></ul><div><hr></div><h2>System state: where we are</h2><p>Post 8 gave <code>OrderProcessorWorker</code> the consumer loop. Post 9 adds the failure path: a message that cannot be processed, exhausts its delivery attempts, and lands in the DLQ. The DLQ always existed &#8212; it is a built-in sibling of every queue and subscription. Post 9 makes it visible and shows how to use it as a diagnostic tool, not a graveyard.</p><blockquote><p><strong>Starting point:</strong> <code>git checkout post/09-servicebus-emulator</code> Run <code>.\verify.ps1</code> &#8212; all 21 Post 1&#8211;8 checks must pass before adding Post 9 components.</p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!7YAc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!7YAc!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png 424w, /__u/substackcdn.com/image/fetch/$s_!7YAc!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png 848w, /__u/substackcdn.com/image/fetch/$s_!7YAc!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png 1272w, /__u/substackcdn.com/image/fetch/$s_!7YAc!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_webp, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!7YAc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png" width="1456" height="1234" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1234,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1078089,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://distributeddotnet.substack.com/i/207391408?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!7YAc!, /__u/distributeddotnet.substack.com/w_424, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png 424w, /__u/substackcdn.com/image/fetch/$s_!7YAc!, /__u/distributeddotnet.substack.com/w_848, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png 848w, /__u/substackcdn.com/image/fetch/$s_!7YAc!, /__u/distributeddotnet.substack.com/w_1272, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png 1272w, /__u/substackcdn.com/image/fetch/$s_!7YAc!, /__u/distributeddotnet.substack.com/w_1456, /__u/distributeddotnet.substack.com/c_limit, /__u/distributeddotnet.substack.com/f_auto, /__u/distributeddotnet.substack.com/q_auto:good, /__u/distributeddotnet.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F186f80b8-8223-4da8-9a1a-0c2095e7f43a_4600x3900.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>
      <p>
          <a href="/__u/distributeddotnet.substack.com/p/post-9-azure-service-bus-dead-letter">
              Read more
          </a>
      </p>
   ]]></content:encoded></item></channel></rss>