<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>.dev notes — CD Smith</title>
  <link>https://www.cdsmith.dev/blog/</link>
  <atom:link href="https://www.cdsmith.dev/blog/feed.xml" rel="self" type="application/rss+xml"/>
  <description>Practical posts on .NET, C#, Azure and building backend integrations.</description>
  <language>en-us</language>
  <item>
    <title>Agentless logging from .NET to Datadog</title>
    <link>https://www.cdsmith.dev/blog/2026-10-04-agentless-logging-to-datadog-from-dotnet/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-10-04-agentless-logging-to-datadog-from-dotnet/</guid>
    <pubDate>Sun, 04 Oct 2026 12:00:00 GMT</pubDate>
    <category>.NET</category>
    <category>Observability</category>
    <category>Datadog</category>
    <description>You don&apos;t need to install the Datadog Agent to get your .NET logs into Datadog. Two agentless options send them straight over HTTPS, structured and ready to search.</description>
    <content:encoded><![CDATA[<p>Datadog's standard setup for logs is to write them to a file and have the <strong>Datadog Agent</strong> tail that file and forward it. That works well on a server or VM you control. On Azure App Service, Azure Functions or a small container, installing and running an Agent alongside your app is extra moving parts.</p>
<p><strong>Agentless logging</strong> skips the Agent: your application sends its logs directly to Datadog's HTTPS intake. There are two ways to do it in .NET.</p>
<h2 id="option-1-the-serilog-sink"><a class="anchor" href="#option-1-the-serilog-sink" aria-hidden="true">#</a>Option 1: the Serilog sink</h2>
<p>If you use Serilog, or are happy to, this is the simplest route. Add the <code>Serilog.AspNetCore</code> and <code>Serilog.Sinks.Datadog.Logs</code> packages:</p>
<pre data-lang="csharp"><code class="language-csharp">builder.Services.<span class="tok-method">AddSerilog</span>((services, logger) =&gt; logger
    .ReadFrom.<span class="tok-method">Configuration</span>(builder.Configuration)
    .Enrich.<span class="tok-method">FromLogContext</span>()
    .WriteTo.<span class="tok-method">DatadogLogs</span>(
        apiKey: builder.Configuration[<span class="tok-string">"Datadog:ApiKey"</span>],
        source: <span class="tok-string">"csharp"</span>,
        service: <span class="tok-string">"orders-api"</span>,
        host: <span class="tok-type">Environment</span>.MachineName,
        tags: [<span class="tok-string">$"env:{builder.Environment.EnvironmentName.ToLowerInvariant()}"</span>, <span class="tok-string">"version:1.4.0"</span>],
        configuration: <span class="tok-keyword">new</span> <span class="tok-type">DatadogConfiguration</span>
        {
            <span class="tok-type">Url</span> = <span class="tok-string">"https://http-intake.logs.datadoghq.com"</span>
        }));</code></pre>
<p>Your code keeps using <code>ILogger&lt;T&gt;</code> as usual. Serilog sits behind it and batches log events to Datadog over HTTPS on port 443.</p>
<p><strong>Set the URL for your Datadog site.</strong> Accounts live in different regions, and each has its own intake address. <code>http-intake.logs.datadoghq.com</code> is for US1; EU accounts use <code>http-intake.logs.datadoghq.eu</code>, and other regions have their own. Check the site in your Datadog URL and the logs documentation if you're unsure. Sending to the wrong site fails quietly.</p>
<h2 id="option-2-the-datadog-net-tracer"><a class="anchor" href="#option-2-the-datadog-net-tracer" aria-hidden="true">#</a>Option 2: the Datadog .NET tracer</h2>
<p>If you already use Datadog APM, or want it, the .NET tracer can send logs directly too, from plain <code>Microsoft.Extensions.Logging</code> with no Serilog required. Add the <code>Datadog.Trace.Bundle</code> NuGet package, enable automatic instrumentation with the <code>CORECLR_*</code> environment variables from Datadog's setup guide for your OS, and set:</p>
<pre><code>DD_API_KEY=&lt;your API key&gt;
DD_SITE=datadoghq.com
DD_LOGS_DIRECT_SUBMISSION_INTEGRATIONS=ILogger
DD_ENV=production
DD_SERVICE=orders-api
DD_VERSION=1.4.0</code></pre>
<p><code>DD_LOGS_DIRECT_SUBMISSION_INTEGRATIONS</code> also accepts <code>Serilog</code>, <code>NLog</code> and <code>Log4Net</code>, separated by semicolons. By default it sends <code>Information</code> and above; change that with <code>DD_LOGS_DIRECT_SUBMISSION_MINIMUM_LEVEL</code>, or filter by category under a <code>Datadog</code> key in the <code>Logging</code> section of <code>appsettings.json</code>.</p>
<p>The big advantage is <strong>log and trace correlation</strong>: the tracer adds trace and span IDs to every log entry, so in Datadog you can jump from a slow request's trace straight to the logs it wrote. Note that this option relies on the tracer's automatic instrumentation, which is more setup than a single NuGet package, and traces themselves are sent separately from logs.</p>
<h2 id="make-the-logs-worth-searching"><a class="anchor" href="#make-the-logs-worth-searching" aria-hidden="true">#</a>Make the logs worth searching</h2>
<p>Agentless or not, a few habits decide whether the logs are useful:</p>
<ul><li><strong>Use message templates, not string interpolation.</strong> <code>logger.LogInformation(&quot;Order {OrderId} shipped&quot;, id)</code> sends <code>OrderId</code> as its own attribute. In Datadog, create a facet on it and you can filter and group by order.</li><li><strong>Use unified service tagging.</strong> Consistent <code>env</code>, <code>service</code> and <code>version</code> values on logs, traces and metrics let Datadog connect them, and let you compare error rates before and after a deployment.</li><li><strong>Watch volume.</strong> Datadog bills by logs ingested and indexed. Send <code>Information</code> and above from production, keep <code>Debug</code> for development, and use exclusion filters in Datadog for noisy entries you don't need to keep.</li></ul>
<h2 id="things-to-know-before-you-rely-on-it"><a class="anchor" href="#things-to-know-before-you-rely-on-it" aria-hidden="true">#</a>Things to know before you rely on it</h2>
<ul><li><strong>Protect the API key.</strong> It allows anyone to send data into your account. Keep it in Key Vault or a secret app setting, never in <code>appsettings.json</code> in the repository.</li><li><strong>Logs are sent from your process.</strong> They're batched in memory and sent in the background. If Datadog is unreachable for long enough, the queue fills and new entries are dropped, and anything still queued when the process is killed is lost. Flush on shutdown (<code>Log.CloseAndFlush()</code> if you use Serilog's static logger), and keep critical audit records somewhere durable as well.</li><li><strong>There's no local copy.</strong> With the Agent approach, a log file stays on disk if the network fails. Agentless trades that resilience for simplicity.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>For App Service, Functions and small containers, agentless logging is the quickest way to get .NET logs into Datadog. Use the Serilog sink for the simplest setup, or the .NET tracer's direct submission if you want APM and log-trace correlation. Either way, point it at the right Datadog site, keep the API key secret, and log with message templates so every value is searchable.</p>]]></content:encoded>
  </item>
  <item>
    <title>Pass your CancellationToken all the way down</title>
    <link>https://www.cdsmith.dev/blog/2026-09-28-pass-your-cancellationtoken-all-the-way-down/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-09-28-pass-your-cancellationtoken-all-the-way-down/</guid>
    <pubDate>Mon, 28 Sep 2026 12:00:00 GMT</pubDate>
    <category>.NET</category>
    <category>ASP.NET Core</category>
    <category>Async</category>
    <description>When a caller gives up on a request, your code should stop working on it too. It only takes a parameter, but you have to pass it through every layer.</description>
    <content:encoded><![CDATA[<p>A client calls your API, waits a few seconds, and gives up. The browser tab closes, or the upstream service hits its own timeout. On the server, your code carries on: it finishes the database query, calls two downstream APIs, maps the results and writes a response nobody will read.</p>
<p>That wasted work adds up under load, and it's easy to avoid. .NET already tells you when the caller has gone. You just have to listen.</p>
<h2 id="where-the-token-comes-from"><a class="anchor" href="#where-the-token-comes-from" aria-hidden="true">#</a>Where the token comes from</h2>
<p>In ASP.NET Core, add a <code>CancellationToken</code> parameter to an action or a minimal API handler and the framework binds it to <code>HttpContext.RequestAborted</code>. It is cancelled when the client disconnects.</p>
<pre data-lang="csharp"><code class="language-csharp">app.<span class="tok-method">MapGet</span>(<span class="tok-string">"/orders/{id:int}"</span>, <span class="tok-keyword">async</span> (<span class="tok-keyword">int</span> id, <span class="tok-type">OrdersService</span> orders, <span class="tok-type">CancellationToken</span> ct) =&gt;
{
    <span class="tok-keyword">var</span> order = <span class="tok-keyword">await</span> orders.<span class="tok-method">GetAsync</span>(id, ct);
    <span class="tok-keyword">return</span> order <span class="tok-keyword">is</span> <span class="tok-keyword">null</span> ? <span class="tok-type">Results</span>.<span class="tok-method">NotFound</span>() : <span class="tok-type">Results</span>.<span class="tok-method">Ok</span>(order);
});</code></pre>
<p>Background services get one too: the <code>stoppingToken</code> passed to <code>ExecuteAsync</code> is cancelled when the host shuts down.</p>
<h2 id="pass-it-through-every-layer"><a class="anchor" href="#pass-it-through-every-layer" aria-hidden="true">#</a>Pass it through every layer</h2>
<p>A token only helps if it reaches the code that does the slow work. Every async method in between should accept one and hand it on:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">Order</span>?&gt; <span class="tok-method">GetAsync</span>(<span class="tok-keyword">int</span> id, <span class="tok-type">CancellationToken</span> ct)
{
    <span class="tok-keyword">var</span> order = <span class="tok-keyword">await</span> db.Orders.<span class="tok-method">FirstOrDefaultAsync</span>(o =&gt; o.Id == id, ct);
    <span class="tok-keyword">if</span> (order <span class="tok-keyword">is</span> <span class="tok-keyword">null</span>) <span class="tok-keyword">return</span> <span class="tok-keyword">null</span>;

    order.Shipping = <span class="tok-keyword">await</span> shippingClient.<span class="tok-method">GetStatusAsync</span>(order.TrackingNumber, ct);
    <span class="tok-keyword">return</span> order;
}</code></pre>
<p>EF Core, <code>HttpClient</code>, <code>Stream</code> and most Azure SDK clients take a token on their async methods. If one method in the chain doesn't pass it on, everything below that point keeps running after the caller has gone.</p>
<h2 id="add-your-own-timeout"><a class="anchor" href="#add-your-own-timeout" aria-hidden="true">#</a>Add your own timeout</h2>
<p>Sometimes you want to give up before the caller does, for example when a downstream API is slow. Link a timeout to the incoming token so that either one cancels the work:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> cts = <span class="tok-type">CancellationTokenSource</span>.<span class="tok-method">CreateLinkedTokenSource</span>(ct);
cts.<span class="tok-method">CancelAfter</span>(<span class="tok-type">TimeSpan</span>.<span class="tok-method">FromSeconds</span>(<span class="tok-number">5</span>));

<span class="tok-keyword">var</span> status = <span class="tok-keyword">await</span> shippingClient.<span class="tok-method">GetStatusAsync</span>(trackingNumber, cts.Token);</code></pre>
<h2 id="dont-treat-cancellation-as-an-error"><a class="anchor" href="#dont-treat-cancellation-as-an-error" aria-hidden="true">#</a>Don't treat cancellation as an error</h2>
<p>When a token is cancelled, the awaited call throws <code>OperationCanceledException</code> (or <code>TaskCanceledException</code>, which derives from it). That is expected, so don't log it as a failure or retry it:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">catch</span> (<span class="tok-type">OperationCanceledException</span>) <span class="tok-keyword">when</span> (ct.IsCancellationRequested)
{
    <span class="tok-comment">// The caller went away. Nothing to report.</span>
}</code></pre>
<p>The <code>when</code> filter matters. If your own timeout fired instead, <code>ct</code> isn't cancelled, and you probably do want to log that.</p>
<blockquote><p><strong>Tip:</strong> turn on the CA2016 analyzer rule. It flags async calls that could take the token you already have but don't.</p></blockquote>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Accept a <code>CancellationToken</code> in every async method and pass it to every call that takes one. It costs one parameter, and your service stops doing work nobody is waiting for.</p>]]></content:encoded>
  </item>
  <item>
    <title>Make your Service Bus message handlers idempotent</title>
    <link>https://www.cdsmith.dev/blog/2026-09-27-make-your-service-bus-handlers-idempotent/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-09-27-make-your-service-bus-handlers-idempotent/</guid>
    <pubDate>Sun, 27 Sep 2026 12:00:00 GMT</pubDate>
    <category>Azure</category>
    <category>Service Bus</category>
    <category>Integrations</category>
    <description>Azure Service Bus delivers every message at least once, and duplicates can arrive at the same moment. Here&apos;s how to make handling the same message twice harmless, even when two copies race.</description>
    <content:encoded><![CDATA[<p>Azure Service Bus guarantees that every message is delivered <strong>at least once</strong>. That wording is deliberate: sometimes you get the same message twice.</p>
<p>It happens more often than you'd think. Your handler processes an order, writes to the database, and then crashes, times out or loses its lock before it completes the message. Service Bus sees a message that was never completed and delivers it again. If the handler charges a card or sends an email, the customer gets it twice.</p>
<p>The fix isn't to prevent redelivery. It's to make processing the same message a second time harmless. That property is called <strong>idempotency</strong>.</p>
<h2 id="how-duplicates-happen"><a class="anchor" href="#how-duplicates-happen" aria-hidden="true">#</a>How duplicates happen</h2>
<p>With the default <strong>peek-lock</strong> mode, receiving a message locks it rather than removing it. Your handler has until the lock expires to complete it. If the handler throws, the process dies or the lock runs out first, the message becomes available again and its delivery count goes up. After <code>MaxDeliveryCount</code> attempts (10 by default) it moves to the dead-letter queue.</p>
<p>Duplicates can also start at the sender. If a send times out after the broker has already stored the message, the sender retries and the queue now holds two copies.</p>
<p>Neither kind of duplicate waits politely for the first copy to finish:</p>
<ul><li>With several instances of your service, or a processor running with <code>MaxConcurrentCalls</code> above 1, two copies of a sender-side duplicate can be picked up within milliseconds of each other.</li><li>If a handler runs past its lock, Service Bus hands the message to another receiver <strong>while the first is still working on it</strong>. The processor renews locks for you, up to <code>MaxAutoLockRenewalDuration</code> (5 minutes by default), but a slow dependency or a long pause can still outlast it.</li></ul>
<p>So your handler has to be safe not only when the same message comes back later, but when two copies are being processed <strong>at the same moment</strong>.</p>
<h2 id="duplicate-detection-helps-but-only-on-the-way-in"><a class="anchor" href="#duplicate-detection-helps-but-only-on-the-way-in" aria-hidden="true">#</a>Duplicate detection helps, but only on the way in</h2>
<p>Service Bus has a duplicate detection feature. When it's on, the broker drops any message whose <code>MessageId</code> it has already seen within a time window (10 minutes by default, up to 7 days). That stops the sender-side duplicates above, as long as the retry reuses the same <code>MessageId</code>.</p>
<p>It does nothing for redelivery to your <strong>receiver</strong>. It also has to be turned on when the queue or topic is created. You can't enable it later.</p>
<h2 id="why-check-then-do-the-work-isnt-enough"><a class="anchor" href="#why-check-then-do-the-work-isnt-enough" aria-hidden="true">#</a>Why &quot;check, then do the work&quot; isn't enough</h2>
<p>The obvious approach is to keep a table of processed message IDs and check it first:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">if</span> (<span class="tok-keyword">await</span> db.ProcessedMessages.<span class="tok-method">AnyAsync</span>(m =&gt; m.Id == messageId, ct))
    <span class="tok-keyword">return</span>; <span class="tok-comment">// already handled</span>

<span class="tok-comment">// ... do the work, then record the message ID ...</span></code></pre>
<p>This handles a message that comes back a minute later. It fails when two copies arrive together. Both handlers run the check before either has saved anything, both see &quot;not processed&quot;, and both do the work. Any check followed by a separate write has this gap, however fast the code is.</p>
<p>The check and the claim have to be <strong>one operation</strong> that the database decides, so only one copy can win.</p>
<h2 id="claim-the-message-first-and-let-the-database-pick-the-winner"><a class="anchor" href="#claim-the-message-first-and-let-the-database-pick-the-winner" aria-hidden="true">#</a>Claim the message first, and let the database pick the winner</h2>
<p>Insert the processed-message record <strong>before</strong> doing the work, inside the same transaction, and make the message ID its primary key:</p>
<pre data-lang="csharp"><code class="language-csharp">processor.ProcessMessageAsync += <span class="tok-keyword">async</span> args =&gt;
{
    <span class="tok-keyword">await</span> <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> scope = services.<span class="tok-method">CreateAsyncScope</span>();
    <span class="tok-keyword">var</span> db = scope.ServiceProvider.<span class="tok-method">GetRequiredService</span>&lt;<span class="tok-type">ShippingDb</span>&gt;();
    <span class="tok-keyword">var</span> order = args.Message.Body.<span class="tok-method">ToObjectFromJson</span>&lt;<span class="tok-type">OrderPlaced</span>&gt;();
    <span class="tok-keyword">var</span> ct = args.CancellationToken;

    <span class="tok-keyword">await</span> <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> tx = <span class="tok-keyword">await</span> db.Database.<span class="tok-method">BeginTransactionAsync</span>(ct);
    <span class="tok-keyword">try</span>
    {
        <span class="tok-comment">// 1. Claim the message. The primary key on Id lets only one transaction hold it.</span>
        db.ProcessedMessages.<span class="tok-method">Add</span>(<span class="tok-keyword">new</span> <span class="tok-type">ProcessedMessage</span>(args.Message.MessageId, <span class="tok-type">DateTimeOffset</span>.UtcNow));
        <span class="tok-keyword">await</span> db.<span class="tok-method">SaveChangesAsync</span>(ct);

        <span class="tok-comment">// 2. Do the work in the same transaction.</span>
        db.Shipments.<span class="tok-method">Add</span>(<span class="tok-type">Shipment</span>.<span class="tok-method">For</span>(order));
        <span class="tok-keyword">await</span> db.<span class="tok-method">SaveChangesAsync</span>(ct);

        <span class="tok-keyword">await</span> tx.<span class="tok-method">CommitAsync</span>(ct);
    }
    <span class="tok-keyword">catch</span> (<span class="tok-type">DbUpdateException</span> ex) <span class="tok-keyword">when</span> (<span class="tok-method">IsDuplicateKey</span>(ex))
    {
        <span class="tok-comment">// Another copy already claimed this message and did the work.</span>
        <span class="tok-keyword">await</span> tx.<span class="tok-method">RollbackAsync</span>(ct);
    }

    <span class="tok-comment">// 3. Complete only after the transaction has finished.</span>
    <span class="tok-keyword">await</span> args.<span class="tok-method">CompleteMessageAsync</span>(args.Message, ct);
};

<span class="tok-keyword">static</span> <span class="tok-keyword">bool</span> <span class="tok-method">IsDuplicateKey</span>(<span class="tok-type">DbUpdateException</span> ex) =&gt;
    ex.InnerException <span class="tok-keyword">is</span> <span class="tok-type">SqlException</span> { <span class="tok-type">Number</span>: <span class="tok-number">2627</span> <span class="tok-keyword">or</span> <span class="tok-number">2601</span> }; <span class="tok-comment">// SQL Server</span></code></pre>
<p>Here's what happens when two copies arrive at the same instant:</p>
<ol><li>Both handlers try to insert the same message ID.</li><li>SQL Server lets the first insert through and makes the second <strong>wait</strong> on that key until the first transaction finishes.</li><li>When the first commits, the second insert fails with a duplicate key error. The handler rolls back, having done no work, and completes its copy.</li><li>If the first handler crashes and rolls back instead, the second insert goes through and that copy does the work. Either way the work happens exactly once.</li></ol>
<p>The database's unique key is the only thing both handlers can see at the same time, which is why it has to make the decision.</p>
<p>A few details matter:</p>
<ul><li><strong>Use a fresh <code>DbContext</code> for each message</strong>, as the scope above does. After a failed <code>SaveChangesAsync</code>, the context still holds the rejected entities, and handlers running in parallel must not share one.</li><li><strong>Create the processor with <code>AutoCompleteMessages = false</code></strong>, so you decide when to complete. Completing before the commit, then crashing, loses the message.</li><li><strong>Senders must set <code>MessageId</code></strong> to something stable, such as the order ID plus the event type. A random ID makes a retried send look like a brand-new message, and none of this can catch it.</li><li><strong>Keep the transaction short.</strong> The claim holds a lock until you commit, so the losing copy waits for as long as the work takes.</li></ul>
<blockquote><p><strong>On MongoDB:</strong> use the message ID as the <code>_id</code> of a processed-messages document. A second insert with the same <code>_id</code> fails with duplicate key error 11000. To keep the claim and the work together, run both in a multi-document transaction. That needs a replica set; every MongoDB Atlas cluster is one.</p></blockquote>
<h2 id="add-a-business-level-constraint-as-a-backstop"><a class="anchor" href="#add-a-business-level-constraint-as-a-backstop" aria-hidden="true">#</a>Add a business-level constraint as a backstop</h2>
<p>The message ID protects you from the same message twice. It can't help when the sender publishes the same event twice with <strong>different</strong> IDs. A unique index on the business key, such as one shipment per order, catches that case as well:</p>
<pre data-lang="csharp"><code class="language-csharp">modelBuilder.<span class="tok-method">Entity</span>&lt;<span class="tok-type">Shipment</span>&gt;()
    .<span class="tok-method">HasIndex</span>(s =&gt; s.OrderId)
    .<span class="tok-method">IsUnique</span>();</code></pre>
<p>Handle a violation of that index the same way: roll back and complete the message.</p>
<h2 id="or-process-one-order-at-a-time-with-sessions"><a class="anchor" href="#or-process-one-order-at-a-time-with-sessions" aria-hidden="true">#</a>Or process one order at a time with sessions</h2>
<p>If messages about the same entity must never be processed in parallel, <strong>sessions</strong> let Service Bus do the serializing. Set <code>SessionId</code> to the order ID when sending and receive with a <code>ServiceBusSessionProcessor</code>. Only one receiver holds a session at a time, so messages for the same order are handled one after another, in order, while different orders still run in parallel.</p>
<p>Sessions have to be enabled when the queue is created, and they cap throughput per session. They don't replace the claim either: a redelivered message still comes back through the same session. What they remove is the race.</p>
<h2 id="when-the-work-isnt-in-your-database"><a class="anchor" href="#when-the-work-isnt-in-your-database" aria-hidden="true">#</a>When the work isn't in your database</h2>
<p>Some work can't join your transaction, like calling a payment API. Claiming the message first still stops a second copy from starting, but if your handler crashes after the payment and before the commit, the redelivered message will try again. For those calls, pass an idempotency key to the other system (many payment and email APIs accept one) and build it from the same message ID. Retrying with the same key then returns the original result instead of repeating the action.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Assume every message can arrive more than once, and sometimes at the same moment. A check followed by a write can't stop two copies racing each other. Claim the message ID with an insert that a unique key protects, do the work in the same transaction, and complete the message only after the commit. Add a unique business key as a backstop, and use sessions when order matters.</p>]]></content:encoded>
  </item>
  <item>
    <title>Pull requests that get good reviews</title>
    <link>https://www.cdsmith.dev/blog/2026-09-20-pull-requests-that-get-good-reviews/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-09-20-pull-requests-that-get-good-reviews/</guid>
    <pubDate>Sun, 20 Sep 2026 12:00:00 GMT</pubDate>
    <category>Practice</category>
    <category>Azure DevOps</category>
    <description>The quality of a code review depends a lot on the pull request. Small, focused changes with a clear description get faster and better reviews, and a template makes that the default.</description>
    <content:encoded><![CDATA[<p>A 2,000-line pull request with the description &quot;fixes&quot; gets one of two reviews: a rubber-stamp approval, or a week of delay. Neither catches bugs. Small, well-described pull requests get reviewed quickly and properly. Most of what makes a review useful is decided by the author before the reviewer opens it.</p>
<h2 id="keep-it-small-and-focused"><a class="anchor" href="#keep-it-small-and-focused" aria-hidden="true">#</a>Keep it small and focused</h2>
<ul><li><strong>One change per pull request.</strong> A refactor, a bug fix and a new feature are three pull requests, even if you did them together.</li><li><strong>Separate moves from changes.</strong> If you rename files or move code, do that in its own pull request, so the reviewer isn't hunting for a logic change inside 40 moved files.</li><li><strong>Aim for something a reviewer can read in one sitting,</strong> usually a few hundred lines at most. Large features can be merged in steps behind a feature flag.</li></ul>
<h2 id="write-a-description-that-answers-the-reviewers-questions"><a class="anchor" href="#write-a-description-that-answers-the-reviewers-questions" aria-hidden="true">#</a>Write a description that answers the reviewer's questions</h2>
<p>A reviewer needs to know:</p>
<ul><li><strong>Why</strong> this change exists: the problem or requirement, with a link to the work item</li><li><strong>What</strong> changed, in a sentence or two, and anything deliberately left out</li><li><strong>How it was tested,</strong> including anything manual</li><li><strong>Risks:</strong> migrations, configuration changes, behavior changes for other teams, anything that needs care when deploying</li></ul>
<p>Screenshots for UI changes and example requests and responses for API changes save the reviewer from running the code just to understand it.</p>
<h2 id="make-it-the-default-with-a-template"><a class="anchor" href="#make-it-the-default-with-a-template" aria-hidden="true">#</a>Make it the default with a template</h2>
<p>Azure Repos picks up a pull request template from a file such as <code>.azuredevops/pull_request_template.md</code> in the default branch:</p>
<pre data-lang="markdown"><code class="language-markdown">## Why
&lt;!-- The problem this solves. Link the work item. --&gt;

## What changed

## How I tested it
- [ ] Unit tests
- [ ] Integration tests
- [ ] Manually verified:

## Deployment notes
&lt;!-- Migrations, new settings, feature flags, anything to watch after release --&gt;</code></pre>
<p>Every new pull request starts with these headings, so writing a good description becomes the easy path.</p>
<h2 id="let-tools-handle-the-trivia"><a class="anchor" href="#let-tools-handle-the-trivia" aria-hidden="true">#</a>Let tools handle the trivia</h2>
<p>Reviews shouldn't spend time on formatting or naming conventions. Put those rules in <code>.editorconfig</code>, turn on analyzers, and run <code>dotnet format --verify-no-changes</code> and the build in a <strong>build validation</strong> branch policy. Machines check the style, so people can focus on design, correctness and risk.</p>
<h2 id="reviewing-well"><a class="anchor" href="#reviewing-well" aria-hidden="true">#</a>Reviewing well</h2>
<ul><li><strong>Review the design first,</strong> then the details. A comment that the whole approach should change is more useful on day one than after 30 line-level comments.</li><li><strong>Ask questions</strong> instead of issuing verdicts: &quot;What happens if this list is empty?&quot; invites a better answer than &quot;This is wrong.&quot;</li><li><strong>Label the weight of your comments,</strong> so the author knows what blocks approval and what's a suggestion.</li><li><strong>Respond quickly.</strong> A pull request waiting two days for review costs more than the review itself.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Keep pull requests small and focused, explain why, what and how it was tested, and use a template so that's the default. Automate formatting and style checks, and spend review time on design and correctness.</p>]]></content:encoded>
  </item>
  <item>
    <title>CQRS without the ceremony</title>
    <link>https://www.cdsmith.dev/blog/2026-09-13-cqrs-without-the-ceremony/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-09-13-cqrs-without-the-ceremony/</guid>
    <pubDate>Sun, 13 Sep 2026 12:00:00 GMT</pubDate>
    <category>Architecture</category>
    <category>.NET</category>
    <description>CQRS often arrives with separate databases, event sourcing and a mediator library. The core idea is much simpler and useful on its own: write through your domain model, read with plain queries.</description>
    <content:encoded><![CDATA[<p>Mention CQRS (Command Query Responsibility Segregation) and people picture a complex system: a write database and a read database, events synchronizing them, event sourcing, and handler classes for everything. That's one way to do it, and for most applications it's far more than they need.</p>
<p>The idea at the core is simple: <strong>reading data and changing data have different needs, so don't force them through the same model.</strong></p>
<h2 id="the-problem-with-one-model-for-both"><a class="anchor" href="#the-problem-with-one-model-for-both" aria-hidden="true">#</a>The problem with one model for both</h2>
<p>In a typical layered app, a screen that lists orders loads <code>Order</code> entities through a repository, with their line items and customer, then maps them to a DTO. The domain model was designed to protect business rules when things change. For reading, those rules don't matter, and loading full entities just to build a list is wasted work and wasted joins.</p>
<h2 id="commands-through-the-domain-model"><a class="anchor" href="#commands-through-the-domain-model" aria-hidden="true">#</a>Commands: through the domain model</h2>
<p>Changes go through entities that enforce the rules:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">Result</span>&gt; <span class="tok-method">CancelOrderAsync</span>(<span class="tok-type">Guid</span> orderId, <span class="tok-keyword">string</span> reason, <span class="tok-type">CancellationToken</span> ct)
{
    <span class="tok-keyword">var</span> order = <span class="tok-keyword">await</span> db.Orders.<span class="tok-method">Include</span>(o =&gt; o.Lines).<span class="tok-method">SingleOrDefaultAsync</span>(o =&gt; o.Id == orderId, ct);
    <span class="tok-keyword">if</span> (order <span class="tok-keyword">is</span> <span class="tok-keyword">null</span>) <span class="tok-keyword">return</span> <span class="tok-type">Result</span>.<span class="tok-method">NotFound</span>();

    order.<span class="tok-method">Cancel</span>(reason, time.<span class="tok-method">GetUtcNow</span>());   <span class="tok-comment">// invariants live here</span>
    <span class="tok-keyword">await</span> db.<span class="tok-method">SaveChangesAsync</span>(ct);
    <span class="tok-keyword">return</span> <span class="tok-type">Result</span>.<span class="tok-method">Success</span>();
}</code></pre>
<h2 id="queries-straight-to-the-shape-you-need"><a class="anchor" href="#queries-straight-to-the-shape-you-need" aria-hidden="true">#</a>Queries: straight to the shape you need</h2>
<p>Reads skip the domain model entirely and project directly into what the screen or API returns:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">List</span>&lt;<span class="tok-type">OrderSummary</span>&gt;&gt; <span class="tok-method">GetRecentOrdersAsync</span>(<span class="tok-keyword">string</span> customerId, <span class="tok-type">CancellationToken</span> ct) =&gt;
    db.Orders
        .<span class="tok-method">AsNoTracking</span>()
        .<span class="tok-method">Where</span>(o =&gt; o.CustomerId == customerId)
        .<span class="tok-method">OrderByDescending</span>(o =&gt; o.CreatedAt)
        .<span class="tok-method">Take</span>(<span class="tok-number">20</span>)
        .<span class="tok-method">Select</span>(o =&gt; <span class="tok-keyword">new</span> <span class="tok-type">OrderSummary</span>(o.Id, o.CreatedAt, o.Status.<span class="tok-method">ToString</span>(), o.Lines.<span class="tok-method">Sum</span>(l =&gt; l.Price * l.Quantity)))
        .<span class="tok-method">ToListAsync</span>(ct);</code></pre>
<p>EF Core translates the projection into a single query that selects only the columns needed. No tracking, no entities, no mapping layer. For complex reports, a raw SQL query or Dapper is equally fine on the read side.</p>
<p>That's CQRS: same database, same <code>DbContext</code>, different paths for reads and writes.</p>
<h2 id="what-you-dont-need-yet"><a class="anchor" href="#what-you-dont-need-yet" aria-hidden="true">#</a>What you don't need (yet)</h2>
<ul><li><strong>A separate read database.</strong> Add a read replica or a denormalized read store only when read load or query complexity actually demands it.</li><li><strong>Event sourcing.</strong> It's a separate pattern with its own costs. It pairs well with CQRS, but neither requires the other.</li><li><strong>A mediator library.</strong> Commands and queries can be plain methods or handler classes called directly.</li></ul>
<h2 id="where-it-pays-off"><a class="anchor" href="#where-it-pays-off" aria-hidden="true">#</a>Where it pays off</h2>
<ul><li><strong>Read performance:</strong> queries fetch exactly what they need.</li><li><strong>Simpler domain model:</strong> entities only need to support changes, not every screen's display needs.</li><li><strong>Clear intent:</strong> a method either changes state or returns data, never both, which makes code easier to reason about and test.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Start CQRS with a simple split in code: commands load entities and change them through the domain model; queries project straight to DTOs with no tracking. Add separate stores, events or libraries only when you have a concrete reason.</p>]]></content:encoded>
  </item>
  <item>
    <title>How retries really work with the Service Bus trigger</title>
    <link>https://www.cdsmith.dev/blog/2026-09-06-service-bus-trigger-retries-in-functions/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-09-06-service-bus-trigger-retries-in-functions/</guid>
    <pubDate>Sun, 06 Sep 2026 12:00:00 GMT</pubDate>
    <category>Azure</category>
    <category>Azure Functions</category>
    <category>Service Bus</category>
    <description>When a Service Bus-triggered function throws, the message is retried immediately, ten times in a row, and then dead-lettered. If you need a delay between attempts, you have to build it.</description>
    <content:encoded><![CDATA[<p>A Service Bus-triggered Azure Function calls a partner API that's down for maintenance. The function throws. What happens next surprises a lot of people.</p>
<h2 id="the-default-immediate-redelivery"><a class="anchor" href="#the-default-immediate-redelivery" aria-hidden="true">#</a>The default: immediate redelivery</h2>
<p>When the function fails, the message is <strong>abandoned</strong>. Its lock is released, its delivery count goes up, and it's available again straight away. The function picks it up again within milliseconds and fails again. That repeats until the delivery count reaches the queue's <code>MaxDeliveryCount</code> (10 by default), at which point the message moves to the dead-letter queue.</p>
<p>So a five-minute outage at the partner turns into ten failures in a couple of seconds and a dead-lettered message. No backoff at all.</p>
<p>Azure Functions has its own retry policies with fixed or exponential delays, but they don't apply to the Service Bus trigger, which relies on Service Bus's delivery count instead.</p>
<h2 id="sort-failures-into-three-kinds"><a class="anchor" href="#sort-failures-into-three-kinds" aria-hidden="true">#</a>Sort failures into three kinds</h2>
<p><strong>Permanent failures,</strong> such as invalid data or a record that doesn't exist. Retrying won't help. Dead-letter immediately with a reason:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">await</span> actions.<span class="tok-method">DeadLetterMessageAsync</span>(message,
    deadLetterReason: <span class="tok-string">"InvalidPayload"</span>, deadLetterErrorDescription: ex.Message, cancellationToken: ct);</code></pre>
<p><strong>Brief transient failures,</strong> such as a dropped connection or a single timeout. Retry inside the function with a resilience handler on the <code>HttpClient</code>, a few attempts over a few seconds, before the function fails at all.</p>
<p><strong>Longer outages,</strong> where you need minutes between attempts. That needs a scheduled retry.</p>
<h2 id="scheduled-retries"><a class="anchor" href="#scheduled-retries" aria-hidden="true">#</a>Scheduled retries</h2>
<p>Instead of failing, complete the original message and schedule a copy for later, with an attempt counter in its properties:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">catch</span> (<span class="tok-type">HttpRequestException</span>) <span class="tok-keyword">when</span> (<span class="tok-method">Attempt</span>(message) &lt; <span class="tok-number">5</span>)
{
    <span class="tok-keyword">var</span> attempt = <span class="tok-method">Attempt</span>(message) + <span class="tok-number">1</span>;
    <span class="tok-keyword">var</span> retry = <span class="tok-keyword">new</span> <span class="tok-type">ServiceBusMessage</span>(message)   <span class="tok-comment">// copies body and properties</span>
    {
        <span class="tok-type">ScheduledEnqueueTime</span> = <span class="tok-type">DateTimeOffset</span>.UtcNow.<span class="tok-method">AddMinutes</span>(<span class="tok-type">Math</span>.<span class="tok-method">Pow</span>(<span class="tok-number">2</span>, attempt))   <span class="tok-comment">// 2, 4, 8, 16, 32 minutes</span>
    };
    retry.ApplicationProperties[<span class="tok-string">"RetryAttempt"</span>] = attempt;

    <span class="tok-keyword">await</span> retrySender.<span class="tok-method">SendMessageAsync</span>(retry, ct);
    <span class="tok-keyword">await</span> actions.<span class="tok-method">CompleteMessageAsync</span>(message, ct);
}

<span class="tok-keyword">static</span> <span class="tok-keyword">int</span> <span class="tok-method">Attempt</span>(<span class="tok-type">ServiceBusReceivedMessage</span> m) =&gt;
    m.ApplicationProperties.<span class="tok-method">TryGetValue</span>(<span class="tok-string">"RetryAttempt"</span>, <span class="tok-keyword">out</span> <span class="tok-keyword">var</span> <span class="tok-keyword">value</span>) ? <span class="tok-type">Convert</span>.<span class="tok-method">ToInt32</span>(<span class="tok-keyword">value</span>) : <span class="tok-number">0</span>;</code></pre>
<p><code>retrySender</code> is a <code>ServiceBusSender</code> for the same queue, registered through dependency injection. After the last attempt, the exception propagates as usual and the message eventually dead-letters.</p>
<p>Two cautions. The copy keeps the original's <code>MessageId</code>, so if duplicate detection is turned on for the queue, give it a new one, such as the original ID plus the attempt number, or the broker drops it as a duplicate. And sending the copy and completing the original aren't atomic, so a crash in between can produce a duplicate. Handlers must be idempotent either way.</p>
<h2 id="tune-the-host"><a class="anchor" href="#tune-the-host" aria-hidden="true">#</a>Tune the host</h2>
<p>In <code>host.json</code>, under <code>extensions.serviceBus</code>, two settings matter most:</p>
<ul><li><strong><code>maxConcurrentCalls</code></strong> limits how many messages each instance processes at once. Lower it if the downstream system can't take the load.</li><li><strong><code>maxAutoLockRenewalDuration</code></strong> controls how long the host keeps renewing a message's lock. It should comfortably exceed your longest expected processing time, or the message is redelivered while still being processed.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>The Service Bus trigger retries failed messages immediately until the delivery count runs out. Dead-letter permanent failures straight away, retry brief glitches in-process, and schedule delayed copies of the message when you need real backoff.</p>]]></content:encoded>
  </item>
  <item>
    <title>Where does validation belong?</title>
    <link>https://www.cdsmith.dev/blog/2026-08-30-where-validation-belongs/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-08-30-where-validation-belongs/</guid>
    <pubDate>Sun, 30 Aug 2026 12:00:00 GMT</pubDate>
    <category>Architecture</category>
    <category>ASP.NET Core</category>
    <category>DDD</category>
    <description>Request validation, business rules and data constraints are different kinds of checks. Putting each one in the right layer avoids both duplication and gaps.</description>
    <content:encoded><![CDATA[<p>Ask a team where validation goes and you'll get several answers: in the controller, in a validator class, in the domain model, in the database. They're all partly right, because &quot;validation&quot; covers several different kinds of checks.</p>
<h2 id="1-is-the-request-well-formed-at-the-edge"><a class="anchor" href="#1-is-the-request-well-formed-at-the-edge" aria-hidden="true">#</a>1. Is the request well formed? At the edge</h2>
<p>Required fields, string lengths, formats and ranges. These checks are about the shape of the input, and they belong where the input arrives: the API endpoint or message handler.</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">PlaceOrderRequest</span>(
    [property: <span class="tok-type">Required</span>] <span class="tok-type">Guid</span>? <span class="tok-type">CustomerId</span>,
    [property: <span class="tok-type">Required</span>, <span class="tok-method">MinLength</span>(<span class="tok-number">1</span>)] <span class="tok-type">List</span>&lt;<span class="tok-type">LineItemRequest</span>&gt; <span class="tok-type">Items</span>,
    [property: <span class="tok-type">EmailAddress</span>] <span class="tok-keyword">string</span>? <span class="tok-type">ConfirmationEmail</span>);</code></pre>
<p>Data annotations, FluentValidation, or the built-in validation for minimal APIs added in .NET 10 (<code>builder.Services.AddValidation()</code>) all work here. The point is to reject bad requests with a clear <code>400</code> before any real work starts.</p>
<h2 id="2-does-it-obey-the-business-rules-in-the-domain"><a class="anchor" href="#2-does-it-obey-the-business-rules-in-the-domain" aria-hidden="true">#</a>2. Does it obey the business rules? In the domain</h2>
<p>&quot;An order can't be shipped before it's paid.&quot; &quot;A discount can't exceed 50%.&quot; &quot;A cancelled subscription can't be renewed.&quot; These are <strong>invariants</strong>: they must hold however the change is made, whether from the API, a background job, a message or an admin tool.</p>
<p>So they belong in the domain model, enforced by the methods that change state:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">void</span> <span class="tok-method">ApplyDiscount</span>(<span class="tok-keyword">decimal</span> percent)
{
    <span class="tok-keyword">if</span> (percent <span class="tok-keyword">is</span> &lt; <span class="tok-number">0</span> <span class="tok-keyword">or</span> &gt; <span class="tok-number">50</span>)
        <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-type">DomainException</span>(<span class="tok-string">"Discounts must be between 0% and 50%."</span>);
    <span class="tok-keyword">if</span> (<span class="tok-type">Status</span> != <span class="tok-type">OrderStatus</span>.Draft)
        <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-type">DomainException</span>(<span class="tok-string">"Discounts can only be applied to draft orders."</span>);

    <span class="tok-type">DiscountPercent</span> = percent;
}</code></pre>
<p>If these rules live only in a request validator, the first background job that changes orders directly skips them.</p>
<h2 id="3-does-it-fit-the-current-data-in-the-application-layer"><a class="anchor" href="#3-does-it-fit-the-current-data-in-the-application-layer" aria-hidden="true">#</a>3. Does it fit the current data? In the application layer</h2>
<p>&quot;The customer must exist.&quot; &quot;The SKU must be in stock.&quot; &quot;The email must not already be registered.&quot; These need a database lookup, so they belong in the handler or application service that coordinates the use case. They usually return a not-found or conflict result rather than a validation error.</p>
<h2 id="4-the-last-line-of-defense-the-database"><a class="anchor" href="#4-the-last-line-of-defense-the-database" aria-hidden="true">#</a>4. The last line of defense: the database</h2>
<p>Uniqueness checked in code has a race: two requests can both check, both find nothing, and both insert. A <strong>unique index</strong> in the database is what actually guarantees it. Use constraints, foreign keys and <code>NOT NULL</code> columns as a backstop for the rules that matter most, and translate their violations into proper responses.</p>
<h2 id="avoiding-duplication"><a class="anchor" href="#avoiding-duplication" aria-hidden="true">#</a>Avoiding duplication</h2>
<p>The same rule at several layers isn't always duplication. &quot;Quantity must be at least 1&quot; at the edge gives the user a friendly message, and the domain enforcing it again keeps the model safe from every other caller. What you want to avoid is business rules that exist <em>only</em> at the edge.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Validate request shape at the edge, enforce business invariants in the domain model, check rules that need current data in the application layer, and back critical guarantees with database constraints.</p>]]></content:encoded>
  </item>
  <item>
    <title>Stop copying pipeline YAML between repos</title>
    <link>https://www.cdsmith.dev/blog/2026-08-23-reusable-azure-pipelines-templates/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-08-23-reusable-azure-pipelines-templates/</guid>
    <pubDate>Sun, 23 Aug 2026 12:00:00 GMT</pubDate>
    <category>DevOps</category>
    <category>Azure DevOps</category>
    <description>Ten services with ten slightly different build pipelines means ten places to fix every change. Azure Pipelines templates let you define the steps once and reuse them everywhere.</description>
    <content:encoded><![CDATA[<p>Every service starts with a copy of the last service's <code>azure-pipelines.yml</code>. A year later, each copy has drifted: one runs tests with coverage and one doesn't, one pins the .NET SDK version and one uses whatever is installed, one has the security scan that was added after an incident and the rest don't.</p>
<p><strong>Templates</strong> let you define pipeline steps, jobs or stages once and reference them from every pipeline.</p>
<h2 id="a-step-template"><a class="anchor" href="#a-step-template" aria-hidden="true">#</a>A step template</h2>
<p>Put shared steps in their own file, with parameters for the parts that vary:</p>
<pre data-lang="yaml"><code class="language-yaml"># templates/build-dotnet.yml
parameters:
  - name: project
    type: string
  - name: dotnetVersion
    type: string
    default: '10.0.x'
  - name: runTests
    type: boolean
    default: true

steps:
  - task: UseDotNet@2
    inputs:
      version: ${{ parameters.dotnetVersion }}

  - script: dotnet build ${{ parameters.project }} --configuration Release
    displayName: Build

  - ${{ if eq(parameters.runTests, true) }}:
    - script: dotnet test --configuration Release --no-build --collect:"XPlat Code Coverage"
      displayName: Test

  - script: dotnet publish ${{ parameters.project }} --configuration Release --no-build --output $(Build.ArtifactStagingDirectory)
    displayName: Publish</code></pre>
<h2 id="use-it-in-a-pipeline"><a class="anchor" href="#use-it-in-a-pipeline" aria-hidden="true">#</a>Use it in a pipeline</h2>
<pre data-lang="yaml"><code class="language-yaml"># azure-pipelines.yml
trigger:
  - main

pool:
  vmImage: ubuntu-latest

steps:
  - template: templates/build-dotnet.yml
    parameters:
      project: src/Orders.Api/Orders.Api.csproj</code></pre>
<p>The pipeline file is now a few lines long and only says what's specific to this service.</p>
<h2 id="share-templates-across-repositories"><a class="anchor" href="#share-templates-across-repositories" aria-hidden="true">#</a>Share templates across repositories</h2>
<p>Keep templates in a dedicated repository and reference it as a resource:</p>
<pre data-lang="yaml"><code class="language-yaml">resources:
  repositories:
    - repository: templates
      type: git
      name: Platform/pipeline-templates
      ref: refs/tags/v3

steps:
  - template: build-dotnet.yml@templates
    parameters:
      project: src/Orders.Api/Orders.Api.csproj</code></pre>
<p>Pinning <code>ref</code> to a tag means a change to the shared template doesn't break every pipeline at once. Teams move to <code>v4</code> when they're ready.</p>
<h2 id="templates-at-every-level"><a class="anchor" href="#templates-at-every-level" aria-hidden="true">#</a>Templates at every level</h2>
<ul><li><strong>Step templates</strong> for a sequence of steps, like the build above</li><li><strong>Job templates</strong> for a whole job, such as &quot;run integration tests in a container&quot;</li><li><strong>Stage templates</strong> for a complete stage, such as &quot;deploy to an environment with approvals and a slot swap&quot;</li><li><strong>Extends templates,</strong> where a pipeline extends a central template that controls its overall structure. This is how organizations enforce required steps, such as security scanning, that individual pipelines can't remove.</li></ul>
<h2 id="template-expressions-vs-runtime-variables"><a class="anchor" href="#template-expressions-vs-runtime-variables" aria-hidden="true">#</a>Template expressions vs runtime variables</h2>
<p><code>${{ parameters.x }}</code> is resolved when the pipeline is compiled, before anything runs. That's why it can include or exclude whole steps with <code>${{ if }}</code>. <code>$(variable)</code> is resolved at runtime. Mixing them up is the most common source of confusing template behavior.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Move shared build and deploy steps into parameterized templates, keep them in their own repository, pin versions, and let each service's pipeline describe only what makes it different.</p>]]></content:encoded>
  </item>
  <item>
    <title>Changing a message without breaking its consumers</title>
    <link>https://www.cdsmith.dev/blog/2026-08-16-versioning-message-contracts/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-08-16-versioning-message-contracts/</guid>
    <pubDate>Sun, 16 Aug 2026 12:00:00 GMT</pubDate>
    <category>Integrations</category>
    <category>Service Bus</category>
    <category>Architecture</category>
    <description>Once other services consume your messages, the message format is a public contract. Additive changes are easy; breaking ones need a plan. Here&apos;s how to evolve message contracts safely.</description>
    <content:encoded><![CDATA[<p>With an HTTP API you can see who's calling you. With messages, you often can't. Any number of subscribers may be reading <code>OrderPlaced</code>, some written by other teams, some deployed months ago. And messages sitting in a queue or dead-letter queue were written by an older version of your code. A change that breaks deserialization breaks all of them.</p>
<h2 id="safe-changes-add-dont-change"><a class="anchor" href="#safe-changes-add-dont-change" aria-hidden="true">#</a>Safe changes: add, don't change</h2>
<p><strong>Adding an optional field</strong> is safe:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-comment">// Before</span>
<span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">OrderPlaced</span>(<span class="tok-type">Guid</span> <span class="tok-type">OrderId</span>, <span class="tok-keyword">string</span> <span class="tok-type">CustomerId</span>, <span class="tok-keyword">decimal</span> <span class="tok-type">Total</span>);

<span class="tok-comment">// After</span>
<span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">OrderPlaced</span>(<span class="tok-type">Guid</span> <span class="tok-type">OrderId</span>, <span class="tok-keyword">string</span> <span class="tok-type">CustomerId</span>, <span class="tok-keyword">decimal</span> <span class="tok-type">Total</span>, <span class="tok-keyword">string</span>? <span class="tok-type">Currency</span> = <span class="tok-keyword">null</span>);</code></pre>
<p>Old consumers don't know about <code>Currency</code> and ignore it: <code>System.Text.Json</code> skips unknown properties by default. New consumers reading old messages get <code>null</code> and must handle that, for example by assuming the currency the old system always used.</p>
<h2 id="breaking-changes"><a class="anchor" href="#breaking-changes" aria-hidden="true">#</a>Breaking changes</h2>
<p>These break consumers:</p>
<ul><li>Renaming or removing a field</li><li>Changing a field's type or meaning, such as <code>Total</code> switching from including tax to excluding it</li><li>Making an optional field required</li></ul>
<p>Changing meaning is the most dangerous, because nothing fails. Every consumer keeps working, with wrong numbers.</p>
<h2 id="making-a-breaking-change-safely"><a class="anchor" href="#making-a-breaking-change-safely" aria-hidden="true">#</a>Making a breaking change safely</h2>
<p>Introduce a <strong>new message type</strong> and run both for a while:</p>
<ol><li>Define <code>OrderPlacedV2</code> with the new shape.</li><li><strong>Update the producer to publish both</strong> <code>OrderPlaced</code> and <code>OrderPlacedV2</code> for every order.</li><li>Each consumer team moves to V2 at its own pace.</li><li>When no subscriber needs V1 any more, stop publishing it.</li></ol>
<p>Put the type and version where consumers and subscription filters can see them without reading the body:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">var</span> message = <span class="tok-keyword">new</span> <span class="tok-type">ServiceBusMessage</span>(<span class="tok-type">BinaryData</span>.<span class="tok-method">FromObjectAsJson</span>(v2))
{
    <span class="tok-type">Subject</span> = <span class="tok-string">"OrderPlaced"</span>,
    <span class="tok-type">MessageId</span> = <span class="tok-string">$"order-placed-v2-{v2.OrderId}"</span>
};
message.ApplicationProperties[<span class="tok-string">"MessageType"</span>] = <span class="tok-string">"OrderPlaced"</span>;
message.ApplicationProperties[<span class="tok-string">"SchemaVersion"</span>] = <span class="tok-number">2</span>;</code></pre>
<h2 id="habits-that-make-this-easier"><a class="anchor" href="#habits-that-make-this-easier" aria-hidden="true">#</a>Habits that make this easier</h2>
<ul><li><strong>Be a tolerant reader.</strong> Consumers should read only the fields they need and ignore everything else.</li><li><strong>Don't send internal types.</strong> Messages built from your entities change whenever the entity does. Define message contracts separately and deliberately.</li><li><strong>Include identifiers and enough context.</strong> Consumers that have to call back to you for basic details are coupled to your API as well as your messages.</li><li><strong>Keep contracts in one place,</strong> such as a shared contracts package or a schema registry, so changes are visible and reviewed.</li><li><strong>Deploy consumers before producers.</strong> When you add a field that consumers must handle, update them first so they're ready when it starts arriving.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Treat message formats as public contracts. Make additive, optional changes freely, never change a field's meaning silently, and introduce a new versioned message type for breaking changes, publishing both until every consumer has moved.</p>]]></content:encoded>
  </item>
  <item>
    <title>Parse text without allocating with Span&lt;T&gt;</title>
    <link>https://www.cdsmith.dev/blog/2026-08-09-parsing-with-span/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-08-09-parsing-with-span/</guid>
    <pubDate>Sun, 09 Aug 2026 12:00:00 GMT</pubDate>
    <category>C#</category>
    <category>.NET</category>
    <category>Performance</category>
    <description>Splitting a line with `string.Split` creates a new string for every field. For large files, `ReadOnlySpan&lt;char&gt;` lets you parse the same data with almost no allocations.</description>
    <content:encoded><![CDATA[<p>Integrations often mean parsing files: a nightly CSV export, a fixed-width bank file, a log. The obvious code works well:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">var</span> parts = line.<span class="tok-method">Split</span>(<span class="tok-string">','</span>);
<span class="tok-keyword">var</span> sku = parts[<span class="tok-number">0</span>];
<span class="tok-keyword">var</span> quantity = <span class="tok-keyword">int</span>.<span class="tok-method">Parse</span>(parts[<span class="tok-number">1</span>]);
<span class="tok-keyword">var</span> price = <span class="tok-keyword">decimal</span>.<span class="tok-method">Parse</span>(parts[<span class="tok-number">2</span>], <span class="tok-type">CultureInfo</span>.InvariantCulture);</code></pre>
<p>For each line, <code>Split</code> allocates an array plus one new string per field. Across a file with five million lines, that's tens of millions of short-lived objects for the garbage collector to clean up, most of which you only needed for a moment to parse a number.</p>
<h2 id="readonlyspanchar-a-view-not-a-copy"><a class="anchor" href="#readonlyspanchar-a-view-not-a-copy" aria-hidden="true">#</a>ReadOnlySpan&lt;char&gt;: a view, not a copy</h2>
<p>A <code>ReadOnlySpan&lt;char&gt;</code> is a window onto existing memory. Slicing it doesn't copy anything:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-type">ReadOnlySpan</span>&lt;<span class="tok-keyword">char</span>&gt; line = <span class="tok-string">"ABC-1,3,19.99"</span>;

<span class="tok-keyword">int</span> first = line.<span class="tok-method">IndexOf</span>(<span class="tok-string">','</span>);
<span class="tok-type">ReadOnlySpan</span>&lt;<span class="tok-keyword">char</span>&gt; sku = line[..first];        <span class="tok-comment">// "ABC-1", no new string</span>
<span class="tok-type">ReadOnlySpan</span>&lt;<span class="tok-keyword">char</span>&gt; rest = line[(first + <span class="tok-number">1</span>)..];</code></pre>
<p>Most parsing methods accept spans directly:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">int</span> quantity = <span class="tok-keyword">int</span>.<span class="tok-method">Parse</span>(rest[..rest.<span class="tok-method">IndexOf</span>(<span class="tok-string">','</span>)]);</code></pre>
<h2 id="splitting-into-ranges"><a class="anchor" href="#splitting-into-ranges" aria-hidden="true">#</a>Splitting into ranges</h2>
<p>Since .NET 8, <code>MemoryExtensions.Split</code> writes the positions of each field into a <code>Span&lt;Range&gt;</code> you provide, instead of creating strings:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">static</span> <span class="tok-type">OrderLine</span> <span class="tok-method">ParseLine</span>(<span class="tok-type">ReadOnlySpan</span>&lt;<span class="tok-keyword">char</span>&gt; line)
{
    <span class="tok-type">Span</span>&lt;<span class="tok-type">Range</span>&gt; fields = <span class="tok-keyword">stackalloc</span> <span class="tok-type">Range</span>[<span class="tok-number">4</span>];
    <span class="tok-keyword">int</span> count = line.<span class="tok-method">Split</span>(fields, <span class="tok-string">','</span>);
    <span class="tok-keyword">if</span> (count != <span class="tok-number">3</span>)
        <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-type">FormatException</span>(<span class="tok-string">$"Expected 3 fields, found {count}."</span>);

    <span class="tok-keyword">return</span> <span class="tok-keyword">new</span> <span class="tok-type">OrderLine</span>(
        <span class="tok-type">Sku</span>: line[fields[<span class="tok-number">0</span>]].<span class="tok-method">ToString</span>(),   <span class="tok-comment">// allocate only for the value you keep</span>
        <span class="tok-type">Quantity</span>: <span class="tok-keyword">int</span>.<span class="tok-method">Parse</span>(line[fields[<span class="tok-number">1</span>]]),
        <span class="tok-type">Price</span>: <span class="tok-keyword">decimal</span>.<span class="tok-method">Parse</span>(line[fields[<span class="tok-number">2</span>]], <span class="tok-type">CultureInfo</span>.InvariantCulture));
}</code></pre>
<p>The only allocation left is the SKU string, because the result needs to keep it. The numbers are parsed straight from the original text. (The range span has one extra slot so a line with too many fields is detected rather than silently merged into the last field.)</p>
<h2 id="where-spans-cant-go"><a class="anchor" href="#where-spans-cant-go" aria-hidden="true">#</a>Where spans can't go</h2>
<p><code>Span&lt;T&gt;</code> is a <code>ref struct</code>: it can only live on the stack. You can't store one in a field of a class, capture it in a lambda, or keep it across an <code>await</code>. Parse synchronously, line by line, inside a non-async method, and keep your async code at the level of reading lines from the stream.</p>
<h2 id="is-it-worth-it"><a class="anchor" href="#is-it-worth-it" aria-hidden="true">#</a>Is it worth it?</h2>
<p>For a config file or an API response, no: readability wins, and <code>string.Split</code> is fine. Spans pay off when you parse a lot of text repeatedly, such as large import files, high-volume message processing or hot request paths. Measure first with BenchmarkDotNet or a memory profiler, and optimize where the allocations actually are.</p>
<h2 id="real-csv-is-harder"><a class="anchor" href="#real-csv-is-harder" aria-hidden="true">#</a>Real CSV is harder</h2>
<p>Simple splitting breaks on quoted fields containing commas, escaped quotes and embedded line breaks. For real-world CSV, use a proper parser such as CsvHelper or Sep, which are themselves built on these techniques.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>When parsing large volumes of text, slice <code>ReadOnlySpan&lt;char&gt;</code> instead of creating substrings, parse numbers directly from spans, and only allocate strings for values you keep. Keep it for measured hot paths, and use a real CSV library for real CSV.</p>]]></content:encoded>
  </item>
  <item>
    <title>MongoDB indexes from C#, and how to know you need one</title>
    <link>https://www.cdsmith.dev/blog/2026-08-02-mongodb-indexes-with-the-csharp-driver/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-08-02-mongodb-indexes-with-the-csharp-driver/</guid>
    <pubDate>Sun, 02 Aug 2026 12:00:00 GMT</pubDate>
    <category>MongoDB</category>
    <category>.NET</category>
    <category>Performance</category>
    <description>A query that&apos;s fast with a thousand documents can scan millions in production. Here&apos;s how to create indexes from the C# driver, order their fields, and check that queries actually use them.</description>
    <content:encoded><![CDATA[<p>MongoDB is quick to start with: no schema to define, no migrations to run. The downside is that it never complains about a missing index. A query on an unindexed field still works. It just reads every document in the collection to find the ones that match, and that only becomes a problem once the collection is big.</p>
<h2 id="create-indexes-from-code"><a class="anchor" href="#create-indexes-from-code" aria-hidden="true">#</a>Create indexes from code</h2>
<p>The C# driver can create indexes with strongly typed definitions:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">var</span> orders = database.<span class="tok-method">GetCollection</span>&lt;<span class="tok-type">Order</span>&gt;(<span class="tok-string">"orders"</span>);

<span class="tok-keyword">var</span> byCustomerAndDate = <span class="tok-keyword">new</span> <span class="tok-type">CreateIndexModel</span>&lt;<span class="tok-type">Order</span>&gt;(
    <span class="tok-type">Builders</span>&lt;<span class="tok-type">Order</span>&gt;.IndexKeys
        .<span class="tok-method">Ascending</span>(o =&gt; o.CustomerId)
        .<span class="tok-method">Descending</span>(o =&gt; o.CreatedAt),
    <span class="tok-keyword">new</span> <span class="tok-type">CreateIndexOptions</span> { <span class="tok-type">Name</span> = <span class="tok-string">"customer_createdAt"</span> });

<span class="tok-keyword">var</span> uniqueExternalId = <span class="tok-keyword">new</span> <span class="tok-type">CreateIndexModel</span>&lt;<span class="tok-type">Order</span>&gt;(
    <span class="tok-type">Builders</span>&lt;<span class="tok-type">Order</span>&gt;.IndexKeys.<span class="tok-method">Ascending</span>(o =&gt; o.ExternalId),
    <span class="tok-keyword">new</span> <span class="tok-type">CreateIndexOptions</span> { <span class="tok-type">Name</span> = <span class="tok-string">"externalId_unique"</span>, <span class="tok-type">Unique</span> = <span class="tok-keyword">true</span> });

<span class="tok-keyword">await</span> orders.Indexes.<span class="tok-method">CreateManyAsync</span>([byCustomerAndDate, uniqueExternalId], ct);</code></pre>
<p>Creating an index that already exists with the same definition does nothing, so running this at startup or from a deployment step is safe. For large collections, prefer a deployment step, since building a new index takes time and resources.</p>
<h2 id="order-compound-index-fields-well"><a class="anchor" href="#order-compound-index-fields-well" aria-hidden="true">#</a>Order compound index fields well</h2>
<p>For a compound index, the order of fields matters. A useful guideline is <strong>ESR: Equality, Sort, Range</strong>:</p>
<ol><li>Fields you match exactly first, such as <code>CustomerId</code></li><li>Then fields you sort by, such as <code>CreatedAt</code></li><li>Then fields you filter by range, such as <code>Total &gt; 100</code></li></ol>
<p>The index above serves this query perfectly:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">var</span> recent = <span class="tok-keyword">await</span> orders
    .<span class="tok-method">Find</span>(o =&gt; o.CustomerId == customerId)
    .<span class="tok-method">SortByDescending</span>(o =&gt; o.CreatedAt)
    .<span class="tok-method">Limit</span>(<span class="tok-number">20</span>)
    .<span class="tok-method">ToListAsync</span>(ct);</code></pre>
<p>MongoDB finds the customer's entries in the index, already in date order, and stops after 20. No sorting in memory and no scanning.</p>
<h2 id="check-that-the-index-is-used"><a class="anchor" href="#check-that-the-index-is-used" aria-hidden="true">#</a>Check that the index is used</h2>
<p>Ask MongoDB how it runs the query. In <code>mongosh</code> or MongoDB Compass:</p>
<pre data-lang="javascript"><code class="language-javascript">db.orders.<span class="tok-method">find</span>({ <span class="tok-type">CustomerId</span>: <span class="tok-string">"C-42"</span> }).<span class="tok-method">sort</span>({ <span class="tok-type">CreatedAt</span>: -<span class="tok-number">1</span> }).<span class="tok-method">limit</span>(<span class="tok-number">20</span>).<span class="tok-method">explain</span>(<span class="tok-string">"executionStats"</span>)</code></pre>
<p>Look for:</p>
<ul><li><strong><code>IXSCAN</code></strong> in the winning plan: an index was used. <strong><code>COLLSCAN</code></strong> means a full collection scan.</li><li><strong><code>totalDocsExamined</code></strong> close to <strong><code>nReturned</code></strong>. Examining 50,000 documents to return 20 means the index doesn't fit the query.</li></ul>
<p>If you use MongoDB Atlas, its Performance Advisor watches slow queries and suggests indexes for them.</p>
<h2 id="useful-index-options"><a class="anchor" href="#useful-index-options" aria-hidden="true">#</a>Useful index options</h2>
<ul><li><strong>Unique</strong> indexes enforce business rules, such as one order per external ID, and protect you from duplicate inserts.</li><li><strong>TTL</strong> indexes delete documents automatically after a period, which is ideal for logs, idempotency keys and other temporary data: <code>new CreateIndexOptions { ExpireAfter = TimeSpan.FromDays(7) }</code> on a date field.</li></ul>
<h2 id="dont-index-everything"><a class="anchor" href="#dont-index-everything" aria-hidden="true">#</a>Don't index everything</h2>
<p>Every index has to be updated on every write and takes memory. Index the queries your application actually runs often, and remove indexes that aren't used.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Create indexes from code for the queries you run often, order compound index fields by equality, then sort, then range, and use <code>explain</code> to confirm queries use them. Unique and TTL indexes also enforce rules and clean up data for you.</p>]]></content:encoded>
  </item>
  <item>
    <title>Replace legacy systems one route at a time</title>
    <link>https://www.cdsmith.dev/blog/2026-07-26-strangler-fig-legacy-migration/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-07-26-strangler-fig-legacy-migration/</guid>
    <pubDate>Sun, 26 Jul 2026 12:00:00 GMT</pubDate>
    <category>Architecture</category>
    <category>ASP.NET Core</category>
    <category>Practice</category>
    <description>Big-bang rewrites of legacy apps tend to run late and launch with surprises. The strangler fig pattern moves functionality to the new system piece by piece, with a working system at every step.</description>
    <content:encoded><![CDATA[<p>Most teams eventually face an old system that's hard to change: an ASP.NET Web Forms or MVC 5 app on .NET Framework, years of business rules nobody fully remembers, and no tests. The tempting plan is to rewrite it from scratch and switch over in one go.</p>
<p>That plan has a track record. The rewrite takes longer than expected, the old system keeps changing while you build, and the cutover day reveals all the behavior nobody knew about.</p>
<h2 id="the-strangler-fig"><a class="anchor" href="#the-strangler-fig" aria-hidden="true">#</a>The strangler fig</h2>
<p>The <strong>strangler fig pattern</strong>, named by Martin Fowler after a vine that slowly grows around a tree until it replaces it, takes the opposite approach:</p>
<ol><li>Put a <strong>routing layer</strong> in front of the legacy system. At first it sends everything to the old app.</li><li>Build one piece of functionality in the new system.</li><li>Route just that piece to the new system.</li><li>Repeat until nothing goes to the old system, then switch it off.</li></ol>
<p>At every step you have a working system in production, and each step is small enough to roll back.</p>
<h2 id="the-routing-layer-with-yarp"><a class="anchor" href="#the-routing-layer-with-yarp" aria-hidden="true">#</a>The routing layer with YARP</h2>
<p>For web apps, a reverse proxy is the natural router, and YARP (Yet Another Reverse Proxy) runs inside an ASP.NET Core app:</p>
<pre data-lang="csharp"><code class="language-csharp">builder.Services.<span class="tok-method">AddReverseProxy</span>()
    .<span class="tok-method">LoadFromConfig</span>(builder.Configuration.<span class="tok-method">GetSection</span>(<span class="tok-string">"ReverseProxy"</span>));

<span class="tok-keyword">var</span> app = builder.<span class="tok-method">Build</span>();

app.<span class="tok-method">MapControllers</span>();      <span class="tok-comment">// routes the new app handles itself</span>
app.<span class="tok-method">MapReverseProxy</span>();     <span class="tok-comment">// everything else goes to the legacy app</span>

app.<span class="tok-method">Run</span>();</code></pre>
<pre data-lang="json"><code class="language-json">{
  <span class="tok-attr">"ReverseProxy"</span>: {
    <span class="tok-attr">"Routes"</span>: {
      <span class="tok-attr">"legacy"</span>: { <span class="tok-attr">"ClusterId"</span>: <span class="tok-string">"legacy"</span>, <span class="tok-attr">"Match"</span>: { <span class="tok-attr">"Path"</span>: <span class="tok-string">"{**catch-all}"</span> }, <span class="tok-attr">"Order"</span>: <span class="tok-number">1000</span> }
    },
    <span class="tok-attr">"Clusters"</span>: {
      <span class="tok-attr">"legacy"</span>: { <span class="tok-attr">"Destinations"</span>: { <span class="tok-attr">"main"</span>: { <span class="tok-attr">"Address"</span>: <span class="tok-string">"https://legacy.internal.example.com/"</span> } } }
    }
  }
}</code></pre>
<p>The new ASP.NET Core app <em>is</em> the front door. Endpoints it implements are handled directly, and the catch-all route with a high <code>Order</code> value proxies everything else to the old app. Moving a feature is a matter of implementing its endpoints. Microsoft uses the same approach in its guidance for incrementally migrating ASP.NET Framework apps to ASP.NET Core.</p>
<h2 id="pick-the-order-carefully"><a class="anchor" href="#pick-the-order-carefully" aria-hidden="true">#</a>Pick the order carefully</h2>
<ul><li><strong>Start with something small and low-risk</strong> to prove the routing, deployment and monitoring work.</li><li><strong>Then go after the areas that change most often.</strong> That's where the old system costs you most, and where the new one pays off soonest.</li><li><strong>Leave stable, rarely touched areas for last.</strong> Some may never be worth moving.</li></ul>
<h2 id="the-hard-part-is-data"><a class="anchor" href="#the-hard-part-is-data" aria-hidden="true">#</a>The hard part is data</h2>
<p>Routing requests is easy. Shared data is hard. While both systems run, they often need the same database. Options include letting the new system read and write the legacy database at first, then moving each area's tables to the new system's database along with its feature. Whatever you choose, decide for each table which system owns it at each stage.</p>
<p>Shared concerns such as login and sessions need a plan too, so users don't notice which system served a page.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Don't rewrite a legacy system in one go. Put a routing layer in front of it, move features across one route at a time, starting small, and plan data ownership as carefully as the code. You deliver value continuously, and every step can be rolled back.</p>]]></content:encoded>
  </item>
  <item>
    <title>Every outbound call needs a timeout</title>
    <link>https://www.cdsmith.dev/blog/2026-07-19-timeouts-done-right/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-07-19-timeouts-done-right/</guid>
    <pubDate>Sun, 19 Jul 2026 12:00:00 GMT</pubDate>
    <category>.NET</category>
    <category>Integrations</category>
    <category>Resilience</category>
    <description>The default HttpClient timeout is 100 seconds, far longer than anyone waiting on you. Set timeouts deliberately, layer them so they make sense together, and pass cancellation through.</description>
    <content:encoded><![CDATA[<p>A dependency doesn't have to fail to take you down. It only has to get slow. If it stops responding and your calls to it wait 100 seconds each, requests pile up, threads and connections run out, and your service stops answering too. A timeout turns &quot;hangs forever&quot; into a fast, handled failure.</p>
<h2 id="know-your-defaults"><a class="anchor" href="#know-your-defaults" aria-hidden="true">#</a>Know your defaults</h2>
<div class="table-wrap"><table><thead><tr><th>Call</th><th>Default timeout</th></tr></thead><tbody><tr><td><code>HttpClient</code> request</td><td>100 seconds</td></tr><tr><td>SQL Server command (ADO.NET and EF Core)</td><td>30 seconds</td></tr><tr><td>SQL Server connection</td><td>15 seconds</td></tr><tr><td>Azure SDK operation (e.g. Service Bus <code>TryTimeout</code>)</td><td>60 seconds</td></tr></tbody></table></div>
<p>Those are generous fallbacks, not good choices. If your own API promises a response in 5 seconds, a 100-second call inside it makes no sense.</p>
<h2 id="work-backwards-from-the-caller"><a class="anchor" href="#work-backwards-from-the-caller" aria-hidden="true">#</a>Work backwards from the caller</h2>
<p>Start with how long the caller of your service will wait, and budget from there. If an API endpoint should respond within 10 seconds and calls two dependencies in sequence, each call gets well under 5 seconds, including any retries.</p>
<p>The rule: <strong>every timeout must be shorter than the one waiting on it.</strong> Otherwise your caller gives up, but your service keeps working on a response nobody will read.</p>
<h2 id="layer-the-timeouts-for-http"><a class="anchor" href="#layer-the-timeouts-for-http" aria-hidden="true">#</a>Layer the timeouts for HTTP</h2>
<p>With the resilience handler there are two timeouts that matter:</p>
<ul><li><strong>Attempt timeout:</strong> how long a single try may take.</li><li><strong>Total timeout:</strong> how long the whole operation, including retries and their delays, may take.</li></ul>
<pre data-lang="csharp"><code class="language-csharp">builder.Services.<span class="tok-method">AddHttpClient</span>&lt;<span class="tok-type">PricingClient</span>&gt;()
    .<span class="tok-method">AddStandardResilienceHandler</span>(options =&gt;
    {
        options.AttemptTimeout.Timeout = <span class="tok-type">TimeSpan</span>.<span class="tok-method">FromSeconds</span>(<span class="tok-number">2</span>);
        options.TotalRequestTimeout.Timeout = <span class="tok-type">TimeSpan</span>.<span class="tok-method">FromSeconds</span>(<span class="tok-number">6</span>);
        options.CircuitBreaker.SamplingDuration = <span class="tok-type">TimeSpan</span>.<span class="tok-method">FromSeconds</span>(<span class="tok-number">10</span>); <span class="tok-comment">// must be at least twice the attempt timeout</span>
    });</code></pre>
<p>Watch out for <code>HttpClient.Timeout</code> itself. It wraps the <strong>entire</strong> handler pipeline, retries included, so if it's shorter than the total resilience timeout it cuts the retries off. With a resilience handler, leave it at its default or set it higher than the total timeout, and let the handler do the timing.</p>
<h2 id="pass-cancellation-through"><a class="anchor" href="#pass-cancellation-through" aria-hidden="true">#</a>Pass cancellation through</h2>
<p>A timeout in one place should stop work everywhere below it. Accept a <code>CancellationToken</code> and pass it to every async call, so when a request times out or the caller disconnects, the database query and downstream calls are cancelled too, rather than finishing for nobody.</p>
<p>For your own deadline inside a method:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">using</span> <span class="tok-keyword">var</span> cts = <span class="tok-type">CancellationTokenSource</span>.<span class="tok-method">CreateLinkedTokenSource</span>(ct);
cts.<span class="tok-method">CancelAfter</span>(<span class="tok-type">TimeSpan</span>.<span class="tok-method">FromSeconds</span>(<span class="tok-number">3</span>));

<span class="tok-keyword">var</span> prices = <span class="tok-keyword">await</span> pricing.<span class="tok-method">GetPricesAsync</span>(skus, cts.Token);</code></pre>
<h2 id="dont-forget-the-database"><a class="anchor" href="#dont-forget-the-database" aria-hidden="true">#</a>Don't forget the database</h2>
<p>EF Core's command timeout applies per command. For a known slow report, raise it for that query only, rather than globally:</p>
<pre data-lang="csharp"><code class="language-csharp">db.Database.<span class="tok-method">SetCommandTimeout</span>(<span class="tok-type">TimeSpan</span>.<span class="tok-method">FromMinutes</span>(<span class="tok-number">2</span>));</code></pre>
<p>And if a normal request regularly needs more than a few seconds of database time, the fix is usually an index or a different query, not a longer timeout.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Never rely on default timeouts. Budget from what your caller will wait, make each inner timeout shorter than the one around it, set attempt and total timeouts on HTTP calls, and pass cancellation tokens all the way down.</p>]]></content:encoded>
  </item>
  <item>
    <title>Pattern matching that reads like the business rule</title>
    <link>https://www.cdsmith.dev/blog/2026-07-12-pattern-matching-that-reads-well/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-07-12-pattern-matching-that-reads-well/</guid>
    <pubDate>Sun, 12 Jul 2026 12:00:00 GMT</pubDate>
    <category>C#</category>
    <description>Switch expressions, property patterns and list patterns can turn a page of nested ifs into a few lines that read like the requirements. Here are the forms worth knowing.</description>
    <content:encoded><![CDATA[<p>Business rules often arrive as a table: if the customer is a partner and the order is over $10,000, apply this; if it's a first order, apply that. Written with nested <code>if</code> statements, the rule is hard to see. C# pattern matching lets the code look much more like the table.</p>
<h2 id="switch-expressions"><a class="anchor" href="#switch-expressions" aria-hidden="true">#</a>Switch expressions</h2>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">decimal</span> <span class="tok-method">ShippingCost</span>(<span class="tok-type">Order</span> order) =&gt; order <span class="tok-keyword">switch</span>
{
    { <span class="tok-type">Total</span>: &gt;= <span class="tok-number">100m</span> } =&gt; <span class="tok-number">0m</span>,
    { <span class="tok-type">Destination</span>.Country: <span class="tok-string">"US"</span>, <span class="tok-type">Express</span>: <span class="tok-keyword">true</span> } =&gt; <span class="tok-number">25m</span>,
    { <span class="tok-type">Destination</span>.Country: <span class="tok-string">"US"</span> } =&gt; <span class="tok-number">8m</span>,
    { <span class="tok-type">Express</span>: <span class="tok-keyword">true</span> } =&gt; <span class="tok-number">60m</span>,
    _ =&gt; <span class="tok-number">30m</span>
};</code></pre>
<p>Each line is one rule, checked top to bottom, and the first match wins. That example combines several pattern types.</p>
<h2 id="property-patterns"><a class="anchor" href="#property-patterns" aria-hidden="true">#</a>Property patterns</h2>
<p><code>{ Total: &gt;= 100m }</code> matches when <code>Total</code> is at least 100. Patterns can reach into nested properties with a dot, as in <code>{ Destination.Country: &quot;US&quot; }</code>, and several properties in one pattern must all match.</p>
<h2 id="relational-and-logical-patterns"><a class="anchor" href="#relational-and-logical-patterns" aria-hidden="true">#</a>Relational and logical patterns</h2>
<p>Comparisons and <code>and</code>, <code>or</code>, <code>not</code> let you express ranges directly:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">string</span> <span class="tok-method">RiskBand</span>(<span class="tok-keyword">int</span> score) =&gt; score <span class="tok-keyword">switch</span>
{
    &lt; <span class="tok-number">0</span> <span class="tok-keyword">or</span> &gt; <span class="tok-number">1000</span> =&gt; <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-type">ArgumentOutOfRangeException</span>(<span class="tok-keyword">nameof</span>(score)),
    &gt;= <span class="tok-number">800</span> =&gt; <span class="tok-string">"Low"</span>,
    &gt;= <span class="tok-number">500</span> =&gt; <span class="tok-string">"Medium"</span>,
    _ =&gt; <span class="tok-string">"High"</span>
};</code></pre>
<p><code>is not null</code> is the clearest null check in C# for the same reason:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">if</span> (order.Coupon <span class="tok-keyword">is</span> <span class="tok-keyword">not</span> <span class="tok-keyword">null</span>) { <span class="tok-comment">/* ... */</span> }</code></pre>
<h2 id="type-patterns-with-conditions"><a class="anchor" href="#type-patterns-with-conditions" aria-hidden="true">#</a>Type patterns with conditions</h2>
<p>Handling different message types is a natural fit:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-type">Task</span> <span class="tok-method">HandleAsync</span>(<span class="tok-keyword">object</span> message, <span class="tok-type">CancellationToken</span> ct) =&gt; message <span class="tok-keyword">switch</span>
{
    <span class="tok-type">OrderPlaced</span> { <span class="tok-type">Total</span>: &gt; <span class="tok-number">10_000m</span> } large =&gt; fraudCheck.<span class="tok-method">ReviewAsync</span>(large, ct),
    <span class="tok-type">OrderPlaced</span> placed =&gt; fulfillment.<span class="tok-method">StartAsync</span>(placed, ct),
    <span class="tok-type">OrderCancelled</span> cancelled =&gt; fulfillment.<span class="tok-method">StopAsync</span>(cancelled.OrderId, ct),
    _ =&gt; <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-type">NotSupportedException</span>(<span class="tok-string">$"Unknown message {message.GetType().Name}"</span>)
};</code></pre>
<h2 id="list-patterns"><a class="anchor" href="#list-patterns" aria-hidden="true">#</a>List patterns</h2>
<p>Since C# 11 you can match on the shape of arrays and lists:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">string</span> <span class="tok-method">Describe</span>(<span class="tok-keyword">string</span>[] parts) =&gt; parts <span class="tok-keyword">switch</span>
{
    [] =&gt; <span class="tok-string">"empty"</span>,
    [<span class="tok-keyword">var</span> only] =&gt; <span class="tok-string">$"just {only}"</span>,
    [<span class="tok-keyword">var</span> first, .., <span class="tok-keyword">var</span> last] =&gt; <span class="tok-string">$"from {first} to {last}"</span>
};</code></pre>
<p>That's handy for parsing simple command formats or CSV lines without index arithmetic.</p>
<h2 id="let-the-compiler-check-exhaustiveness"><a class="anchor" href="#let-the-compiler-check-exhaustiveness" aria-hidden="true">#</a>Let the compiler check exhaustiveness</h2>
<p>If a switch expression on an enum doesn't handle every value, the compiler warns you (CS8509). Add a value to the enum later and every switch that forgot it gets flagged. Avoid a catch-all <code>_</code> arm on enums when you want that safety net.</p>
<h2 id="know-when-to-stop"><a class="anchor" href="#know-when-to-stop" aria-hidden="true">#</a>Know when to stop</h2>
<p>A switch with fifteen arms and three levels of nested patterns isn't more readable than the <code>if</code> statements it replaced. If a rule needs a comment to explain it, give it a well-named method or variable instead.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Use switch expressions with property, relational and list patterns to write business rules as a list of cases that reads like the requirement. Let the compiler check that you've covered every case, and keep each pattern simple enough to read at a glance.</p>]]></content:encoded>
  </item>
  <item>
    <title>Ship code dark with feature flags</title>
    <link>https://www.cdsmith.dev/blog/2026-07-05-feature-flags-with-azure-app-configuration/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-07-05-feature-flags-with-azure-app-configuration/</guid>
    <pubDate>Sun, 05 Jul 2026 12:00:00 GMT</pubDate>
    <category>Azure</category>
    <category>ASP.NET Core</category>
    <category>DevOps</category>
    <description>Feature flags separate deploying code from releasing it. With Azure App Configuration, you can turn a feature on for 10% of users, or off in an emergency, without a deployment.</description>
    <content:encoded><![CDATA[<p>Without feature flags, deploying and releasing are the same event. Merging a half-finished feature means hiding it with a long-lived branch, and turning off a misbehaving feature means an emergency deployment.</p>
<p>A <strong>feature flag</strong> is a switch your code checks at runtime. Deploy the code with the flag off, turn it on when you're ready, and turn it off again in seconds if something goes wrong.</p>
<h2 id="setting-it-up"><a class="anchor" href="#setting-it-up" aria-hidden="true">#</a>Setting it up</h2>
<p>Store flags in <strong>Azure App Configuration</strong> and read them with the feature management libraries:</p>
<pre data-lang="csharp"><code class="language-csharp">builder.Configuration.<span class="tok-method">AddAzureAppConfiguration</span>(options =&gt;
{
    options.<span class="tok-method">Connect</span>(<span class="tok-keyword">new</span> <span class="tok-type">Uri</span>(<span class="tok-string">"https://my-config.azconfig.io"</span>), <span class="tok-keyword">new</span> <span class="tok-type">DefaultAzureCredential</span>())
           .<span class="tok-method">UseFeatureFlags</span>(flags =&gt; flags.<span class="tok-method">SetRefreshInterval</span>(<span class="tok-type">TimeSpan</span>.<span class="tok-method">FromSeconds</span>(<span class="tok-number">30</span>)));
});

builder.Services.<span class="tok-method">AddAzureAppConfiguration</span>();
builder.Services.<span class="tok-method">AddFeatureManagement</span>();

<span class="tok-keyword">var</span> app = builder.<span class="tok-method">Build</span>();
app.<span class="tok-method">UseAzureAppConfiguration</span>();   <span class="tok-comment">// refreshes flags as requests come in</span></code></pre>
<p>The packages are <code>Microsoft.Azure.AppConfiguration.AspNetCore</code> and <code>Microsoft.FeatureManagement.AspNetCore</code>.</p>
<h2 id="checking-a-flag"><a class="anchor" href="#checking-a-flag" aria-hidden="true">#</a>Checking a flag</h2>
<pre data-lang="csharp"><code class="language-csharp">app.<span class="tok-method">MapGet</span>(<span class="tok-string">"/quotes/{id}"</span>, <span class="tok-keyword">async</span> (<span class="tok-type">Guid</span> id, <span class="tok-type">IFeatureManager</span> features, <span class="tok-type">QuoteService</span> quotes) =&gt;
{
    <span class="tok-keyword">var</span> quote = <span class="tok-keyword">await</span> features.<span class="tok-method">IsEnabledAsync</span>(<span class="tok-string">"NewPricingEngine"</span>)
        ? <span class="tok-keyword">await</span> quotes.<span class="tok-method">PriceWithNewEngineAsync</span>(id)
        : <span class="tok-keyword">await</span> quotes.<span class="tok-method">PriceAsync</span>(id);

    <span class="tok-keyword">return</span> <span class="tok-type">Results</span>.<span class="tok-method">Ok</span>(quote);
});</code></pre>
<p>For MVC controllers or actions, <code>[FeatureGate(&quot;NewPricingEngine&quot;)]</code> returns a <code>404</code> while the flag is off.</p>
<h2 id="rolling-out-gradually"><a class="anchor" href="#rolling-out-gradually" aria-hidden="true">#</a>Rolling out gradually</h2>
<p>A flag doesn't have to be all or nothing. Built-in filters let you enable it for:</p>
<ul><li><strong>A percentage of requests,</strong> for example 10%, then 50%, then 100%</li><li><strong>Specific users or groups,</strong> through the targeting filter, such as internal staff first</li><li><strong>A time window,</strong> such as a promotion that switches itself on and off</li></ul>
<p>You change these in the App Configuration portal, and apps pick up the change at their next refresh, within the interval you set.</p>
<h2 id="the-kill-switch"><a class="anchor" href="#the-kill-switch" aria-hidden="true">#</a>The kill switch</h2>
<p>Flags aren't only for new features. Wrapping a risky integration, such as a call to a new partner API, in a flag gives you an off switch that works faster than any deployment and doesn't need a developer.</p>
<h2 id="clean-up"><a class="anchor" href="#clean-up" aria-hidden="true">#</a>Clean up</h2>
<p>Every flag is an <code>if</code> statement with two code paths to test and maintain. Once a feature is fully rolled out and stable, remove the flag and the old code path. Keep a short list of active flags with an owner and an expected removal date, or they accumulate for years.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Use feature flags to separate deploying code from releasing it. Store them in Azure App Configuration, roll features out gradually to a percentage or a group of users, keep a kill switch around risky integrations, and remove flags once they've done their job.</p>]]></content:encoded>
  </item>
  <item>
    <title>Keep other systems&apos; models out of yours</title>
    <link>https://www.cdsmith.dev/blog/2026-06-28-anti-corruption-layer/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-06-28-anti-corruption-layer/</guid>
    <pubDate>Sun, 28 Jun 2026 12:00:00 GMT</pubDate>
    <category>Architecture</category>
    <category>Integrations</category>
    <category>DDD</category>
    <description>When a third-party API&apos;s field names and status codes spread through your code, every change on their side becomes a change on yours. An anti-corruption layer stops it at the boundary.</description>
    <content:encoded><![CDATA[<p>You integrate with a CRM. Its API returns accounts with fields like <code>Acct_Status__c</code>, statuses such as <code>&quot;Prospect - Cold&quot;</code>, and dates as strings in a format of its own. To get going quickly, you deserialize the response into a class that mirrors it and pass that class around.</p>
<p>Six months later, those names and codes are in your services, your database and your business rules. Then the CRM team renames a status, or you switch CRM vendors, and you're changing code everywhere.</p>
<h2 id="translate-at-the-edge"><a class="anchor" href="#translate-at-the-edge" aria-hidden="true">#</a>Translate at the edge</h2>
<p>An <strong>anti-corruption layer</strong> (a term from Domain-Driven Design) is a thin piece of code at the boundary that translates between an external system's model and yours. Nothing outside it knows the external model exists.</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-comment">// The CRM's shape, internal to the integration</span>
<span class="tok-keyword">internal</span> <span class="tok-keyword">record</span> <span class="tok-method">CrmAccountDto</span>(<span class="tok-keyword">string</span> <span class="tok-type">Id</span>, <span class="tok-keyword">string</span> <span class="tok-type">Name</span>, <span class="tok-keyword">string</span> <span class="tok-type">Acct_Status__c</span>, <span class="tok-keyword">string</span>? <span class="tok-type">Created_Date</span>);

<span class="tok-comment">// Your shape, used everywhere else</span>
<span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">Customer</span>(<span class="tok-type">CustomerId</span> <span class="tok-type">Id</span>, <span class="tok-keyword">string</span> <span class="tok-type">Name</span>, <span class="tok-type">CustomerStatus</span> <span class="tok-type">Status</span>, <span class="tok-type">DateOnly</span>? <span class="tok-type">Since</span>);

<span class="tok-keyword">public</span> <span class="tok-keyword">enum</span> <span class="tok-type">CustomerStatus</span> { <span class="tok-type">Prospect</span>, <span class="tok-type">Active</span>, <span class="tok-type">Suspended</span>, <span class="tok-type">Closed</span> }</code></pre>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">CrmCustomerSource</span>(<span class="tok-type">CrmClient</span> crm) : <span class="tok-type">ICustomerSource</span>
{
    <span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">Customer</span>?&gt; <span class="tok-method">GetAsync</span>(<span class="tok-type">CustomerId</span> id, <span class="tok-type">CancellationToken</span> ct)
    {
        <span class="tok-keyword">var</span> dto = <span class="tok-keyword">await</span> crm.<span class="tok-method">GetAccountAsync</span>(id.Value, ct);
        <span class="tok-keyword">return</span> dto <span class="tok-keyword">is</span> <span class="tok-keyword">null</span> ? <span class="tok-keyword">null</span> : <span class="tok-method">ToCustomer</span>(dto);
    }

    <span class="tok-keyword">internal</span> <span class="tok-keyword">static</span> <span class="tok-type">Customer</span> <span class="tok-method">ToCustomer</span>(<span class="tok-type">CrmAccountDto</span> dto) =&gt; <span class="tok-keyword">new</span>(
        <span class="tok-keyword">new</span> <span class="tok-type">CustomerId</span>(dto.Id),
        dto.Name.<span class="tok-method">Trim</span>(),
        dto.Acct_Status__c <span class="tok-keyword">switch</span>
        {
            <span class="tok-string">"Prospect - Cold"</span> <span class="tok-keyword">or</span> <span class="tok-string">"Prospect - Warm"</span> =&gt; <span class="tok-type">CustomerStatus</span>.Prospect,
            <span class="tok-string">"Customer"</span> =&gt; <span class="tok-type">CustomerStatus</span>.Active,
            <span class="tok-string">"On Hold"</span> =&gt; <span class="tok-type">CustomerStatus</span>.Suspended,
            <span class="tok-string">"Churned"</span> =&gt; <span class="tok-type">CustomerStatus</span>.Closed,
            <span class="tok-keyword">var</span> unknown =&gt; <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-type">UnknownCrmStatusException</span>(unknown)
        },
        <span class="tok-type">DateOnly</span>.<span class="tok-method">TryParse</span>(dto.Created_Date, <span class="tok-keyword">out</span> <span class="tok-keyword">var</span> date) ? date : <span class="tok-keyword">null</span>);
}</code></pre>
<p>The rest of your code depends on <code>ICustomerSource</code> and <code>Customer</code>. It never sees <code>Acct_Status__c</code>.</p>
<h2 id="what-the-layer-is-responsible-for"><a class="anchor" href="#what-the-layer-is-responsible-for" aria-hidden="true">#</a>What the layer is responsible for</h2>
<ul><li><strong>Names and shapes.</strong> Map external fields to your own names and types.</li><li><strong>Codes and statuses.</strong> Collapse their status values into yours, and decide explicitly what happens with values you don't recognize. Failing loudly is usually better than silently guessing.</li><li><strong>Formats.</strong> Parse dates, numbers and currencies once, here.</li><li><strong>Quirks.</strong> Trim whitespace, treat empty strings as missing, handle the field that's sometimes a string and sometimes a number.</li></ul>
<h2 id="test-the-mapping"><a class="anchor" href="#test-the-mapping" aria-hidden="true">#</a>Test the mapping</h2>
<p>The translation is pure code, which makes it easy to test thoroughly. Keep a few real (anonymized) API responses as test fixtures and assert on the domain objects they produce. When the external API changes, these tests tell you exactly what broke.</p>
<h2 id="when-its-worth-it"><a class="anchor" href="#when-its-worth-it" aria-hidden="true">#</a>When it's worth it</h2>
<p>Not every integration needs a formal layer. If an external API is simple, stable and its model already matches yours, a small DTO and a mapping method may be all you need. The more the external model differs from yours, and the more likely it is to change or be replaced, the more the boundary pays off.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Don't let external systems' models leak into your code. Translate their data into your own types at the boundary, handle unknown values deliberately, and test the mapping with real responses. When they change, only the boundary changes.</p>]]></content:encoded>
  </item>
  <item>
    <title>Test time-dependent code with TimeProvider</title>
    <link>https://www.cdsmith.dev/blog/2026-06-21-testing-time-with-timeprovider/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-06-21-testing-time-with-timeprovider/</guid>
    <pubDate>Sun, 21 Jun 2026 12:00:00 GMT</pubDate>
    <category>.NET</category>
    <category>Testing</category>
    <category>C#</category>
    <description>Code that calls `DateTime.UtcNow` directly is hard to test. Since .NET 8, `TimeProvider` gives you a standard abstraction, and a fake one that you can move forward on demand.</description>
    <content:encoded><![CDATA[<p>A lot of business logic depends on the current time: tokens expire, invoices become overdue, retries wait, subscriptions renew. If that code calls <code>DateTime.UtcNow</code> directly, testing it means either waiting in real time or living with tests that pass or fail depending on when they run.</p>
<p>For years every codebase invented its own <code>IClock</code> or <code>ISystemClock</code>. Since .NET 8 there's a standard one: <strong><code>TimeProvider</code></strong>.</p>
<h2 id="use-it-in-your-code"><a class="anchor" href="#use-it-in-your-code" aria-hidden="true">#</a>Use it in your code</h2>
<p>Inject <code>TimeProvider</code> and ask it for the time:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">InvoiceService</span>(<span class="tok-type">TimeProvider</span> time)
{
    <span class="tok-keyword">public</span> <span class="tok-keyword">bool</span> <span class="tok-method">IsOverdue</span>(<span class="tok-type">Invoice</span> invoice) =&gt;
        time.<span class="tok-method">GetUtcNow</span>() &gt; invoice.DueDate.<span class="tok-method">AddDays</span>(<span class="tok-number">30</span>);
}</code></pre>
<p>Register the real one once:</p>
<pre data-lang="csharp"><code class="language-csharp">builder.Services.<span class="tok-method">AddSingleton</span>(<span class="tok-type">TimeProvider</span>.System);</code></pre>
<h2 id="use-the-fake-in-tests"><a class="anchor" href="#use-the-fake-in-tests" aria-hidden="true">#</a>Use the fake in tests</h2>
<p>The <code>Microsoft.Extensions.TimeProvider.Testing</code> package includes <code>FakeTimeProvider</code>, which you control:</p>
<pre data-lang="csharp"><code class="language-csharp">[<span class="tok-type">Fact</span>]
<span class="tok-keyword">public</span> <span class="tok-keyword">void</span> <span class="tok-method">Invoice_becomes_overdue_after_30_days</span>()
{
    <span class="tok-keyword">var</span> time = <span class="tok-keyword">new</span> <span class="tok-type">FakeTimeProvider</span>(<span class="tok-keyword">new</span> <span class="tok-type">DateTimeOffset</span>(<span class="tok-number">2026</span>, <span class="tok-number">6</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>, <span class="tok-type">TimeSpan</span>.Zero));
    <span class="tok-keyword">var</span> service = <span class="tok-keyword">new</span> <span class="tok-type">InvoiceService</span>(time);
    <span class="tok-keyword">var</span> invoice = <span class="tok-keyword">new</span> <span class="tok-type">Invoice</span> { <span class="tok-type">DueDate</span> = <span class="tok-keyword">new</span> <span class="tok-type">DateTimeOffset</span>(<span class="tok-number">2026</span>, <span class="tok-number">6</span>, <span class="tok-number">1</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>, <span class="tok-number">0</span>, <span class="tok-type">TimeSpan</span>.Zero) };

    time.<span class="tok-method">Advance</span>(<span class="tok-type">TimeSpan</span>.<span class="tok-method">FromDays</span>(<span class="tok-number">30</span>));
    <span class="tok-type">Assert</span>.<span class="tok-method">False</span>(service.<span class="tok-method">IsOverdue</span>(invoice));

    time.<span class="tok-method">Advance</span>(<span class="tok-type">TimeSpan</span>.<span class="tok-method">FromSeconds</span>(<span class="tok-number">1</span>));
    <span class="tok-type">Assert</span>.<span class="tok-method">True</span>(service.<span class="tok-method">IsOverdue</span>(invoice));
}</code></pre>
<p>The test runs in microseconds and gives the same result every time.</p>
<h2 id="it-covers-delays-and-timers-too"><a class="anchor" href="#it-covers-delays-and-timers-too" aria-hidden="true">#</a>It covers delays and timers too</h2>
<p><code>TimeProvider</code> isn't just &quot;what time is it&quot;. Timers and delays can use it as well:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">await</span> <span class="tok-type">Task</span>.<span class="tok-method">Delay</span>(<span class="tok-type">TimeSpan</span>.<span class="tok-method">FromMinutes</span>(<span class="tok-number">5</span>), time, ct);

<span class="tok-keyword">using</span> <span class="tok-keyword">var</span> timer = <span class="tok-keyword">new</span> <span class="tok-type">PeriodicTimer</span>(<span class="tok-type">TimeSpan</span>.<span class="tok-method">FromMinutes</span>(<span class="tok-number">1</span>), time);</code></pre>
<p>With <code>FakeTimeProvider</code>, those waits complete when you call <code>Advance</code>, so you can test retry delays, polling loops and timeouts without actually waiting.</p>
<h2 id="watch-for-the-hidden-calls"><a class="anchor" href="#watch-for-the-hidden-calls" aria-hidden="true">#</a>Watch for the hidden calls</h2>
<p>Switching to <code>TimeProvider</code> only helps if you switch everywhere. Search the code for <code>DateTime.Now</code>, <code>DateTime.UtcNow</code>, <code>DateTimeOffset.UtcNow</code> and plain <code>Task.Delay</code> calls. A banned-API analyzer rule (<code>Microsoft.CodeAnalysis.BannedApiAnalyzers</code>) can stop new ones from creeping back in.</p>
<p>While you're there: prefer <code>DateTimeOffset</code> and UTC for anything stored or compared. <code>DateTime.Now</code> depends on the server's time zone, which is a different source of bugs.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Inject <code>TimeProvider</code> instead of reading the clock directly, register <code>TimeProvider.System</code> in production, and use <code>FakeTimeProvider</code> in tests to move time forward on demand. Time-dependent logic becomes fast and reliable to test.</p>]]></content:encoded>
  </item>
  <item>
    <title>Zero-downtime deployments with slots</title>
    <link>https://www.cdsmith.dev/blog/2026-06-14-deployment-slots-and-swaps/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-06-14-deployment-slots-and-swaps/</guid>
    <pubDate>Sun, 14 Jun 2026 12:00:00 GMT</pubDate>
    <category>Azure</category>
    <category>DevOps</category>
    <description>Deploying straight to production means a cold start and no quick way back. Deployment slots let you deploy, warm up and check a release first, then swap it live in seconds.</description>
    <content:encoded><![CDATA[<p>Deploying directly to a production App Service has two problems. During the deployment, the app restarts and the first requests hit a cold process. And if the release is broken, rolling back means another deployment, while users see errors.</p>
<p><strong>Deployment slots</strong> fix both. A slot is a separate, live copy of your app, such as <code>staging</code>, with its own hostname, running on the same App Service plan. They're available on the Standard tier and above, and also for Azure Functions.</p>
<h2 id="the-flow"><a class="anchor" href="#the-flow" aria-hidden="true">#</a>The flow</h2>
<ol><li><strong>Deploy to the staging slot.</strong> Production keeps serving traffic untouched.</li><li><strong>Warm it up and check it.</strong> Hit the staging URL, run smoke tests, check the health endpoint.</li><li><strong>Swap.</strong> App Service warms up the staging instances, then switches the routing so staging becomes production. Users don't see a restart.</li><li><strong>If something's wrong, swap back.</strong> The previous version is still sitting in the other slot, already warm. Rollback takes seconds.</li></ol>
<h2 id="in-azure-pipelines"><a class="anchor" href="#in-azure-pipelines" aria-hidden="true">#</a>In Azure Pipelines</h2>
<pre data-lang="yaml"><code class="language-yaml">- task: AzureWebApp@1
  inputs:
    azureSubscription: 'my-service-connection'
    appType: 'webApp'
    appName: 'orders-api'
    deployToSlotOrASE: true
    resourceGroupName: 'orders-rg'
    slotName: 'staging'
    package: '$(Pipeline.Workspace)/drop/*.zip'

- script: curl --fail --retry 5 --retry-delay 10 https://orders-api-staging.azurewebsites.net/health/ready
  displayName: Smoke test staging

- task: AzureAppServiceManage@0
  inputs:
    azureSubscription: 'my-service-connection'
    Action: 'Swap Slots'
    WebAppName: 'orders-api'
    ResourceGroupName: 'orders-rg'
    SourceSlot: 'staging'</code></pre>
<h2 id="sticky-settings"><a class="anchor" href="#sticky-settings" aria-hidden="true">#</a>Sticky settings</h2>
<p>When slots swap, their app settings swap too, by default. That's usually what you want for most settings, but not for everything. Some must stay with the slot, such as a connection string pointing staging at a test database, or a flag that turns off scheduled jobs in staging.</p>
<p>Mark those as <strong>deployment slot settings</strong> (the checkbox in <strong>Configuration</strong>, or <code>slotSetting: true</code> in infrastructure code). They stay put while the code moves.</p>
<p>Check this carefully before your first swap. A staging connection string that follows the code into production is a very bad afternoon.</p>
<h2 id="warm-up"><a class="anchor" href="#warm-up" aria-hidden="true">#</a>Warm-up</h2>
<p>App Service sends requests to the staging instances before swapping, so the first real user doesn't pay the startup cost. You can point the warm-up at a specific path, such as a health endpoint, with the <code>WEBSITE_SWAP_WARMUP_PING_PATH</code> app setting, and set <code>WEBSITE_SWAP_WARMUP_PING_STATUSES</code> to the status codes that count as ready.</p>
<h2 id="things-to-watch"><a class="anchor" href="#things-to-watch" aria-hidden="true">#</a>Things to watch</h2>
<ul><li><strong>Background work runs in both slots.</strong> Timer-triggered functions and hosted services in staging run too, unless you turn them off with a sticky setting.</li><li><strong>Database migrations aren't swapped.</strong> Schema changes apply to the shared database before the swap, so they must work with both the old and new code.</li><li><strong>Slots share the plan's resources.</strong> A heavy smoke test in staging competes with production for CPU and memory.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Deploy to a staging slot, verify it, then swap. Mark environment-specific settings as slot settings, keep migrations backward-compatible, and keep the previous version in the other slot as your instant rollback.</p>]]></content:encoded>
  </item>
  <item>
    <title>Domain events and integration events aren&apos;t the same thing</title>
    <link>https://www.cdsmith.dev/blog/2026-06-07-domain-events-vs-integration-events/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-06-07-domain-events-vs-integration-events/</guid>
    <pubDate>Sun, 07 Jun 2026 12:00:00 GMT</pubDate>
    <category>Architecture</category>
    <category>DDD</category>
    <category>Integrations</category>
    <description>Both describe something that happened, but one stays inside your service and the other is a public contract. Mixing them up couples services you meant to keep apart.</description>
    <content:encoded><![CDATA[<p>&quot;Event&quot; gets used for two quite different things, and treating them as one causes trouble later.</p>
<h2 id="domain-events-inside-the-boundary"><a class="anchor" href="#domain-events-inside-the-boundary" aria-hidden="true">#</a>Domain events: inside the boundary</h2>
<p>A <strong>domain event</strong> records something that happened in your domain model, for other parts of the <strong>same</strong> service or module to react to:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">OrderShipped</span>(<span class="tok-type">Guid</span> <span class="tok-type">OrderId</span>, <span class="tok-type">DateTimeOffset</span> <span class="tok-type">ShippedAt</span>) : <span class="tok-type">IDomainEvent</span>;

<span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-type">Order</span>
{
    <span class="tok-keyword">private</span> <span class="tok-keyword">readonly</span> <span class="tok-type">List</span>&lt;<span class="tok-type">IDomainEvent</span>&gt; _events = [];
    <span class="tok-keyword">public</span> <span class="tok-type">IReadOnlyList</span>&lt;<span class="tok-type">IDomainEvent</span>&gt; <span class="tok-type">Events</span> =&gt; _events;

    <span class="tok-keyword">public</span> <span class="tok-keyword">void</span> <span class="tok-method">Ship</span>(<span class="tok-type">DateTimeOffset</span> now)
    {
        <span class="tok-keyword">if</span> (<span class="tok-type">Status</span> != <span class="tok-type">OrderStatus</span>.Paid)
            <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-type">InvalidOperationException</span>(<span class="tok-string">"Only paid orders can be shipped."</span>);

        <span class="tok-type">Status</span> = <span class="tok-type">OrderStatus</span>.Shipped;
        _events.<span class="tok-method">Add</span>(<span class="tok-keyword">new</span> <span class="tok-type">OrderShipped</span>(<span class="tok-type">Id</span>, now));
    }
}</code></pre>
<p>Handlers run in-process, often just before or after <code>SaveChanges</code>, for example from an EF Core interceptor that dispatches each entity's events. They can update another aggregate, write an audit entry or schedule work. They're free to use internal types, because no one outside the service sees them, and you can change them whenever you like.</p>
<h2 id="integration-events-across-boundaries"><a class="anchor" href="#integration-events-across-boundaries" aria-hidden="true">#</a>Integration events: across boundaries</h2>
<p>An <strong>integration event</strong> tells <strong>other</strong> services something happened. It travels through a message broker such as Service Bus, and it's a published contract:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-comment">// In a shared contracts package</span>
<span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">OrderShippedV1</span>(<span class="tok-type">Guid</span> <span class="tok-type">OrderId</span>, <span class="tok-keyword">string</span> <span class="tok-type">CustomerId</span>, <span class="tok-keyword">string</span> <span class="tok-type">Carrier</span>, <span class="tok-type">DateTimeOffset</span> <span class="tok-type">ShippedAt</span>);</code></pre>
<p>Different rules apply:</p>
<ul><li><strong>It's a public API.</strong> Other teams depend on its shape. Changing it needs versioning and a migration plan.</li><li><strong>It contains only what consumers need,</strong> using simple types, not your internal entities or value objects.</li><li><strong>It must be published reliably,</strong> typically through an outbox, so it's never lost if the publish fails after the database commit.</li><li><strong>Consumers receive it at least once,</strong> later, and possibly out of order.</li></ul>
<h2 id="how-they-connect"><a class="anchor" href="#how-they-connect" aria-hidden="true">#</a>How they connect</h2>
<p>The common pattern: a domain event handler translates the domain event into an integration event and writes it to the outbox, in the same transaction.</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">PublishOrderShipped</span>(<span class="tok-type">OrdersDb</span> db) : <span class="tok-type">IDomainEventHandler</span>&lt;<span class="tok-type">OrderShipped</span>&gt;
{
    <span class="tok-keyword">public</span> <span class="tok-type">Task</span> <span class="tok-method">HandleAsync</span>(<span class="tok-type">OrderShipped</span> e, <span class="tok-type">CancellationToken</span> ct)
    {
        <span class="tok-keyword">var</span> order = db.Orders.Local.<span class="tok-method">Single</span>(o =&gt; o.Id == e.OrderId);
        db.Outbox.<span class="tok-method">Add</span>(<span class="tok-type">OutboxMessage</span>.<span class="tok-method">From</span>(
            <span class="tok-keyword">new</span> <span class="tok-type">OrderShippedV1</span>(order.Id, order.CustomerId, order.Carrier, e.ShippedAt)));
        <span class="tok-keyword">return</span> <span class="tok-type">Task</span>.CompletedTask;
    }
}</code></pre>
<p>Not every domain event becomes an integration event. Most stay internal. Publishing externally is a deliberate decision about what other services should know.</p>
<h2 id="the-mistake-to-avoid"><a class="anchor" href="#the-mistake-to-avoid" aria-hidden="true">#</a>The mistake to avoid</h2>
<p>Publishing domain events directly to the broker feels efficient: one event type, no translation. But now other services depend on your domain model's internal structure. Renaming a property in your entity becomes a breaking change for three other teams, and the boundary you designed exists only on paper.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Use domain events to react to changes inside a service, and change them freely. Use integration events, published reliably through an outbox and versioned like an API, to tell other services what happened. Translate between the two on purpose.</p>]]></content:encoded>
  </item>
  <item>
    <title>Route messages with Service Bus topic filters</title>
    <link>https://www.cdsmith.dev/blog/2026-05-31-service-bus-topics-and-subscription-filters/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-05-31-service-bus-topics-and-subscription-filters/</guid>
    <pubDate>Sun, 31 May 2026 12:00:00 GMT</pubDate>
    <category>Azure</category>
    <category>Service Bus</category>
    <category>Integrations</category>
    <description>With a topic, every subscription gets every message by default. Filters let each consumer receive only what it needs, without the publisher knowing who&apos;s listening.</description>
    <content:encoded><![CDATA[<p>A queue delivers each message to one consumer. A <strong>topic</strong> delivers each message to every <strong>subscription</strong>, and each subscription works like its own queue. That's how one <code>OrderPlaced</code> event reaches billing, shipping and analytics independently.</p>
<p>By default, a new subscription receives every message sent to the topic. Often that's not what you want: the EU warehouse only cares about EU orders, and the fraud check only about orders over a certain value. Filtering in each consumer's code works, but it wastes delivery and processing on messages that are thrown away. Subscription filters do it in the broker.</p>
<h2 id="filters-work-on-properties-not-the-body"><a class="anchor" href="#filters-work-on-properties-not-the-body" aria-hidden="true">#</a>Filters work on properties, not the body</h2>
<p>Service Bus never looks inside the message body. Filters evaluate the message's system properties and its <strong>application properties</strong>, so the publisher sets those for anything subscribers might route on:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">var</span> message = <span class="tok-keyword">new</span> <span class="tok-type">ServiceBusMessage</span>(<span class="tok-type">BinaryData</span>.<span class="tok-method">FromObjectAsJson</span>(orderPlaced))
{
    <span class="tok-type">Subject</span> = <span class="tok-string">"OrderPlaced"</span>,
    <span class="tok-type">MessageId</span> = <span class="tok-string">$"order-placed-{orderPlaced.OrderId}"</span>
};
message.ApplicationProperties[<span class="tok-string">"Region"</span>] = orderPlaced.Region;   <span class="tok-comment">// "EU", "US", ...</span>
message.ApplicationProperties[<span class="tok-string">"Total"</span>] = orderPlaced.Total;

<span class="tok-keyword">await</span> sender.<span class="tok-method">SendMessageAsync</span>(message, ct);</code></pre>
<h2 id="two-kinds-of-filter"><a class="anchor" href="#two-kinds-of-filter" aria-hidden="true">#</a>Two kinds of filter</h2>
<p><strong>Correlation filters</strong> match exact values on properties. They're the most efficient and cover most cases:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">await</span> admin.<span class="tok-method">CreateRuleAsync</span>(<span class="tok-string">"orders"</span>, <span class="tok-string">"eu-warehouse"</span>, <span class="tok-keyword">new</span> <span class="tok-type">CreateRuleOptions</span>(<span class="tok-string">"eu-orders"</span>,
    <span class="tok-keyword">new</span> <span class="tok-type">CorrelationRuleFilter</span> { <span class="tok-type">Subject</span> = <span class="tok-string">"OrderPlaced"</span>, <span class="tok-type">ApplicationProperties</span> = { [<span class="tok-string">"Region"</span>] = <span class="tok-string">"EU"</span> } }));</code></pre>
<p><strong>SQL filters</strong> allow conditions with comparisons, <code>AND</code>, <code>OR</code>, <code>IN</code> and more:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">await</span> admin.<span class="tok-method">CreateRuleAsync</span>(<span class="tok-string">"orders"</span>, <span class="tok-string">"fraud-check"</span>, <span class="tok-keyword">new</span> <span class="tok-type">CreateRuleOptions</span>(<span class="tok-string">"large-orders"</span>,
    <span class="tok-keyword">new</span> <span class="tok-type">SqlRuleFilter</span>(<span class="tok-string">"Total &gt; 5000 AND Region IN ('EU', 'US')"</span>)));</code></pre>
<p><code>admin</code> is a <code>ServiceBusAdministrationClient</code>.</p>
<h2 id="remove-the-default-rule"><a class="anchor" href="#remove-the-default-rule" aria-hidden="true">#</a>Remove the default rule</h2>
<p>Every new subscription starts with a rule named <code>$Default</code> that matches everything. A message is delivered if <strong>any</strong> rule matches, so adding your filter without removing the default changes nothing:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">await</span> admin.<span class="tok-method">DeleteRuleAsync</span>(<span class="tok-string">"orders"</span>, <span class="tok-string">"eu-warehouse"</span>, <span class="tok-type">RuleProperties</span>.DefaultRuleName);</code></pre>
<p>Better still, define subscriptions and their rules in your infrastructure code, such as Bicep or Terraform, so they're created correctly from the start and reviewed like any other change.</p>
<h2 id="design-tips"><a class="anchor" href="#design-tips" aria-hidden="true">#</a>Design tips</h2>
<ul><li><strong>Decide routing properties up front</strong> and treat them as part of the message contract, like the body.</li><li><strong>Prefer correlation filters.</strong> Use SQL filters only when you need comparisons or combinations.</li><li><strong>Watch for messages no subscription wants.</strong> If no filter matches, the message is simply dropped. That might be fine, or it might hide a bug.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Use a topic when several consumers need the same events, put routing information in application properties, and give each subscription a filter, removing the <code>$Default</code> rule, so each consumer gets only the messages it actually handles.</p>]]></content:encoded>
  </item>
  <item>
    <title>Do you still need ConfigureAwait(false)?</title>
    <link>https://www.cdsmith.dev/blog/2026-05-24-configureawait-false-in-2026/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-05-24-configureawait-false-in-2026/</guid>
    <pubDate>Sun, 24 May 2026 12:00:00 GMT</pubDate>
    <category>C#</category>
    <category>.NET</category>
    <category>Async</category>
    <description>In ASP.NET Core application code, no. In libraries, yes. Here&apos;s why the answer depends on where your code runs.</description>
    <content:encoded><![CDATA[<p>Older .NET codebases have <code>.ConfigureAwait(false)</code> on almost every <code>await</code>. Newer ones often have none. Both can be right, depending on what the code is.</p>
<h2 id="what-it-actually-does"><a class="anchor" href="#what-it-actually-does" aria-hidden="true">#</a>What it actually does</h2>
<p>When you <code>await</code> a task, the code after the <code>await</code> resumes on the captured <strong>synchronization context</strong>, if there is one. In a WinForms or WPF app, that's the UI thread. In classic ASP.NET on .NET Framework, it's the request context.</p>
<p><code>ConfigureAwait(false)</code> says &quot;I don't need to come back to that context; resume on any thread pool thread.&quot; That avoids the cost of switching back, and it avoids a classic deadlock: code that blocks on an async method with <code>.Result</code> while holding the context the method is waiting to resume on.</p>
<h2 id="aspnet-core-has-no-synchronization-context"><a class="anchor" href="#aspnet-core-has-no-synchronization-context" aria-hidden="true">#</a>ASP.NET Core has no synchronization context</h2>
<p>ASP.NET Core doesn't install a <code>SynchronizationContext</code>. Continuations already run on the thread pool, so <code>ConfigureAwait(false)</code> changes nothing there. The same is true for console apps, worker services and Azure Functions.</p>
<p>So in <strong>application code</strong> that only runs in those hosts, such as your controllers, handlers and services, you can leave it out. It's noise.</p>
<h2 id="libraries-still-need-it"><a class="anchor" href="#libraries-still-need-it" aria-hidden="true">#</a>Libraries still need it</h2>
<p>If you write a <strong>library</strong>, such as a NuGet package or a shared client used across teams, you don't know where it will run. Someone might call it from a WPF app, or from an old ASP.NET application that blocks on <code>.Result</code>. Using <code>ConfigureAwait(false)</code> on every <code>await</code> in the library protects those callers from deadlocks and avoids needless context switches.</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">Customer</span>?&gt; <span class="tok-method">GetCustomerAsync</span>(<span class="tok-keyword">string</span> id, <span class="tok-type">CancellationToken</span> ct = <span class="tok-keyword">default</span>)
{
    <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> response = <span class="tok-keyword">await</span> _http.<span class="tok-method">GetAsync</span>(<span class="tok-string">$"customers/{id}"</span>, ct).<span class="tok-method">ConfigureAwait</span>(<span class="tok-keyword">false</span>);
    <span class="tok-keyword">if</span> (response.StatusCode == <span class="tok-type">HttpStatusCode</span>.NotFound) <span class="tok-keyword">return</span> <span class="tok-keyword">null</span>;

    response.<span class="tok-method">EnsureSuccessStatusCode</span>();
    <span class="tok-keyword">return</span> <span class="tok-keyword">await</span> response.Content.<span class="tok-method">ReadFromJsonAsync</span>&lt;<span class="tok-type">Customer</span>&gt;(ct).<span class="tok-method">ConfigureAwait</span>(<span class="tok-keyword">false</span>);
}</code></pre>
<p>The analyzer rule <strong>CA2007</strong> flags missing <code>ConfigureAwait</code> calls. Turn it on in library projects only.</p>
<h2 id="the-real-fix-for-deadlocks"><a class="anchor" href="#the-real-fix-for-deadlocks" aria-hidden="true">#</a>The real fix for deadlocks</h2>
<p><code>ConfigureAwait(false)</code> in a library protects callers who block on async code. The better fix is not to block at all: async all the way up, no <code>.Result</code> and no <code>.Wait()</code>. In ASP.NET Core, blocking doesn't deadlock, but it still ties up thread pool threads and hurts throughput under load.</p>
<h2 id="a-newer-option"><a class="anchor" href="#a-newer-option" aria-hidden="true">#</a>A newer option</h2>
<p>Since .NET 8, <code>Task.ConfigureAwait</code> also accepts <code>ConfigureAwaitOptions</code>. For example, <code>ConfigureAwaitOptions.SuppressThrowing</code> lets you await a task purely to wait for it to finish, without rethrowing its exception. That's handy when shutting down background work.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>In ASP.NET Core, worker and Functions application code, skip <code>ConfigureAwait(false)</code>. In reusable libraries, keep using it on every <code>await</code>. Either way, don't block on async code with <code>.Result</code> or <code>.Wait()</code>.</p>]]></content:encoded>
  </item>
  <item>
    <title>Share blobs with user delegation SAS tokens</title>
    <link>https://www.cdsmith.dev/blog/2026-05-17-user-delegation-sas-for-blob-storage/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-05-17-user-delegation-sas-for-blob-storage/</guid>
    <pubDate>Sun, 17 May 2026 12:00:00 GMT</pubDate>
    <category>Azure</category>
    <category>Security</category>
    <category>.NET</category>
    <description>A SAS token signed with the storage account key can&apos;t be revoked without rotating the key. A user delegation SAS is signed with Entra ID credentials, expires sooner and fits with managed identity.</description>
    <content:encoded><![CDATA[<p>Sooner or later an app needs to let someone download a file from Blob Storage without making the container public: an invoice PDF, an export, an uploaded document. The standard answer is a <strong>shared access signature</strong> (SAS): a URL with a signed token that grants limited access for a limited time.</p>
<p>How that token is signed matters more than most people realize.</p>
<h2 id="two-kinds-of-sas"><a class="anchor" href="#two-kinds-of-sas" aria-hidden="true">#</a>Two kinds of SAS</h2>
<ul><li><strong>Account key SAS (service SAS).</strong> Signed with the storage account's access key. To revoke a leaked token early, you have to rotate the key, which breaks everything else using it. It also means your app needs the key in the first place.</li><li><strong>User delegation SAS.</strong> Signed with a <strong>user delegation key</strong> that your app obtains with its Entra ID identity, such as a managed identity. No account key involved, the delegation key is valid for at most seven days, and access is logged against the identity that created it.</li></ul>
<p>If your app already uses managed identity, user delegation SAS is the natural choice. You can then turn off shared key access on the storage account entirely.</p>
<h2 id="creating-one"><a class="anchor" href="#creating-one" aria-hidden="true">#</a>Creating one</h2>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">var</span> service = <span class="tok-keyword">new</span> <span class="tok-type">BlobServiceClient</span>(
    <span class="tok-keyword">new</span> <span class="tok-type">Uri</span>(<span class="tok-string">"https://mystorage.blob.core.windows.net"</span>), <span class="tok-keyword">new</span> <span class="tok-type">DefaultAzureCredential</span>());

<span class="tok-keyword">var</span> now = <span class="tok-type">DateTimeOffset</span>.UtcNow;
<span class="tok-type">UserDelegationKey</span> key = <span class="tok-keyword">await</span> service.<span class="tok-method">GetUserDelegationKeyAsync</span>(
    startsOn: now.<span class="tok-method">AddMinutes</span>(-<span class="tok-number">5</span>), expiresOn: now.<span class="tok-method">AddHours</span>(<span class="tok-number">1</span>));

<span class="tok-keyword">var</span> sas = <span class="tok-keyword">new</span> <span class="tok-type">BlobSasBuilder</span>
{
    <span class="tok-type">BlobContainerName</span> = <span class="tok-string">"invoices"</span>,
    <span class="tok-type">BlobName</span> = <span class="tok-string">"2026/05/INV-1042.pdf"</span>,
    <span class="tok-type">Resource</span> = <span class="tok-string">"b"</span>,                          <span class="tok-comment">// a single blob</span>
    <span class="tok-type">StartsOn</span> = now.<span class="tok-method">AddMinutes</span>(-<span class="tok-number">5</span>),           <span class="tok-comment">// allow for clock differences</span>
    <span class="tok-type">ExpiresOn</span> = now.<span class="tok-method">AddMinutes</span>(<span class="tok-number">15</span>),
    <span class="tok-type">Protocol</span> = <span class="tok-type">SasProtocol</span>.Https
};
sas.<span class="tok-method">SetPermissions</span>(<span class="tok-type">BlobSasPermissions</span>.Read);

<span class="tok-keyword">var</span> uri = <span class="tok-keyword">new</span> <span class="tok-type">BlobUriBuilder</span>(service.Uri)
{
    <span class="tok-type">BlobContainerName</span> = sas.BlobContainerName,
    <span class="tok-type">BlobName</span> = sas.BlobName,
    <span class="tok-type">Sas</span> = sas.<span class="tok-method">ToSasQueryParameters</span>(key, service.AccountName)
}.<span class="tok-method">ToUri</span>();</code></pre>
<p>The app's identity needs permission to request delegation keys. The <strong>Storage Blob Data Contributor</strong> role includes it; the narrower <strong>Storage Blob Delegator</strong> role grants just that permission.</p>
<p>You can cache the user delegation key and reuse it for many SAS tokens until it expires, rather than requesting a new one for every download.</p>
<h2 id="keep-tokens-tight"><a class="anchor" href="#keep-tokens-tight" aria-hidden="true">#</a>Keep tokens tight</h2>
<ul><li><strong>Short expiry.</strong> Minutes for a download link, not days. The client can always ask for a new one.</li><li><strong>One blob, minimum permissions.</strong> <code>Read</code> on a single blob, not <code>Read</code> and <code>List</code> on the container.</li><li><strong>HTTPS only.</strong></li><li><strong>Start slightly in the past,</strong> a few minutes, so small clock differences don't make a fresh token look not yet valid.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>When you hand out blob access, use a user delegation SAS signed with your app's managed identity instead of the account key. Scope it to one blob, give it read-only access and a short lifetime, and turn off shared key access on the account once nothing depends on it.</p>]]></content:encoded>
  </item>
  <item>
    <title>Integration tests against a real database</title>
    <link>https://www.cdsmith.dev/blog/2026-05-10-integration-tests-with-testcontainers/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-05-10-integration-tests-with-testcontainers/</guid>
    <pubDate>Sun, 10 May 2026 12:00:00 GMT</pubDate>
    <category>.NET</category>
    <category>Testing</category>
    <category>ASP.NET Core</category>
    <description>The EF Core in-memory provider doesn&apos;t behave like SQL Server, so tests pass and production fails. WebApplicationFactory plus Testcontainers lets you test the real thing.</description>
    <content:encoded><![CDATA[<p>Unit tests with mocked repositories are fast, but they mostly test that your mocks return what you told them to. The bugs that reach production are usually at the edges: a query EF Core can't translate, a unique index you forgot, a migration that fails, JSON that doesn't bind.</p>
<p>The EF Core in-memory provider doesn't catch those either. It isn't a relational database: no constraints, no transactions, no SQL translation. Microsoft's own guidance discourages using it for testing.</p>
<p>The better option is to run tests against a real database in a container.</p>
<h2 id="the-pieces"><a class="anchor" href="#the-pieces" aria-hidden="true">#</a>The pieces</h2>
<ul><li><strong><code>WebApplicationFactory&lt;Program&gt;</code></strong> (from <code>Microsoft.AspNetCore.Mvc.Testing</code>) starts your real API in memory, with its real startup, middleware and dependency injection, and gives you an <code>HttpClient</code> to call it.</li><li><strong>Testcontainers</strong> (the <code>Testcontainers.MsSql</code> package, with others for PostgreSQL, MongoDB and more) starts a disposable database container for the test run and removes it afterwards.</li></ul>
<h2 id="setting-it-up-with-xunit"><a class="anchor" href="#setting-it-up-with-xunit" aria-hidden="true">#</a>Setting it up with xUnit</h2>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-type">ApiFactory</span> : <span class="tok-type">WebApplicationFactory</span>&lt;<span class="tok-type">Program</span>&gt;, <span class="tok-type">IAsyncLifetime</span>
{
    <span class="tok-keyword">private</span> <span class="tok-keyword">readonly</span> <span class="tok-type">MsSqlContainer</span> _sql = <span class="tok-keyword">new</span> <span class="tok-type">MsSqlBuilder</span>()
        .<span class="tok-method">WithImage</span>(<span class="tok-string">"mcr.microsoft.com/mssql/server:2022-latest"</span>)
        .<span class="tok-method">Build</span>();

    <span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span> <span class="tok-method">InitializeAsync</span>()
    {
        <span class="tok-keyword">await</span> _sql.<span class="tok-method">StartAsync</span>();

        <span class="tok-keyword">using</span> <span class="tok-keyword">var</span> scope = <span class="tok-type">Services</span>.<span class="tok-method">CreateScope</span>();
        <span class="tok-keyword">var</span> db = scope.ServiceProvider.<span class="tok-method">GetRequiredService</span>&lt;<span class="tok-type">OrdersDb</span>&gt;();
        <span class="tok-keyword">await</span> db.Database.<span class="tok-method">MigrateAsync</span>();
    }

    <span class="tok-keyword">protected</span> <span class="tok-keyword">override</span> <span class="tok-keyword">void</span> <span class="tok-method">ConfigureWebHost</span>(<span class="tok-type">IWebHostBuilder</span> builder)
    {
        builder.<span class="tok-method">ConfigureTestServices</span>(services =&gt;
        {
            services.<span class="tok-method">RemoveAll</span>&lt;<span class="tok-method">DbContextOptions</span>&lt;<span class="tok-type">OrdersDb</span>&gt;&gt;();
            services.<span class="tok-method">AddDbContext</span>&lt;<span class="tok-type">OrdersDb</span>&gt;(o =&gt; o.<span class="tok-method">UseSqlServer</span>(_sql.<span class="tok-method">GetConnectionString</span>()));
        });
    }

    <span class="tok-keyword">public</span> <span class="tok-keyword">new</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span> <span class="tok-method">DisposeAsync</span>() =&gt; <span class="tok-keyword">await</span> _sql.<span class="tok-method">DisposeAsync</span>();
}</code></pre>
<p>A test then calls the API like a real client would:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">PlaceOrderTests</span>(<span class="tok-type">ApiFactory</span> factory) : <span class="tok-type">IClassFixture</span>&lt;<span class="tok-type">ApiFactory</span>&gt;
{
    [<span class="tok-type">Fact</span>]
    <span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span> <span class="tok-method">Placing_an_order_returns_its_location</span>()
    {
        <span class="tok-keyword">var</span> client = factory.<span class="tok-method">CreateClient</span>();

        <span class="tok-keyword">var</span> response = <span class="tok-keyword">await</span> client.<span class="tok-method">PostAsJsonAsync</span>(<span class="tok-string">"/orders"</span>, <span class="tok-keyword">new</span>
        {
            customerId = <span class="tok-type">Guid</span>.<span class="tok-method">NewGuid</span>(),
            items = <span class="tok-keyword">new</span>[] { <span class="tok-keyword">new</span> { sku = <span class="tok-string">"ABC-1"</span>, quantity = <span class="tok-number">2</span> } }
        });

        <span class="tok-type">Assert</span>.<span class="tok-method">Equal</span>(<span class="tok-type">HttpStatusCode</span>.Created, response.StatusCode);
        <span class="tok-type">Assert</span>.<span class="tok-method">NotNull</span>(response.Headers.Location);
    }
}</code></pre>
<p>With top-level statements, add <code>public partial class Program { }</code> at the end of <code>Program.cs</code> so the test project can reference it.</p>
<h2 id="keeping-it-fast"><a class="anchor" href="#keeping-it-fast" aria-hidden="true">#</a>Keeping it fast</h2>
<ul><li><strong>Share one container</strong> per test class or the whole test run with fixtures, rather than one per test. Starting SQL Server takes seconds; running a test takes milliseconds.</li><li><strong>Run migrations, not <code>EnsureCreated</code>,</strong> so the tests also prove your migrations work.</li><li><strong>Isolate data between tests,</strong> by using unique IDs per test or resetting tables between tests. The Respawn library can wipe data quickly while keeping the schema.</li></ul>
<h2 id="replace-only-whats-outside-your-control"><a class="anchor" href="#replace-only-whats-outside-your-control" aria-hidden="true">#</a>Replace only what's outside your control</h2>
<p>Use real infrastructure you own, like the database. Replace third-party APIs with fakes in <code>ConfigureTestServices</code>, or with a stub HTTP server such as WireMock.Net, so tests don't depend on someone else's sandbox being up.</p>
<h2 id="in-the-pipeline"><a class="anchor" href="#in-the-pipeline" aria-hidden="true">#</a>In the pipeline</h2>
<p>Testcontainers needs Docker. Microsoft-hosted Linux agents in Azure Pipelines, such as <code>ubuntu-latest</code>, have it available, so the same tests run in CI with no extra setup.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Test your API the way it runs in production: start it with <code>WebApplicationFactory</code>, point it at a real database in a container, and fake only the services you don't own. These tests catch the bugs mocks and in-memory providers miss.</p>]]></content:encoded>
  </item>
  <item>
    <title>Add idempotency keys to your POST endpoints</title>
    <link>https://www.cdsmith.dev/blog/2026-05-03-idempotency-keys-for-your-api/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-05-03-idempotency-keys-for-your-api/</guid>
    <pubDate>Sun, 03 May 2026 12:00:00 GMT</pubDate>
    <category>ASP.NET Core</category>
    <category>Integrations</category>
    <category>API Design</category>
    <description>When a client times out and retries a POST, did the first one work? An idempotency key lets your API recognize the retry and return the original result instead of doing the work twice.</description>
    <content:encoded><![CDATA[<p>A partner calls your API to create a payment. Their request reaches you, the payment is created, and then their connection drops before your response arrives. From their side, the request failed. They retry, and now there are two payments.</p>
<p>They can't know whether the first request worked, so they can't safely decide whether to retry. The fix has to come from your API.</p>
<h2 id="the-idea"><a class="anchor" href="#the-idea" aria-hidden="true">#</a>The idea</h2>
<p>The client generates a unique key, usually a GUID, for each <strong>logical operation</strong> and sends it in a header:</p>
<pre><code>POST /payments
Idempotency-Key: 6f1c2f4e-0d5b-4a8e-9a77-3e3c0f5d8b21
Content-Type: application/json</code></pre>
<p>The first time your API sees that key, it does the work and saves the response alongside the key. If the same key arrives again, it skips the work and returns the saved response. Retries become safe, however many there are.</p>
<p>Payment providers have worked this way for years, and there's an IETF draft standardizing the <code>Idempotency-Key</code> header.</p>
<h2 id="store-keys-in-your-database"><a class="anchor" href="#store-keys-in-your-database" aria-hidden="true">#</a>Store keys in your database</h2>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-type">IdempotencyRecord</span>
{
    <span class="tok-keyword">public</span> <span class="tok-keyword">required</span> <span class="tok-keyword">string</span> <span class="tok-type">Key</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">init</span>; }          <span class="tok-comment">// primary key</span>
    <span class="tok-keyword">public</span> <span class="tok-keyword">required</span> <span class="tok-keyword">string</span> <span class="tok-type">RequestHash</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">init</span>; }
    <span class="tok-keyword">public</span> <span class="tok-keyword">int</span>? <span class="tok-type">StatusCode</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; }
    <span class="tok-keyword">public</span> <span class="tok-keyword">string</span>? <span class="tok-type">ResponseBody</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">set</span>; }
    <span class="tok-keyword">public</span> <span class="tok-type">DateTimeOffset</span> <span class="tok-type">CreatedAt</span> { <span class="tok-keyword">get</span>; <span class="tok-keyword">init</span>; }
}</code></pre>
<p>The flow for each request:</p>
<ol><li><strong>No key?</strong> Reject with <code>400</code>, or process normally if keys are optional for this endpoint.</li><li><strong>Try to insert a record</strong> with the key and a hash of the request body. The primary key on <code>Key</code> makes this atomic: if two identical retries arrive at the same moment, only one insert succeeds.</li><li><strong>Insert succeeded:</strong> do the work, then store the status code and response body on the record.</li><li><strong>Insert failed because the key exists:</strong> - <strong>Response saved:</strong> return it as-is. - <strong>No response yet:</strong> the original request is still running. Return <code>409 Conflict</code> so the client tries again shortly. - <strong>Different request hash:</strong> the client reused a key for a different request. Return <code>422 Unprocessable Content</code>.</li></ol>
<p>Putting the record insert and the business work in the same database transaction keeps them consistent: if the work fails and rolls back, the key isn't left behind as &quot;in progress&quot;.</p>
<h2 id="practical-details"><a class="anchor" href="#practical-details" aria-hidden="true">#</a>Practical details</h2>
<ul><li><strong>Scope keys per client,</strong> for example by combining them with the caller's ID, so two clients can't collide.</li><li><strong>Expire old keys</strong> after a day or so. Retries happen within seconds or minutes, not weeks.</li><li><strong>Only cache meaningful outcomes.</strong> If the request failed with a <code>500</code>, don't store it; let the retry try again.</li><li><strong>Document it.</strong> Tell clients to generate one key per operation and reuse it only for retries of that operation.</li></ul>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Any <code>POST</code> that creates something, especially money movements or orders, should accept an idempotency key. Claim the key atomically, store the response with it, and return the saved response on retries. Your clients can then retry safely, and duplicates stop being your problem.</p>]]></content:encoded>
  </item>
  <item>
    <title>Primary constructors and their surprises</title>
    <link>https://www.cdsmith.dev/blog/2026-04-26-primary-constructors-pitfalls/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-04-26-primary-constructors-pitfalls/</guid>
    <pubDate>Sun, 26 Apr 2026 12:00:00 GMT</pubDate>
    <category>C#</category>
    <category>.NET</category>
    <description>Primary constructors on classes save a lot of boilerplate for dependency injection. Their parameters aren&apos;t readonly fields, though, and that has consequences.</description>
    <content:encoded><![CDATA[<p>C# 12 added primary constructors to classes and structs, and they're a natural fit for services that receive dependencies:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">OrderService</span>(<span class="tok-type">OrdersDb</span> db, <span class="tok-type">ILogger</span>&lt;<span class="tok-type">OrderService</span>&gt; logger)
{
    <span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span> <span class="tok-method">CancelAsync</span>(<span class="tok-type">Guid</span> id, <span class="tok-type">CancellationToken</span> ct)
    {
        <span class="tok-keyword">var</span> order = <span class="tok-keyword">await</span> db.Orders.<span class="tok-method">FindAsync</span>([id], ct);
        logger.<span class="tok-method">LogInformation</span>(<span class="tok-string">"Cancelling order {OrderId}"</span>, id);
        <span class="tok-comment">// ...</span>
    }
}</code></pre>
<p>No private fields, no constructor body, no assignments. It's a clear improvement. But primary constructor parameters on a class behave differently from what you might assume.</p>
<h2 id="1-they-arent-readonly"><a class="anchor" href="#1-they-arent-readonly" aria-hidden="true">#</a>1. They aren't readonly</h2>
<p>Parameters are captured as mutable state. Nothing stops this:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">OrderService</span>(<span class="tok-type">OrdersDb</span> db)
{
    <span class="tok-keyword">public</span> <span class="tok-keyword">void</span> <span class="tok-method">Reset</span>() =&gt; db = <span class="tok-keyword">null</span>!;   <span class="tok-comment">// compiles</span>
}</code></pre>
<p>You'd never write that on purpose, but an accidental reassignment can slip through review. If immutability matters, assign the parameter to a <code>readonly</code> field:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">OrderService</span>(<span class="tok-type">OrdersDb</span> db)
{
    <span class="tok-keyword">private</span> <span class="tok-keyword">readonly</span> <span class="tok-type">OrdersDb</span> _db = db;
}</code></pre>
<p>Then use only <code>_db</code> in the class body.</p>
<h2 id="2-using-both-the-parameter-and-a-field-stores-it-twice"><a class="anchor" href="#2-using-both-the-parameter-and-a-field-stores-it-twice" aria-hidden="true">#</a>2. Using both the parameter and a field stores it twice</h2>
<p>If you initialize a field from the parameter <strong>and</strong> also use the parameter elsewhere, the compiler stores two copies:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">OrderService</span>(<span class="tok-type">OrdersDb</span> db)
{
    <span class="tok-keyword">private</span> <span class="tok-keyword">readonly</span> <span class="tok-type">OrdersDb</span> _db = db;

    <span class="tok-keyword">public</span> <span class="tok-type">Task</span> <span class="tok-method">SaveAsync</span>() =&gt; db.<span class="tok-method">SaveChangesAsync</span>();   <span class="tok-comment">// warning CS9124</span>
}</code></pre>
<p>The warning tells you the parameter is captured as well as used to initialize a member. Pick one and use it consistently.</p>
<h2 id="3-theyre-not-properties"><a class="anchor" href="#3-theyre-not-properties" aria-hidden="true">#</a>3. They're not properties</h2>
<p>On a <code>record</code>, positional parameters become public properties. On a class or struct, they don't: they're only visible inside the type. Copying a record declaration into a class and expecting <code>service.Db</code> to work won't compile.</p>
<h2 id="4-validation-needs-a-field-initializer"><a class="anchor" href="#4-validation-needs-a-field-initializer" aria-hidden="true">#</a>4. Validation needs a field initializer</h2>
<p>There's no constructor body to put checks in. Use a field or property initializer:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">RetryPolicy</span>(<span class="tok-keyword">int</span> maxAttempts)
{
    <span class="tok-keyword">private</span> <span class="tok-keyword">readonly</span> <span class="tok-keyword">int</span> _maxAttempts = maxAttempts &gt; <span class="tok-number">0</span>
        ? maxAttempts
        : <span class="tok-keyword">throw</span> <span class="tok-keyword">new</span> <span class="tok-type">ArgumentOutOfRangeException</span>(<span class="tok-keyword">nameof</span>(maxAttempts));
}</code></pre>
<p>Or write a normal constructor. Primary constructors are a convenience, not a requirement.</p>
<h2 id="where-they-fit-best"><a class="anchor" href="#where-they-fit-best" aria-hidden="true">#</a>Where they fit best</h2>
<ul><li><strong>Dependency injection</strong> in services, handlers, controllers and hosted services, where the parameters are just dependencies to call.</li><li><strong>Simple types</strong> with no validation and no need for immutability guarantees.</li></ul>
<p>For domain types with invariants, an explicit constructor with readonly fields is often clearer.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Primary constructors remove a lot of DI boilerplate. Remember that their parameters are mutable captured variables, not readonly fields or properties. Assign to a readonly field when that matters, don't mix the field and the parameter, and use a normal constructor when you need validation.</p>]]></content:encoded>
  </item>
  <item>
    <title>Fan-out and fan-in with Durable Functions</title>
    <link>https://www.cdsmith.dev/blog/2026-04-19-durable-functions-fan-out-fan-in/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-04-19-durable-functions-fan-out-fan-in/</guid>
    <pubDate>Sun, 19 Apr 2026 12:00:00 GMT</pubDate>
    <category>Azure</category>
    <category>Azure Functions</category>
    <category>Integrations</category>
    <description>Processing 5,000 records one after another takes too long, and running them all at once in one function times out. Durable Functions runs them in parallel and collects the results.</description>
    <content:encoded><![CDATA[<p>A common integration job: get a list of 5,000 accounts, call an external API for each, and produce a summary at the end. In a single function, doing them one at a time is too slow, and starting them all at once with <code>Task.WhenAll</code> risks hitting the function's timeout and loses all progress if the instance restarts.</p>
<p><strong>Durable Functions</strong> solves this with an orchestration: a function that coordinates other functions, survives restarts and can wait for many parallel tasks.</p>
<h2 id="the-pattern"><a class="anchor" href="#the-pattern" aria-hidden="true">#</a>The pattern</h2>
<ol><li><strong>Fan out:</strong> start one activity per item.</li><li><strong>Fan in:</strong> wait for all of them and combine the results.</li></ol>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">class</span> <span class="tok-method">AccountSync</span>(<span class="tok-type">CrmClient</span> crm, <span class="tok-type">SyncService</span> syncService)
{
    [<span class="tok-type">Function</span>(<span class="tok-keyword">nameof</span>(<span class="tok-type">SyncAllAccounts</span>))]
    <span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">SyncSummary</span>&gt; <span class="tok-method">SyncAllAccounts</span>(
        [<span class="tok-type">OrchestrationTrigger</span>] <span class="tok-type">TaskOrchestrationContext</span> context)
    {
        <span class="tok-keyword">var</span> accountIds = <span class="tok-keyword">await</span> context.<span class="tok-method">CallActivityAsync</span>&lt;<span class="tok-method">List</span>&lt;<span class="tok-keyword">string</span>&gt;&gt;(<span class="tok-keyword">nameof</span>(<span class="tok-type">GetAccountIds</span>));

        <span class="tok-keyword">var</span> tasks = accountIds
            .<span class="tok-method">Select</span>(id =&gt; context.<span class="tok-method">CallActivityAsync</span>&lt;<span class="tok-keyword">bool</span>&gt;(<span class="tok-keyword">nameof</span>(<span class="tok-type">SyncAccount</span>), id))
            .<span class="tok-method">ToList</span>();

        <span class="tok-keyword">var</span> results = <span class="tok-keyword">await</span> <span class="tok-type">Task</span>.<span class="tok-method">WhenAll</span>(tasks);

        <span class="tok-keyword">return</span> <span class="tok-keyword">new</span> <span class="tok-type">SyncSummary</span>(<span class="tok-type">Total</span>: results.Length, <span class="tok-type">Failed</span>: results.<span class="tok-method">Count</span>(ok =&gt; !ok));
    }

    [<span class="tok-type">Function</span>(<span class="tok-keyword">nameof</span>(<span class="tok-type">GetAccountIds</span>))]
    <span class="tok-keyword">public</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">List</span>&lt;<span class="tok-keyword">string</span>&gt;&gt; <span class="tok-method">GetAccountIds</span>([<span class="tok-type">ActivityTrigger</span>] <span class="tok-keyword">object</span>? input, <span class="tok-type">FunctionContext</span> ctx)
        =&gt; crm.<span class="tok-method">GetActiveAccountIdsAsync</span>();

    [<span class="tok-type">Function</span>(<span class="tok-keyword">nameof</span>(<span class="tok-type">SyncAccount</span>))]
    <span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-keyword">bool</span>&gt; <span class="tok-method">SyncAccount</span>([<span class="tok-type">ActivityTrigger</span>] <span class="tok-keyword">string</span> accountId, <span class="tok-type">FunctionContext</span> ctx)
    {
        <span class="tok-comment">// call the external API, write the result; return false on a handled failure</span>
        <span class="tok-keyword">return</span> <span class="tok-keyword">await</span> syncService.<span class="tok-method">SyncAsync</span>(accountId);
    }
}</code></pre>
<p>(The example uses the isolated worker model, with dependencies injected through the class's constructor.)</p>
<p>Each activity runs as its own function execution, spread across instances. The orchestrator checkpoints its progress to storage, so if an instance restarts halfway through, completed activities aren't run again.</p>
<p>Start the orchestration from any trigger:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">string</span> instanceId = <span class="tok-keyword">await</span> durableClient.<span class="tok-method">ScheduleNewOrchestrationInstanceAsync</span>(<span class="tok-keyword">nameof</span>(<span class="tok-type">SyncAllAccounts</span>));</code></pre>
<h2 id="the-orchestrator-rules"><a class="anchor" href="#the-orchestrator-rules" aria-hidden="true">#</a>The orchestrator rules</h2>
<p>An orchestrator function is <strong>replayed</strong> from its history every time it wakes up. To replay correctly, its code must be deterministic:</p>
<ul><li><strong>No I/O</strong> in the orchestrator. Database calls and HTTP requests go in activities.</li><li><strong>No <code>DateTime.UtcNow</code>.</strong> Use <code>context.CurrentUtcDateTime</code>.</li><li><strong>No random values or new GUIDs</strong> generated directly; generate them in an activity, or use <code>context.NewGuid()</code>.</li></ul>
<p>Breaking these rules causes errors or strange behavior on replay, often only under load.</p>
<h2 id="dont-overwhelm-the-other-side"><a class="anchor" href="#dont-overwhelm-the-other-side" aria-hidden="true">#</a>Don't overwhelm the other side</h2>
<p>Starting 5,000 activities at once can easily exceed the external API's rate limit. Two ways to control it:</p>
<ul><li><strong>Limit concurrency per instance</strong> with <code>maxConcurrentActivityFunctions</code> in <code>host.json</code>.</li><li><strong>Fan out in batches:</strong> process 100 items, wait, then the next 100.</li></ul>
<h2 id="large-lists"><a class="anchor" href="#large-lists" aria-hidden="true">#</a>Large lists</h2>
<p>Each orchestration's history grows with every activity it starts. For very large jobs, split the work into sub-orchestrations (for example, one per batch of 500) with <code>CallSubOrchestratorAsync</code>, so no single history gets huge.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>When a job needs to process many items in parallel and must survive restarts, use a Durable Functions orchestration: fan out an activity per item, fan in with <code>Task.WhenAll</code>, keep the orchestrator deterministic, and limit concurrency so you don't flood the systems you're calling.</p>]]></content:encoded>
  </item>
  <item>
    <title>Organize code by feature, not by layer</title>
    <link>https://www.cdsmith.dev/blog/2026-04-12-vertical-slice-architecture/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-04-12-vertical-slice-architecture/</guid>
    <pubDate>Sun, 12 Apr 2026 12:00:00 GMT</pubDate>
    <category>Architecture</category>
    <category>ASP.NET Core</category>
    <description>Controllers, Services, Repositories: a layered folder structure spreads one feature across five places. Vertical slices keep everything for a feature together.</description>
    <content:encoded><![CDATA[<p>Here's a familiar project layout:</p>
<pre><code>Controllers/OrdersController.cs
Services/OrderService.cs
Services/IOrderService.cs
Repositories/OrderRepository.cs
Repositories/IOrderRepository.cs
Models/PlaceOrderRequest.cs
Validators/PlaceOrderValidator.cs</code></pre>
<p>To change how an order is placed, you touch most of those files. <code>OrderService</code> grows to 1,500 lines because every order feature goes through it. And a change for one feature risks breaking another that happens to share a method.</p>
<h2 id="slice-by-use-case-instead"><a class="anchor" href="#slice-by-use-case-instead" aria-hidden="true">#</a>Slice by use case instead</h2>
<p><strong>Vertical slice architecture</strong> groups code by what it does for the user. Each slice contains everything one use case needs, from the endpoint to the database:</p>
<pre><code>Features/
  Orders/
    PlaceOrder.cs
    CancelOrder.cs
    GetOrderDetails.cs
    ListOrdersForCustomer.cs
  Shipping/
    ShipOrder.cs</code></pre>
<p>A slice can be a single file:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-keyword">class</span> <span class="tok-type">PlaceOrder</span>
{
    <span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">Request</span>(<span class="tok-type">Guid</span> <span class="tok-type">CustomerId</span>, <span class="tok-type">List</span>&lt;<span class="tok-type">LineItem</span>&gt; <span class="tok-type">Items</span>);
    <span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">Response</span>(<span class="tok-type">Guid</span> <span class="tok-type">OrderId</span>, <span class="tok-keyword">decimal</span> <span class="tok-type">Total</span>);

    <span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-keyword">void</span> <span class="tok-method">Map</span>(<span class="tok-type">IEndpointRouteBuilder</span> app) =&gt;
        app.<span class="tok-method">MapPost</span>(<span class="tok-string">"/orders"</span>, <span class="tok-type">Handle</span>);

    <span class="tok-keyword">private</span> <span class="tok-keyword">static</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">IResult</span>&gt; <span class="tok-method">Handle</span>(<span class="tok-type">Request</span> request, <span class="tok-type">OrdersDb</span> db, <span class="tok-type">CancellationToken</span> ct)
    {
        <span class="tok-keyword">if</span> (request.Items.Count == <span class="tok-number">0</span>)
            <span class="tok-keyword">return</span> <span class="tok-type">Results</span>.<span class="tok-method">ValidationProblem</span>(<span class="tok-keyword">new</span> <span class="tok-type">Dictionary</span>&lt;<span class="tok-keyword">string</span>, <span class="tok-keyword">string</span>[]&gt;
            {
                [<span class="tok-string">"items"</span>] = [<span class="tok-string">"An order needs at least one item."</span>]
            });

        <span class="tok-keyword">var</span> order = <span class="tok-type">Order</span>.<span class="tok-method">Create</span>(request.CustomerId, request.Items);
        db.Orders.<span class="tok-method">Add</span>(order);
        <span class="tok-keyword">await</span> db.<span class="tok-method">SaveChangesAsync</span>(ct);

        <span class="tok-keyword">return</span> <span class="tok-type">Results</span>.<span class="tok-method">Created</span>(<span class="tok-string">$"/orders/{order.Id}"</span>, <span class="tok-keyword">new</span> <span class="tok-type">Response</span>(order.Id, order.Total));
    }
}</code></pre>
<h2 id="why-it-works"><a class="anchor" href="#why-it-works" aria-hidden="true">#</a>Why it works</h2>
<ul><li><strong>Changes stay local.</strong> A new requirement for placing orders changes <code>PlaceOrder.cs</code> and nothing else.</li><li><strong>Each slice can be as simple or complex as it needs.</strong> A read-only list can project straight from <code>DbContext</code> to a DTO. A complex command can use a rich domain model. No layer forces every feature through the same ceremony.</li><li><strong>Less abstraction for its own sake.</strong> No <code>IOrderService</code> with one implementation and no repository that only wraps <code>DbContext</code>.</li><li><strong>Easier to delete.</strong> Removing a feature means removing a file or folder.</li></ul>
<h2 id="whats-still-shared"><a class="anchor" href="#whats-still-shared" aria-hidden="true">#</a>What's still shared</h2>
<p>Slices aren't a ban on sharing. The domain model, such as the <code>Order</code> entity and its rules, is shared. So are infrastructure concerns: the <code>DbContext</code>, authentication, logging, validation and error handling. What you avoid is a shared <em>service layer</em> that every feature has to squeeze through.</p>
<p>When two slices really do need the same logic, extract it then, into the domain or a small helper. Don't start with the abstraction.</p>
<h2 id="do-you-need-mediatr"><a class="anchor" href="#do-you-need-mediatr" aria-hidden="true">#</a>Do you need MediatR?</h2>
<p>Many vertical slice examples send each request through a mediator library. It's not required. Minimal API endpoints or controller actions calling a handler directly are a perfectly good slice. Add a mediator only if you want its pipeline behaviors and are happy with the dependency.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Group code by feature, so one use case lives in one place. Share the domain model and infrastructure, not a service layer, and extract common code only when duplication actually appears.</p>]]></content:encoded>
  </item>
  <item>
    <title>Paging through data without skipping records</title>
    <link>https://www.cdsmith.dev/blog/2026-04-05-paging-through-apis-correctly/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-04-05-paging-through-apis-correctly/</guid>
    <pubDate>Sun, 05 Apr 2026 12:00:00 GMT</pubDate>
    <category>Integrations</category>
    <category>.NET</category>
    <category>Data</category>
    <description>Offset paging quietly skips or repeats records when data changes mid-sync. Cursor and keyset paging don&apos;t, whether you&apos;re calling an API or building one.</description>
    <content:encoded><![CDATA[<p>A nightly job syncs contacts from a CRM, 100 at a time:</p>
<pre><code>GET /contacts?offset=0&amp;limit=100
GET /contacts?offset=100&amp;limit=100
GET /contacts?offset=200&amp;limit=100</code></pre>
<p>It runs every night without errors, and yet a few contacts never arrive. The reason is that the data changes while you're paging through it.</p>
<h2 id="why-offset-paging-drifts"><a class="anchor" href="#why-offset-paging-drifts" aria-hidden="true">#</a>Why offset paging drifts</h2>
<p>Say you've read page one, records 1 to 100. Before you request page two, someone deletes record 50. Everything after it shifts up by one, so the record that was 101 is now 100. Your request for &quot;offset 100&quot; starts at the old record 102, and the old record 101 is never read.</p>
<p>Insertions cause the opposite problem: a record shifts down onto the next page and you read it twice. Duplicates are annoying; silently missed records are worse, because nothing tells you they happened.</p>
<h2 id="prefer-cursors-when-the-api-offers-them"><a class="anchor" href="#prefer-cursors-when-the-api-offers-them" aria-hidden="true">#</a>Prefer cursors when the API offers them</h2>
<p>Many APIs return a <strong>cursor</strong>, an opaque token that marks your position:</p>
<pre><code>GET /contacts?limit=100
→ { "items": [...], "nextCursor": "eyJpZCI6..." }

GET /contacts?limit=100&amp;cursor=eyJpZCI6...</code></pre>
<p>The cursor encodes where you were, usually the sort key of the last item, so inserts and deletes elsewhere don't move you. If an API offers both offset and cursor paging, use the cursor.</p>
<h2 id="sync-by-modification-date-with-overlap"><a class="anchor" href="#sync-by-modification-date-with-overlap" aria-hidden="true">#</a>Sync by modification date, with overlap</h2>
<p>For incremental syncs, page by &quot;modified since&quot; rather than reading everything:</p>
<ul><li>Store the latest <code>modifiedAt</code> you processed.</li><li>Next run, request records modified <strong>at or after</strong> that time, minus a small overlap such as a few minutes, to cover clock differences and records that were mid-update.</li><li>Make your writes idempotent, an upsert by external ID, so the overlap's duplicates are harmless.</li></ul>
<h2 id="building-your-own-api-keyset-paging"><a class="anchor" href="#building-your-own-api-keyset-paging" aria-hidden="true">#</a>Building your own API: keyset paging</h2>
<p>If you're the one exposing the data, give callers a stable way to page. <strong>Keyset paging</strong> filters on the last key seen instead of skipping rows:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">Page</span>&lt;<span class="tok-type">OrderDto</span>&gt;&gt; <span class="tok-method">GetOrdersAsync</span>(<span class="tok-type">DateTimeOffset</span>? afterCreated, <span class="tok-type">Guid</span>? afterId, <span class="tok-keyword">int</span> limit, <span class="tok-type">CancellationToken</span> ct)
{
    <span class="tok-keyword">var</span> query = db.Orders.<span class="tok-method">AsNoTracking</span>();

    <span class="tok-keyword">if</span> (afterCreated <span class="tok-keyword">is</span> { } created &amp;&amp; afterId <span class="tok-keyword">is</span> { } id)
    {
        query = query.<span class="tok-method">Where</span>(o =&gt; o.CreatedAt &gt; created || (o.CreatedAt == created &amp;&amp; o.Id &gt; id));
    }

    <span class="tok-keyword">var</span> items = <span class="tok-keyword">await</span> query
        .<span class="tok-method">OrderBy</span>(o =&gt; o.CreatedAt).<span class="tok-method">ThenBy</span>(o =&gt; o.Id)
        .<span class="tok-method">Take</span>(limit)
        .<span class="tok-method">Select</span>(o =&gt; <span class="tok-keyword">new</span> <span class="tok-type">OrderDto</span>(o.Id, o.CreatedAt, o.Total))
        .<span class="tok-method">ToListAsync</span>(ct);

    <span class="tok-keyword">return</span> <span class="tok-keyword">new</span> <span class="tok-type">Page</span>&lt;<span class="tok-type">OrderDto</span>&gt;(items, items.Count == limit ? <span class="tok-method">EncodeCursor</span>(items[^<span class="tok-number">1</span>]) : <span class="tok-keyword">null</span>);
}</code></pre>
<p>Two details make it work:</p>
<ul><li><strong>Order by a unique combination.</strong> <code>CreatedAt</code> alone can have ties; adding <code>Id</code> makes the order total, so no row is skipped or repeated at a page boundary.</li><li><strong>Index those columns.</strong> With an index on <code>(CreatedAt, Id)</code>, each page is a quick seek, however deep into the data you are. Offset paging gets slower with every page because the database still reads every skipped row.</li></ul>
<p>Encode the last item's values into an opaque cursor string, so you can change the scheme later without breaking clients.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Don't page through changing data with offsets. Use cursors when an API provides them, sync incrementally by modification date with a small overlap and idempotent writes, and give your own APIs keyset paging on a unique, indexed sort order.</p>]]></content:encoded>
  </item>
  <item>
    <title>Faster JSON with System.Text.Json source generation</title>
    <link>https://www.cdsmith.dev/blog/2026-03-29-system-text-json-source-generation/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-03-29-system-text-json-source-generation/</guid>
    <pubDate>Sun, 29 Mar 2026 12:00:00 GMT</pubDate>
    <category>.NET</category>
    <category>Performance</category>
    <description>By default, System.Text.Json uses reflection to work out how to serialize your types. A source generator can do that work at compile time instead, for faster startup and trimming support.</description>
    <content:encoded><![CDATA[<p>The first time <code>System.Text.Json</code> serializes a type, it inspects it with reflection and builds metadata about its properties. That costs time at startup, on every type, and it's the reason reflection-based serialization doesn't work with trimming or Native AOT.</p>
<p>The source generator does the same work at compile time.</p>
<h2 id="set-it-up"><a class="anchor" href="#set-it-up" aria-hidden="true">#</a>Set it up</h2>
<p>Declare a partial context class listing the types you serialize:</p>
<pre data-lang="csharp"><code class="language-csharp">[<span class="tok-type">JsonSourceGenerationOptions</span>(<span class="tok-type">PropertyNamingPolicy</span> = <span class="tok-type">JsonKnownNamingPolicy</span>.CamelCase)]
[<span class="tok-type">JsonSerializable</span>(<span class="tok-keyword">typeof</span>(<span class="tok-type">OrderPlaced</span>))]
[<span class="tok-type">JsonSerializable</span>(<span class="tok-keyword">typeof</span>(<span class="tok-type">List</span>&lt;<span class="tok-type">OrderSummary</span>&gt;))]
<span class="tok-keyword">public</span> <span class="tok-keyword">partial</span> <span class="tok-keyword">class</span> <span class="tok-type">AppJsonContext</span> : <span class="tok-type">JsonSerializerContext</span>
{
}</code></pre>
<p>The compiler generates the serialization metadata for those types. Then pass it when you serialize:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">var</span> json = <span class="tok-type">JsonSerializer</span>.<span class="tok-method">Serialize</span>(message, <span class="tok-type">AppJsonContext</span>.Default.OrderPlaced);
<span class="tok-keyword">var</span> order = <span class="tok-type">JsonSerializer</span>.<span class="tok-method">Deserialize</span>(json, <span class="tok-type">AppJsonContext</span>.Default.OrderPlaced);</code></pre>
<p>The overloads that take a <code>JsonTypeInfo&lt;T&gt;</code> are strongly typed, so there's no need for a generic argument either.</p>
<h2 id="use-it-in-aspnet-core"><a class="anchor" href="#use-it-in-aspnet-core" aria-hidden="true">#</a>Use it in ASP.NET Core</h2>
<p>Register the context so minimal APIs use it for request and response bodies:</p>
<pre data-lang="csharp"><code class="language-csharp">builder.Services.<span class="tok-method">ConfigureHttpJsonOptions</span>(options =&gt;
    options.SerializerOptions.TypeInfoResolverChain.<span class="tok-method">Insert</span>(<span class="tok-number">0</span>, <span class="tok-type">AppJsonContext</span>.Default));</code></pre>
<p>Types that aren't in your context still fall back to the default reflection-based resolver, so you can add types gradually.</p>
<h2 id="what-you-get"><a class="anchor" href="#what-you-get" aria-hidden="true">#</a>What you get</h2>
<ul><li><strong>Faster startup.</strong> No reflection pass the first time each type is serialized. That's most noticeable in Azure Functions and other apps that start often.</li><li><strong>Trimming and Native AOT support.</strong> Required if you publish trimmed or AOT-compiled apps, where reflection-based serialization doesn't work.</li><li><strong>Compile-time feedback.</strong> Some unsupported type shapes show up as build warnings instead of runtime surprises.</li></ul>
<h2 id="when-it-isnt-worth-it"><a class="anchor" href="#when-it-isnt-worth-it" aria-hidden="true">#</a>When it isn't worth it</h2>
<p>For a long-running web API where startup time doesn't matter, the gains are modest. And the context has to know every type up front, which doesn't fit code that serializes arbitrary types at runtime, such as a generic <code>object</code> payload. Use it where startup, trimming or AOT matter, and don't feel you have to convert everything else.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Add a <code>JsonSerializerContext</code> for the types you serialize most, pass its type info when you serialize, and register it with ASP.NET Core. You get faster startup and support for trimming and Native AOT, and you can adopt it one type at a time.</p>]]></content:encoded>
  </item>
  <item>
    <title>Exceptions or results for expected failures?</title>
    <link>https://www.cdsmith.dev/blog/2026-03-22-result-pattern-vs-exceptions/</link>
    <guid isPermaLink="true">https://www.cdsmith.dev/blog/2026-03-22-result-pattern-vs-exceptions/</guid>
    <pubDate>Sun, 22 Mar 2026 12:00:00 GMT</pubDate>
    <category>C#</category>
    <category>Architecture</category>
    <description>&quot;Customer not found&quot; isn&apos;t exceptional, but most .NET code throws for it anyway. A simple result type makes expected failures part of the method&apos;s signature.</description>
    <content:encoded><![CDATA[<p>Most .NET codebases handle every kind of failure with exceptions. A database that's down throws. So does a customer that doesn't exist, an order that's already shipped and a discount code that expired. The last three aren't exceptional at all: they're normal outcomes the business expects.</p>
<p>Using exceptions for them has costs:</p>
<ul><li><strong>The signature hides them.</strong> <code>Order Ship(Guid orderId)</code> doesn't tell you it can fail because the order was cancelled. You find out by reading the implementation or from a production log.</li><li><strong>Control flow gets hard to follow.</strong> A <code>throw</code> deep in a service is caught in middleware three layers up, which maps it to an HTTP status code.</li><li><strong>They're slow for hot paths.</strong> Throwing captures a stack trace. For an expected outcome that happens thousands of times a minute, that's wasted work.</li></ul>
<h2 id="a-minimal-result-type"><a class="anchor" href="#a-minimal-result-type" aria-hidden="true">#</a>A minimal result type</h2>
<p>You don't need a library to start:</p>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-method">Error</span>(<span class="tok-keyword">string</span> <span class="tok-type">Code</span>, <span class="tok-keyword">string</span> <span class="tok-type">Message</span>);

<span class="tok-keyword">public</span> <span class="tok-keyword">record</span> <span class="tok-type">Result</span>&lt;<span class="tok-type">T</span>&gt;
{
    <span class="tok-keyword">public</span> <span class="tok-type">T</span>? <span class="tok-type">Value</span> { <span class="tok-keyword">get</span>; }
    <span class="tok-keyword">public</span> <span class="tok-type">Error</span>? <span class="tok-type">Error</span> { <span class="tok-keyword">get</span>; }
    <span class="tok-keyword">public</span> <span class="tok-keyword">bool</span> <span class="tok-type">IsSuccess</span> =&gt; <span class="tok-type">Error</span> <span class="tok-keyword">is</span> <span class="tok-keyword">null</span>;

    <span class="tok-keyword">private</span> <span class="tok-method">Result</span>(<span class="tok-type">T</span>? <span class="tok-keyword">value</span>, <span class="tok-type">Error</span>? error) =&gt; (<span class="tok-type">Value</span>, <span class="tok-type">Error</span>) = (<span class="tok-keyword">value</span>, error);

    <span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-type">Result</span>&lt;<span class="tok-type">T</span>&gt; <span class="tok-method">Success</span>(<span class="tok-type">T</span> <span class="tok-keyword">value</span>) =&gt; <span class="tok-keyword">new</span>(<span class="tok-keyword">value</span>, <span class="tok-keyword">null</span>);
    <span class="tok-keyword">public</span> <span class="tok-keyword">static</span> <span class="tok-type">Result</span>&lt;<span class="tok-type">T</span>&gt; <span class="tok-method">Failure</span>(<span class="tok-type">Error</span> error) =&gt; <span class="tok-keyword">new</span>(<span class="tok-keyword">default</span>, error);
}</code></pre>
<pre data-lang="csharp"><code class="language-csharp"><span class="tok-keyword">public</span> <span class="tok-keyword">async</span> <span class="tok-type">Task</span>&lt;<span class="tok-type">Result</span>&lt;<span class="tok-type">Shipment</span>&gt;&gt; <span class="tok-method">ShipAsync</span>(<span class="tok-type">Guid</span> orderId, <span class="tok-type">CancellationToken</span> ct)
{
    <span class="tok-keyword">var</span> order = <span class="tok-keyword">await</span> db.Orders.<span class="tok-method">FindAsync</span>([orderId], ct);
    <span class="tok-keyword">if</span> (order <span class="tok-keyword">is</span> <span class="tok-keyword">null</span>)
        <span class="tok-keyword">return</span> <span class="tok-type">Result</span>&lt;<span class="tok-type">Shipment</span>&gt;.<span class="tok-method">Failure</span>(<span class="tok-keyword">new</span>(<span class="tok-string">"order.not_found"</span>, <span class="tok-string">"Order not found."</span>));
    <span class="tok-keyword">if</span> (order.Status == <span class="tok-type">OrderStatus</span>.Cancelled)
        <span class="tok-keyword">return</span> <span class="tok-type">Result</span>&lt;<span class="tok-type">Shipment</span>&gt;.<span class="tok-method">Failure</span>(<span class="tok-keyword">new</span>(<span class="tok-string">"order.cancelled"</span>, <span class="tok-string">"Cancelled orders can't be shipped."</span>));

    <span class="tok-keyword">var</span> shipment = order.<span class="tok-method">Ship</span>();
    <span class="tok-keyword">await</span> db.<span class="tok-method">SaveChangesAsync</span>(ct);
    <span class="tok-keyword">return</span> <span class="tok-type">Result</span>&lt;<span class="tok-type">Shipment</span>&gt;.<span class="tok-method">Success</span>(shipment);
}</code></pre>
<p>The return type now says &quot;this can fail&quot;, and the caller has to look at the result to get the value.</p>
<h2 id="mapping-to-http"><a class="anchor" href="#mapping-to-http" aria-hidden="true">#</a>Mapping to HTTP</h2>
<p>At the edge, translate errors to status codes in one place:</p>
<pre data-lang="csharp"><code class="language-csharp">app.<span class="tok-method">MapPost</span>(<span class="tok-string">"/orders/{id:guid}/ship"</span>, <span class="tok-keyword">async</span> (<span class="tok-type">Guid</span> id, <span class="tok-type">ShippingService</span> shipping, <span class="tok-type">CancellationToken</span> ct) =&gt;
{
    <span class="tok-keyword">var</span> result = <span class="tok-keyword">await</span> shipping.<span class="tok-method">ShipAsync</span>(id, ct);
    <span class="tok-keyword">if</span> (result.IsSuccess) <span class="tok-keyword">return</span> <span class="tok-type">Results</span>.<span class="tok-method">Ok</span>(result.Value);

    <span class="tok-keyword">return</span> result.Error!.Code <span class="tok-keyword">switch</span>
    {
        <span class="tok-string">"order.not_found"</span> =&gt; <span class="tok-type">Results</span>.<span class="tok-method">NotFound</span>(result.Error),
        _ =&gt; <span class="tok-type">Results</span>.<span class="tok-method">Conflict</span>(result.Error)
    };
});</code></pre>
<h2 id="keep-exceptions-for-exceptional-things"><a class="anchor" href="#keep-exceptions-for-exceptional-things" aria-hidden="true">#</a>Keep exceptions for exceptional things</h2>
<p>Results don't replace exceptions. A database timeout, a null reference or a misconfigured connection string should still throw: there's nothing sensible the immediate caller can do, and the global exception handler should log it and return a <code>500</code>.</p>
<p>A useful rule: <strong>if the business would describe the outcome, return a result. If only a developer would, throw.</strong></p>
<h2 id="libraries"><a class="anchor" href="#libraries" aria-hidden="true">#</a>Libraries</h2>
<p>If you'd rather not maintain your own type, FluentResults, ErrorOr and OneOf are popular options with helpers for chaining and matching. Pick one and use it consistently. Mixing styles is worse than either.</p>
<h2 id="takeaway"><a class="anchor" href="#takeaway" aria-hidden="true">#</a>Takeaway</h2>
<p>Return a result for failures the business expects, so they're visible in the method signature and handled on purpose. Keep exceptions for the truly unexpected, and translate results to HTTP status codes in one place at the edge.</p>]]></content:encoded>
  </item>
</channel>
</rss>
