<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Giannis Georgopoulos</title>
    <description>The latest articles on DEV Community by Giannis Georgopoulos (@georgopoulosgiannis).</description>
    <link>https://dev.to/georgopoulosgiannis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1248597%2F325c9184-2880-4672-8749-95c29c2f51cc.jpeg</url>
      <title>DEV Community: Giannis Georgopoulos</title>
      <link>https://dev.to/georgopoulosgiannis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/georgopoulosgiannis"/>
    <language>en</language>
    <item>
      <title>Logging Like a Pro in .NET</title>
      <dc:creator>Giannis Georgopoulos</dc:creator>
      <pubDate>Thu, 15 May 2025 16:56:32 +0000</pubDate>
      <link>https://dev.to/georgopoulosgiannis/logging-like-a-pro-in-net-1bpj</link>
      <guid>https://dev.to/georgopoulosgiannis/logging-like-a-pro-in-net-1bpj</guid>
      <description>&lt;p&gt;Logs are your primary tool for understanding what your API is doing in production.&lt;br&gt;
Whether it's debugging a bug report, identifying performance issues, or reacting to an incident logs are your first and best signal.&lt;/p&gt;

&lt;p&gt;But many developers fall into two extremes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too little logging, and you're blind in production.&lt;/li&gt;
&lt;li&gt;Too much logging, and you're drowning in noise, cost, or leaked sensitive data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, we’ll take a smarter approach and cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up &lt;strong&gt;Serilog&lt;/strong&gt; for structured logging in .NET&lt;/li&gt;
&lt;li&gt;Logging exceptions using &lt;strong&gt;source generators&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Outputting request payloads as structured JSON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Masking sensitive data&lt;/strong&gt; to stay compliant (GDPR, etc.)&lt;/li&gt;
&lt;li&gt;Enriching logs with contextual information like &lt;code&gt;OrderId&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s dive in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Adding serilog to your project
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="n"&gt;Serilog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AspNetCore&lt;/span&gt;
&lt;span class="n"&gt;dotnet&lt;/span&gt; &lt;span class="k"&gt;add&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="n"&gt;Serilog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sinks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We’ll use the &lt;strong&gt;console sink&lt;/strong&gt; for simplicity because it works out of the box, doesn’t require any extra setup, and still gives us structured log output in your terminal or Docker logs.&lt;/p&gt;

&lt;p&gt;Once you're ready for production, you can plug in any of Serilog’s many &lt;a href="https://github.com/serilog/serilog/wiki/Provided-Sinks" rel="noopener noreferrer"&gt;available sinks&lt;/a&gt;, like &lt;strong&gt;Seq&lt;/strong&gt;, &lt;strong&gt;Application Insights&lt;/strong&gt;, etc.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Configure Serilog in &lt;code&gt;Program.cs&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Here’s a simple Serilog setup that logs to the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;
&lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoggerConfiguration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteTo&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"[{Timestamp:HH:mm:ss} {Level:u3}] [{EventId}:{EventName}] {Message:lj}{NewLine}{Exception}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLogger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSerilog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// your app setup...&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With just a few lines of setup, Serilog is now capturing structured logs to the console.  &lt;/p&gt;

&lt;p&gt;We will now launch our api and call the &lt;code&gt;/api/v2/Order/{orderId}/shipping&lt;/code&gt; endpoint from [[production-ready-api-devex|Part 1]].&lt;/p&gt;

&lt;p&gt;If we take a look at the console now we will see the HTTP request being logged.&lt;br&gt;
By default, all incoming HTTP requests will be logged, including successful ones.&lt;/p&gt;

&lt;p&gt;While this might seem helpful, it can quickly overwhelm your logs, especially when most requests succeed and add no diagnostic value. Even worse, platforms like Application Insights charge by the volume of logs ingested.&lt;/p&gt;

&lt;p&gt;For detailed traces, I recommend using OpenTelemetry with sampling. Logs should be reserved for &lt;strong&gt;intentional diagnostics&lt;/strong&gt;, not every HTTP 200.&lt;/p&gt;

&lt;p&gt;To reduce noise, disable default HTTP logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.MinimumLevel.Override&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.AspNetCore.Hosting"&lt;/span&gt;, LogEventLevel.Warning&lt;span class="o"&gt;)&lt;/span&gt;
.MinimumLevel.Override&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.AspNetCore.Mvc"&lt;/span&gt;, LogEventLevel.Warning&lt;span class="o"&gt;)&lt;/span&gt;
.MinimumLevel.Override&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.AspNetCore.Routing"&lt;/span&gt;, LogEventLevel.Warning&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You could also use &lt;code&gt;appsettings.json&lt;/code&gt; for configuring serilog. Read more &lt;a href="https://github.com/serilog/serilog-settings-configuration" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3: Logging our first exception
&lt;/h3&gt;

&lt;p&gt;So if we don't want to log our HTTP requests what do we want to log?&lt;/p&gt;

&lt;p&gt;The first thing is exceptions. When something goes wrong in our system, we need visibility.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I’ll cover global exception handling in a later article to keep the scope small.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, let’s simulate an exception in &lt;code&gt;OrderService.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetShippingInfoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Oops! Failed to set shipping info"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now update the controller to catch and log the error. Inject &lt;code&gt;ILogger&amp;lt;OrderController&amp;gt; logger&lt;/code&gt; and wrap the call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/v{v:apiVersion}/[controller]"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
&lt;span class="p"&gt;....&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;  &lt;/span&gt;
    &lt;span class="c1"&gt;/// Sets the shipping address for an order.    /// &amp;lt;/summary&amp;gt;    /// &amp;lt;response code="400"&amp;gt;    /// Returned when:    /// - 'ZipCode' is missing or invalid    /// - The order cannot be updated due to its current status    /// &amp;lt;/response&amp;gt;    [MapToApiVersion("2")]  &lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{orderId:guid}/shipping"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetShippingInfoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;  
        &lt;span class="p"&gt;{&lt;/span&gt;  
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
        &lt;span class="p"&gt;}&lt;/span&gt;  
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="p"&gt;{&lt;/span&gt;  
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Failed to set shipping info for request:{Request}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
                &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StackTrace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
                &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
    &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Use Source-Generated Logging
&lt;/h2&gt;

&lt;p&gt;The previous approach works but we can do better.&lt;/p&gt;

&lt;p&gt;.NET provides &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/high-performance-logging" rel="noopener noreferrer"&gt;high-performance source-generated logging&lt;/a&gt;, which avoids boxing, allocations, and improves log searchability via &lt;code&gt;EventId&lt;/code&gt; and &lt;code&gt;EventName&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Update the controller to use a static partial log method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EventName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogFailedSetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Failed to set shipping info for {Request}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LogFailedSetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetShippingInfoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event id and name we defined in the &lt;code&gt;LoggerMessage&lt;/code&gt; attribute will be extremely useful later on, when we will want to search our logs and add rules for automated alerting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Controlling Log Output for Complex Objects
&lt;/h2&gt;

&lt;p&gt;Let’s tweak our log message to include the actual request payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;LoggerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EventId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogFailedSetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Failed to set shipping info for {@Request}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;LogFailedSetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetShippingInfoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The key difference here is the &lt;code&gt;@&lt;/code&gt; symbol in &lt;code&gt;{@Request}&lt;/code&gt;. Without it, your logs will just say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Failed to &lt;span class="nb"&gt;set &lt;/span&gt;shipping info &lt;span class="k"&gt;for &lt;/span&gt;SetShippingInfoRequest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But with &lt;code&gt;@&lt;/code&gt;, Serilog serializes the entire object as structured JSON and includes all public properties of &lt;code&gt;SetShippingInfoRequest&lt;/code&gt; in your logs. This makes it much easier to understand what actually went wrong.&lt;/p&gt;

&lt;p&gt;However, this technique comes with an important caveat.&lt;/p&gt;

&lt;p&gt;When logging full objects, you risk unintentionally including sensitive data like full recipient names, emails, phone numbers, or tokens. This can quickly lead to GDPR violations or internal policy breaches, especially if logs are forwarded to external systems like Application Insights or Seq.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Redacting Sensitive Data
&lt;/h2&gt;

&lt;p&gt;To avoid leaking sensitive data, we’ll use the &lt;a href="https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive" rel="noopener noreferrer"&gt;&lt;code&gt;Serilog.Enrichers.Sensitive&lt;/code&gt;&lt;/a&gt; package, which allows you to redact specific properties automatically.&lt;/p&gt;

&lt;p&gt;Install the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Serilog.Enrichers.Sensitive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your &lt;code&gt;Program.cs&lt;/code&gt;, configure Serilog to redact sensitive properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoggerConfiguration&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinimumLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Override&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.Hosting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogEventLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinimumLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Override&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.Mvc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogEventLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinimumLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Override&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.Routing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogEventLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enrich&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithSensitiveDataMasking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaskProperties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"RecipientName"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="p"&gt;})&lt;/span&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteTo&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"[{Timestamp:HH:mm:ss} {Level:u3}] [{EventId}:{EventName}] {Message:lj}{NewLine}{Exception}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateLogger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the &lt;code&gt;RecipientName&lt;/code&gt; property will be automatically be masked in the logs, so running our example will give us this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;22:09:28 ERR] &lt;span class="o"&gt;[{&lt;/span&gt; Id: 1, Name: &lt;span class="s2"&gt;"LogFailedSetShippingInfo"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;:]
Failed to &lt;span class="nb"&gt;set &lt;/span&gt;shipping info &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"RecipientName"&lt;/span&gt;: &lt;span class="s2"&gt;"***MASKED***"&lt;/span&gt;, &lt;span class="s2"&gt;"Street"&lt;/span&gt;: &lt;span class="s2"&gt;"123 Main St"&lt;/span&gt;, &lt;span class="s2"&gt;"City"&lt;/span&gt;: &lt;span class="s2"&gt;"New York"&lt;/span&gt;, &lt;span class="s2"&gt;"State"&lt;/span&gt;: &lt;span class="s2"&gt;"NY"&lt;/span&gt;, &lt;span class="s2"&gt;"ZipCode"&lt;/span&gt;: &lt;span class="s2"&gt;"10001"&lt;/span&gt;, &lt;span class="s2"&gt;"Country"&lt;/span&gt;: &lt;span class="s2"&gt;"USA"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
System.Exception: OOPS Failed to &lt;span class="nb"&gt;set &lt;/span&gt;shipping info
   at BloggingExamples.OrderService.SetShippingInfo&lt;span class="o"&gt;(&lt;/span&gt;Guid &lt;span class="nb"&gt;id&lt;/span&gt;, SetShippingInfoRequest request&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; /.../BloggingExamples/OrderService.cs:line 12
   at BloggingExamples.OrderController.SetShippingInfo&lt;span class="o"&gt;(&lt;/span&gt;Guid orderId, SetShippingInfoRequest request&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; /.../BloggingExamples/OrderController.cs:line 42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;You can mask by property name, regex pattern, or type name. This makes it easy to comply  with GDPR and protect your users.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 6: Add Contextual Properties Using &lt;code&gt;LogContext&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Sometimes you want to include additional information (like the current order ID, tenant ID, or user ID) in every log line within a given scope. You can do this using &lt;code&gt;LogContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the controller, push the &lt;code&gt;orderId&lt;/code&gt; into the log context:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="p"&gt;...&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{orderId:guid}/shipping"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetShippingInfoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;logContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LogContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PushProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;LogFailedSetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StackTrace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Don't return the stacktrace in production&lt;/span&gt;
                &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every log within that scope, including the one from &lt;code&gt;LogFailedSetShippingInfo&lt;/code&gt;, will now automatically include the &lt;code&gt;OrderId&lt;/code&gt; property in its structured output.&lt;/p&gt;

&lt;p&gt;This is incredibly useful for filtering logs by entity, user, or request—without cluttering every log call manually.&lt;/p&gt;

&lt;p&gt;We haven’t set up user or tenant context here, but in a real app, the most common pattern is to push those values into the log context from middleware.&lt;/p&gt;

&lt;p&gt;With this setup, you’re no longer logging blindly, you’re logging with purpose, clarity, and safety.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you enjoyed this post visit &lt;a href="https://eventuallyconsistent.xyz/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt; for more.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>dotnet</category>
      <category>webdev</category>
      <category>programming</category>
      <category>api</category>
    </item>
    <item>
      <title>A Professional Looking API</title>
      <dc:creator>Giannis Georgopoulos</dc:creator>
      <pubDate>Thu, 15 May 2025 16:47:51 +0000</pubDate>
      <link>https://dev.to/georgopoulosgiannis/building-production-grade-apis-in-net-part-2-a-professional-looking-api-2d8</link>
      <guid>https://dev.to/georgopoulosgiannis/building-production-grade-apis-in-net-part-2-a-professional-looking-api-2d8</guid>
      <description>&lt;p&gt;Your &lt;strong&gt;OpenAPI spec&lt;/strong&gt; is the contract that defines how consumers interact with your API. It powers visual tools that help developers understand, test, and integrate with your endpoints.&lt;/p&gt;

&lt;p&gt;This spec powers visual tools that help developers understand, test, and integrate with your API. While Swagger UI has been the go-to for years, modern alternatives like &lt;a href="https://scalar.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Scalar&lt;/strong&gt;&lt;/a&gt; are raising the bar with cleaner UX, better search, and more intuitive navigation. For this post, we’ll be using &lt;strong&gt;Scalar&lt;/strong&gt; to showcase how a well-designed OpenAPI spec gives your API a professional, developer-friendly front.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Hint&lt;/strong&gt;: Starting in &lt;strong&gt;.NET 9&lt;/strong&gt;, there’s a built-in way to generate the OpenAPI spec using &lt;code&gt;builder.Services.AddOpenApi()&lt;/code&gt;. However, as of now, it &lt;strong&gt;does not support XML comments&lt;/strong&gt;, which are essential for rich and descriptive documentation. This makes it a &lt;strong&gt;no-go for production APIs&lt;/strong&gt; that prioritize clarity. It is supported however in &lt;strong&gt;.NET 10&lt;/strong&gt; and you can see an example &lt;a href="https://github.com/captainsafia/aspnet-openapi-xml" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;strong&gt;Part 1&lt;/strong&gt;, we cleaned up a flawed &lt;code&gt;UpdateOrder&lt;/code&gt; endpoint and replaced it with clear, task-based actions like &lt;code&gt;SetShippingInfo&lt;/code&gt;. Now in &lt;strong&gt;Part 2&lt;/strong&gt;, we’ll visualize the impact of those changes in our OpenAPI docs. You’ll see how thoughtful design decisions translate into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cleaner, more understandable docs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easier client integration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fewer support questions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And a stronger impression of your API as production-grade&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s make your OpenAPI spec do more than just validate — let’s make it teach, guide, and inspire confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualizing the Difference: v1 vs v2
&lt;/h2&gt;

&lt;p&gt;Let’s bring our changes to life by exposing &lt;strong&gt;two versions&lt;/strong&gt; of the same endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Version 1&lt;/strong&gt; (&lt;code&gt;v1&lt;/code&gt;) shows the original, flawed &lt;code&gt;UpdateOrder&lt;/code&gt; design&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Version 2&lt;/strong&gt; (&lt;code&gt;v2&lt;/code&gt;) presents the refactored, task-based &lt;code&gt;SetShippingInfo&lt;/code&gt; approach&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This not only helps us compare the &lt;strong&gt;before/after&lt;/strong&gt; in Swagger/Scalar — it also gives us a chance to introduce &lt;strong&gt;API versioning&lt;/strong&gt;, a crucial concept for long-term maintainability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Versioning and Swagger
&lt;/h3&gt;

&lt;p&gt;We’ll use &lt;strong&gt;URL-based versioning&lt;/strong&gt; (&lt;code&gt;/api/v1/...&lt;/code&gt;, &lt;code&gt;/api/v2/...&lt;/code&gt;) since it’s the most straightforward to demonstrate in OpenAPI docs and widely supported by tools like Scalar.&lt;/p&gt;

&lt;p&gt;First, install the necessary NuGet packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# API versioning for controllers&lt;/span&gt;
dotnet add package Asp.Versioning.Mvc 

&lt;span class="c"&gt;# Adds support for exposing versioned API docs&lt;/span&gt;
dotnet add package Asp.Versioning.Mvc.ApiExplorer 

&lt;span class="c"&gt;# Swagger/OpenAPI generation&lt;/span&gt;
dotnet add package Swashbuckle.AspNetCore

&lt;span class="c"&gt;# Adds support for example responses in Swagger, we will use it to provide examples for `ProblemDetails` responses&lt;/span&gt;
dotnet add package Swashbuckle.AspNetCore.Filters

&lt;span class="c"&gt;# Optional: Use Scalar instead of Swagger UI for a better experience&lt;/span&gt;
dotnet add package Scalar.AspNetCore

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

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Asp.Versioning&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;BloggingExamples&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Scalar.AspNetCore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Swashbuckle.AspNetCore.Filters&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSwaggerGen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigureOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ConfigureSwaggerOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSwaggerExamplesFromAssemblyOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;NotFoundProblemDetailsExample&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApiVersioning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// adds api versioning and configures the strategy  &lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultApiVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ApiVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReportApiVersions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiVersionReader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UrlSegmentApiVersionReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
    &lt;span class="p"&gt;})&lt;/span&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMvc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// add support for versioning controllers   &lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApiExplorer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupNameFormat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"'v'VVV"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// e.g., v1  &lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SubstituteApiVersionInUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;});&lt;/span&gt;  
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddControllers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSwagger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapControllers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapScalarApiReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OpenApiRoutePattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/swagger/{documentName}/swagger.json"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"v1"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"v2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="p"&gt;});&lt;/span&gt;  

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

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;OrderController.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ApiVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ApiVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/v{v:apiVersion}/[controller]"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MapToApiVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"updateOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateOrder&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;OrderDto&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;updatedOrder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updatedOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MapToApiVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{orderId}/shipping"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status204NoContent&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProblemDetails&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status400BadRequest&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status404NotFound&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetShippingInfoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
    &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if you launch your API and visit &lt;code&gt;/swagger&lt;/code&gt;, you’ll see both &lt;strong&gt;v1&lt;/strong&gt; and &lt;strong&gt;v2&lt;/strong&gt; grouped cleanly — and instantly appreciate the differences:&lt;/p&gt;

&lt;p&gt;Let's take a look at v1 first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0z48fruv1zotb5lweo9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0z48fruv1zotb5lweo9.png" alt="scalar-v1" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is functional, but far from intuitive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No example values provided, everything is nullable abd you’re left guessing how to structure your request&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Errors like &lt;code&gt;400 Bad Request&lt;/code&gt; aren’t documented, leaving you blind to what might go wrong&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No clear separation between request and response — just a generic 200 OK&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It works, but it feels unpolished and leaves the developer with more questions than answers&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now lets take a look at v2 and compare them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fecr2knca5mrjhuuhueau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fecr2knca5mrjhuuhueau.png" alt="scalar-v2" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In v2 we can see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ready-to-run example values make it easy to try out the endpoint without guesswork&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All possible status codes are clearly documented, along with explanations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A structured &lt;code&gt;ProblemDetails&lt;/code&gt; schema shows exactly how errors are returned&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Clear separation between request and response models&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The UI communicates confidence — this feels like a production-grade API&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Improving your OpenAPI spec isn’t just about aesthetics — it’s about creating a &lt;strong&gt;developer experience&lt;/strong&gt; that feels reliable, guided, and easy to integrate with.&lt;/p&gt;

&lt;p&gt;By versioning your API and refining your endpoints, you instantly elevate the quality of your documentation. It becomes easier to test, easier to onboard new consumers, and easier to maintain over time.&lt;/p&gt;

&lt;p&gt;In this post, we saw how small changes — like switching from a vague &lt;code&gt;UpdateOrder&lt;/code&gt; to a specific &lt;code&gt;SetShippingInfo&lt;/code&gt; — dramatically improve how your API &lt;em&gt;looks&lt;/em&gt; and &lt;em&gt;feels&lt;/em&gt; in tools like Scalar.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you enjoyed this article visit &lt;a href="https://www.eventuallyconsistent.xyz/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt; for more.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>dotnet</category>
      <category>api</category>
      <category>swagger</category>
      <category>openapi</category>
    </item>
    <item>
      <title>Design Clean and Intuitive APIs</title>
      <dc:creator>Giannis Georgopoulos</dc:creator>
      <pubDate>Sat, 10 May 2025 16:50:22 +0000</pubDate>
      <link>https://dev.to/georgopoulosgiannis/building-production-grade-apis-in-net-part-1-design-clean-and-intuitive-apis-87g</link>
      <guid>https://dev.to/georgopoulosgiannis/building-production-grade-apis-in-net-part-1-design-clean-and-intuitive-apis-87g</guid>
      <description>&lt;p&gt;Let’s say you join a new team and find this in one of the core API controllers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"updateOrder"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;UpdateOrder&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;OrderDto&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;updatedOrder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updatedOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it seems reasonable. It compiles. It works. It even passes QA.&lt;br&gt;&lt;br&gt;
But if this looks fine to you — &lt;strong&gt;keep reading&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This kind of code is deceptively simple. In fact, it’s a perfect example of how &lt;strong&gt;bad design doesn’t look broken&lt;/strong&gt;, until it is. Under the surface, this endpoint is full of subtle flaws that will confuse consumers, pollute your Swagger docs, and slow your team down over time.&lt;/p&gt;

&lt;p&gt;Let’s unpack what’s wrong, and more importantly, how to fix it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What’s Wrong With This Endpoint?
&lt;/h2&gt;

&lt;p&gt;Let’s break it down:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Misused HTTP Verb
&lt;/h3&gt;

&lt;p&gt;You're updating an existing resource, not creating a new one. This should be a &lt;code&gt;PUT&lt;/code&gt; or &lt;code&gt;PATCH&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The verb might feel like a minor detail, but it shapes how clients understand and interact with your API. Misusing it breaks expectations and undermines REST semantics.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. CRUD Smell: The “updateEverything” Trap
&lt;/h3&gt;

&lt;p&gt;This is the biggest red flag. You're trying to model everything through &lt;strong&gt;CRUD&lt;/strong&gt;. That seems simple at first, but it becomes unmaintainable fast.&lt;/p&gt;

&lt;p&gt;Over time, you'll bolt on more logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Don’t allow canceling if already shipped.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don’t allow address changes if invoiced.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add this flag, but ignore it depending on status...&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ve now got a monster method trying to cover every edge case.&lt;/p&gt;

&lt;p&gt;Instead, shift toward &lt;strong&gt;task-based endpoints&lt;/strong&gt; that represent real user intent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;SetShippingAddress&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;CancelOrder&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;MarkOrderAsPaid&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each task has clear rules, clear inputs, and a clear domain boundary.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Highly recommended&lt;/strong&gt;: &lt;a href="https://codeopinion.com/decomposing-crud-to-a-task-based-ui/" rel="noopener noreferrer"&gt;Decomposing CRUD to a Task Based UI&lt;/a&gt; by CodeOpinion.&lt;br&gt;&lt;br&gt;
It’s one of the best resources out there for understanding &lt;strong&gt;task-based API design&lt;/strong&gt; and how to get out of the CRUD mindset.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  3. IActionResult: Flexibility That Hurts
&lt;/h3&gt;

&lt;p&gt;Returning &lt;code&gt;IActionResult&lt;/code&gt; may seem convenient, but it leads to vague Swagger documentation. Clients won’t know what to expect.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;ActionResult&amp;lt;T&amp;gt;&lt;/code&gt; to explicitly define your response types — it improves both clarity and tooling support.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Overloaded, Generic &lt;code&gt;OrderDto&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;What is &lt;code&gt;OrderDto&lt;/code&gt;? For whom is it meant? the database? The client? The update request? &lt;br&gt;
If it’s used for both input and output, it’ll end up looking something like this, in order to accommodate all use cases and request/responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderDto&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;AddressInfoDto&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;AddressInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;OrderSummaryDto&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;OrderSummary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;OrderLine&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;OrderLines&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderLine&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;LineId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;AddressInfoDto&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;RecipientName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Street&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;City&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ZipCode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  


    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Country&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderSummaryDto&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;TotalAmount&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Vat&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Should the client send this? Ignore it? What about &lt;code&gt;status&lt;/code&gt;, or &lt;code&gt;totalAmount&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;This leads to guessing, confusion, and coupling between internal models and external contracts.&lt;/p&gt;

&lt;p&gt;Define &lt;strong&gt;separate models&lt;/strong&gt; for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Request payloads (e.g. &lt;code&gt;SetShippingInfoRequest&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Responses (e.g. &lt;code&gt;ShippingInfoResponse&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. No Error Modeling
&lt;/h3&gt;

&lt;p&gt;What happens if the update fails? Say the order is already shipped and can’t be changed?&lt;/p&gt;

&lt;p&gt;Right now, you might return a plain &lt;code&gt;400 Bad Request&lt;/code&gt; — but that’s meaningless without context.&lt;br&gt;&lt;br&gt;
Clients will ask: &lt;em&gt;“What did I do wrong?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You must do &lt;strong&gt;two things&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Return a structured, predictable error response.&lt;/li&gt;
&lt;li&gt;Document &lt;strong&gt;why&lt;/strong&gt; a &lt;code&gt;400&lt;/code&gt; might happen.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProblemDetails&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status400BadRequest&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here are some valid &lt;code&gt;400&lt;/code&gt; scenarios you should document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The address fields are missing or invalid (e.g. empty &lt;code&gt;ZipCode&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The order has already been shipped and address changes are forbidden.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The order has been cancelled and is no longer editable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The country specified is unsupported.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Business rules prevent shipping updates once payment is finalized.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, clients know what to expect and how to recover.&lt;/p&gt;

&lt;p&gt;You can reflect this using the standard &lt;code&gt;ValidationProblemDetails&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://tools.ietf.org/html/rfc7231#section-6.5.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"One or more validation errors occurred."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ZipCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Zip code is required."&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Shipping information cannot be changed after shipment."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Swagger Docs Will Be Garbage
&lt;/h3&gt;

&lt;p&gt;Without XML comments and meaningful examples, Swagger will generate things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells consumers nothing. Which fields are required? What does a valid address look like?&lt;/p&gt;

&lt;p&gt;This is why we need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;XML comments&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Swagger examples&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Concrete request/response types&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A Better Design: Task-Based &lt;code&gt;SetShippingInfo&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Let’s redesign this endpoint with all the above in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  SetShippingInfo Endpoint
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;  &lt;/span&gt;
&lt;span class="c1"&gt;/// Sets the shipping address for an order.  &lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;  &lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;response code="400"&amp;gt;  &lt;/span&gt;
&lt;span class="c1"&gt;/// Returned when:  &lt;/span&gt;
&lt;span class="c1"&gt;/// - 'ZipCode' is missing or invalid  &lt;/span&gt;
&lt;span class="c1"&gt;/// - The order cannot be updated due to its current status  &lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/response&amp;gt;  &lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;MapToApiVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{orderId}/shipping"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status204NoContent&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ValidationProblemDetails&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status400BadRequest&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status404NotFound&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;SwaggerResponseExample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status400BadRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ZipCodeExample&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;  
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;SwaggerResponseExample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status404NotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotFoundProblemDetailsExample&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;  
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetShippingInfoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SetShippingInfoRequest
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Represents the request to set shipping information for an order.&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SetShippingInfoRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;/// &amp;lt;example&amp;gt;John Smith&amp;lt;/example&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RecipientName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;example&amp;gt;123 Main St&amp;lt;/example&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Street&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;example&amp;gt;New York&amp;lt;/example&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;City&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;example&amp;gt;NY&amp;lt;/example&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;example&amp;gt;10001&amp;lt;/example&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ZipCode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;/// &amp;lt;example&amp;gt;USA&amp;lt;/example&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Country&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ShippingInfoResponse
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// The response returned after successfully setting the shipping info.&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShippingInfoResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;RecipientName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// any other fields that could be needed for the response&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might use this model if you want to &lt;strong&gt;immediately return the updated shipping information&lt;/strong&gt; after the operation. This can be useful if the client needs confirmation of what’s now stored on the server.&lt;/p&gt;

&lt;p&gt;But ask yourself: &lt;em&gt;Is this really needed?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Since this is a &lt;code&gt;PATCH&lt;/code&gt;, and the client likely &lt;strong&gt;already knows what it sent&lt;/strong&gt;, returning the entire updated model may be wasteful, especially if the response adds no new value.&lt;/p&gt;

&lt;p&gt;In many cases, a better choice is to &lt;strong&gt;return &lt;code&gt;204 No Content&lt;/code&gt;&lt;/strong&gt;, which tells the client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The operation succeeded
&lt;/li&gt;
&lt;li&gt;There’s nothing more to say&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you choose that route, the controller action can be simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders/{orderId}/shipping"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status204NoContent&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ValidationErrorResponse&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status400BadRequest&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ProducesResponseType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status404NotFound&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromRoute&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;SetShippingInfoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetShippingInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Just because an API “works” doesn’t mean it’s well-designed.&lt;/li&gt;
&lt;li&gt;CRUD endpoints invite technical debt; use &lt;strong&gt;task-based designs&lt;/strong&gt; to model intent.&lt;/li&gt;
&lt;li&gt;Use precise verbs, structured models, and document your failure scenarios.&lt;/li&gt;
&lt;li&gt;Swagger is not just for machines; make it a first-class developer experience.&lt;/li&gt;
&lt;li&gt;Know when to return a rich response, and when to just say &lt;code&gt;204&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you enjoyed this article visit &lt;a href="https://www.eventuallyconsistent.xyz/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt; for more&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>dotnet</category>
      <category>webdev</category>
      <category>backenddevelopment</category>
      <category>api</category>
    </item>
    <item>
      <title>Safe Refactoring in .NET with Light/Dark Mode and Feature Flags</title>
      <dc:creator>Giannis Georgopoulos</dc:creator>
      <pubDate>Tue, 06 May 2025 16:16:27 +0000</pubDate>
      <link>https://dev.to/georgopoulosgiannis/safe-refactoring-in-net-with-lightdark-mode-and-feature-flags-1dil</link>
      <guid>https://dev.to/georgopoulosgiannis/safe-refactoring-in-net-with-lightdark-mode-and-feature-flags-1dil</guid>
      <description>&lt;p&gt;You’ve been forced to maintain a poorly written legacy app. Spaghetti code, no tests, and every new feature breaks two existing ones.&lt;/p&gt;

&lt;p&gt;Team morale is at rock bottom. New features take forever to ship. Regression bugs are constant.&lt;/p&gt;

&lt;p&gt;You gather your arguments and head to management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You&lt;/strong&gt;: We need time to refactor.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Management&lt;/strong&gt;: What are you talking about?&lt;br&gt;&lt;br&gt;
&lt;strong&gt;You&lt;/strong&gt;: Let us refactor. We’ll ship faster, with fewer bugs, and engineers won’t want to quit. I can do it.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Management&lt;/strong&gt;: ...Okay.&lt;/p&gt;

&lt;p&gt;Now comes the hard part.&lt;/p&gt;

&lt;p&gt;Are you going to deliver on your promises? Or end up behind schedule &lt;em&gt;and&lt;/em&gt; still stuck with a spaghetti codebase?&lt;/p&gt;

&lt;p&gt;While large-scale refactoring depends on many factors like team alignment, technical constraints, and timing; there’s one powerful technique I’ve used successfully in production to de-risk the process: &lt;strong&gt;Light/Dark Mode refactoring&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I first learned this technique in the book &lt;a href="https://www.oreilly.com/library/view/refactoring-at-scale/9781492085186/" rel="noopener noreferrer"&gt;&lt;em&gt;Refactoring at Scale&lt;/em&gt;&lt;/a&gt;, and I’ve since applied it many times in real systems to refactor code safely and confidently.&lt;/p&gt;

&lt;p&gt;In this post, I’ll show you how to implement it in .NET using feature flags, and how it can help you refactor without fear.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Is Light/Dark Mode Refactoring?
&lt;/h2&gt;

&lt;p&gt;Light/Dark Mode is a technique that lets you safely refactor code by running both the &lt;strong&gt;existing implementation&lt;/strong&gt; ("light") and the &lt;strong&gt;new refactored implementation&lt;/strong&gt; ("dark") side by side without exposing users to any risk.&lt;/p&gt;

&lt;p&gt;You run the light implementation as usual, return its result to the user, but also execute the dark version in the background and compare the two outputs. If they match, you know your refactor is likely safe. If they don’t, you’ve caught a mismatch before it hits production.&lt;/p&gt;

&lt;p&gt;This lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ship the refactor behind a feature flag&lt;/li&gt;
&lt;li&gt;Gain confidence in correctness by comparing real-world results&lt;/li&gt;
&lt;li&gt;Gradually roll out the new implementation without surprises&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, when you're confident, you flip a flag and start returning the dark version to users.&lt;/p&gt;

&lt;p&gt;It’s a low-risk, high-safety way to refactor logic, especially &lt;strong&gt;queries&lt;/strong&gt;, where the same inputs should lead to the same outputs.&lt;/p&gt;

&lt;p&gt;In the next section, I’ll show you how we applied this technique in a .NET application.&lt;/p&gt;

&lt;p&gt;Let’s take an example of a service that performs a moderately complex operation like calculating the final price of a product, taking into account discounts, tax rules, currency rounding, and maybe even user-specific pricing logic.&lt;/p&gt;

&lt;p&gt;Here’s how the legacy version might look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CalculateFinalPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_productRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BasePrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsSpecialUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;0.85m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Loyalty tier discount logic&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_userRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoyaltyTier&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Gold"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoyaltyTier&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Silver"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.05m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Regional tax policy (duplicated logic scattered in multiple places)&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"EU"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.20m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"US"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5.00m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// flat tax for some reason&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Random price adjustment based on product tags&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Clearance"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;3.00m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Last-minute adjustment for legacy reasons&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSubscription&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"EU"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="p"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;0.90m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;finalPrice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Round to two decimals in a questionable way&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finalPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MidpointRounding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToEven&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of logic that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Spreads business rules across services and layers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Has inconsistent handling (some percentage-based, some fixed)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Includes hardcoded exceptions and tribal knowledge&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is hard to test and reason about&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it is not mildly as complex as to what can be found in legacy apps.&lt;/p&gt;

&lt;p&gt;Now imagine you want to replace this with a cleaner, rules-driven &lt;code&gt;PricingEngine&lt;/code&gt; that encapsulates all this behavior in a composable, maintainable way.&lt;/p&gt;

&lt;p&gt;You can’t afford to flip the switch and hope it works.&lt;/p&gt;

&lt;p&gt;That’s where &lt;code&gt;ExecuteLightDark&lt;/code&gt; comes in. Let’s see how to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;ExecuteLightDark&lt;/code&gt; Helper
&lt;/h2&gt;

&lt;p&gt;Let’s start with the core idea: run both the legacy and the refactored implementations, compare their results, and return the trusted one.&lt;/p&gt;

&lt;p&gt;Here’s the simplest version of &lt;code&gt;ExecuteLightDark&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExecuteLightDark&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CallerMemberName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;callingFunc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lightResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;light&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;darkResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lightResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// default in case dark fails&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;darkResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lightSerialized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;darkSerialized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;darkResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightSerialized&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;darkSerialized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Light/dark results match for {func}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callingFunc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Mismatch in light/dark results \nLight: {light}\nDark: {dark}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lightSerialized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;darkSerialized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Dark execution failed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lightResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s a solid start for internal testing environments but it’s not good enough for production.&lt;/p&gt;

&lt;p&gt;In production, we need more control.&lt;/p&gt;

&lt;p&gt;What we really want is to first release the refactor in &lt;strong&gt;observation mode&lt;/strong&gt;: we return the light result and only log the comparison. Then, &lt;strong&gt;without redeploying the app&lt;/strong&gt;, we want the ability to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable Light/Dark execution dynamically&lt;/strong&gt; for a subset of users whether that’s a percentage, a specific user group, or internal beta testers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradually ramp up&lt;/strong&gt; the dark execution as we build confidence, while still falling back to the light result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control performance impact&lt;/strong&gt; by only executing the dark logic some percentage of the time (since running both can be expensive or slow).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To accomplish all of this, we need to bring in &lt;strong&gt;feature flags&lt;/strong&gt;—specifically, something like &lt;a href="https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-feature-flag-aspnet-core" rel="noopener noreferrer"&gt;Azure App Configuration with Feature Management&lt;/a&gt; if you’re on Azure.&lt;/p&gt;

&lt;p&gt;Let’s look at how we extended our &lt;code&gt;ExecuteLightDark&lt;/code&gt; method to support those production-grade capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making It Production-Ready with Feature Flags
&lt;/h2&gt;

&lt;p&gt;To use Light/Dark Mode safely in production, we wrapped it with feature flags. This lets us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn Light/Dark Mode on or off without redeploying&lt;/li&gt;
&lt;li&gt;Decide whether to return the light or dark result&lt;/li&gt;
&lt;li&gt;Only run the dark logic some percentage of the time (to reduce performance impact)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s our final &lt;code&gt;ExecuteLightDark&lt;/code&gt; implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExecuteLightDark&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;light&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CallerMemberName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;callingFunc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Check if we should run both implementations&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shouldRunDark&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;featureManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabledAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FeatureFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseLightDarkMode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lightTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;light&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;darkTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shouldRunDark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;darkTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Running light/dark comparison for {func}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callingFunc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;lightResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;lightTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;darkTask&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lightResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;darkResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lightResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;darkResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;darkTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lightSerialized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;darkSerialized&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;darkResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightSerialized&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;darkSerialized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Results match for {func}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callingFunc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Mismatch in light/dark results for {func}\nLight: {light}\nDark: {dark}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;callingFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lightSerialized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;darkSerialized&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Dark execution failed for {func}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callingFunc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;returnDark&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;featureManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsEnabledAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FeatureFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReturnDarkResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;returnDark&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;darkResult&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lightResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This version offloads all the logic like sampling, targeting, gradual rollout to your &lt;strong&gt;feature management system&lt;/strong&gt;, which can be configured to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Target internal users, specific companies, or beta testers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enable for X% of users&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use filters like country, environment, user ID, etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Result: Your &lt;code&gt;PricingService&lt;/code&gt; After Refactoring
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;ExecuteLightDark&lt;/code&gt; in place, here’s what your &lt;code&gt;PricingService&lt;/code&gt; might look like after adopting the pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PricingService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IProductRepository&lt;/span&gt; &lt;span class="n"&gt;productRepo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IDiscountService&lt;/span&gt; &lt;span class="n"&gt;discountService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ITaxCalculator&lt;/span&gt; &lt;span class="n"&gt;taxCalculator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IPricingEngine&lt;/span&gt; &lt;span class="n"&gt;newPricingEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PricingService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IFeatureManager&lt;/span&gt; &lt;span class="n"&gt;featureManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CalculateFinalPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ExecuteLightDark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CalculateOldPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CalculateNewPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CalculateOldPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;productRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BasePrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"4b2f58d0-a738-4f96-b9a3-f3f4f0c9515d"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;0.85m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;discountService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoyaltyTier&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"Gold"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.10m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"Silver"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.05m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0m&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Clearance"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="m"&gt;3.00m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"EU"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.20m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"US"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;5.00m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0m&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSubscription&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Region&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"EU"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="p"&gt;*=&lt;/span&gt; &lt;span class="m"&gt;0.90m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePrice&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MidpointRounding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToEven&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CalculateNewPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PricingContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;newPricingEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CalculateFinalPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benefits and Caveats
&lt;/h2&gt;

&lt;p&gt;Like any tool, Light/Dark Mode refactoring comes with trade-offs. But in the right context it’s incredibly powerful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confidence through real data&lt;/strong&gt;&lt;br&gt;
You’re testing the new logic against real production inputs, not just unit tests or pre-seeded environments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero-risk rollouts&lt;/strong&gt;&lt;br&gt;
You can ship and monitor the refactor without ever returning the dark result until you’re ready.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gradual, flexible rollout&lt;/strong&gt;&lt;br&gt;
Feature flags let you enable dark execution for internal users, specific companies, or small percentages of traffic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Easy reversion&lt;/strong&gt;&lt;br&gt;
If something goes wrong, just toggle the flag off, no rollback or hotfix needed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Caveats
&lt;/h3&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance cost&lt;/strong&gt;&lt;br&gt;
You’re potentially doubling the work by running two implementations. This is especially relevant if queries are heavy or slow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Comparison accuracy&lt;/strong&gt;&lt;br&gt;
Serializing and comparing objects can fail due to field order, time precision, or non-deterministic values. Consider using custom comparers or domain-specific checks if needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log noise&lt;/strong&gt;&lt;br&gt;
If your dark implementation is still evolving, early mismatches may flood your logs. You may want to filter, throttle, or alert only on critical deltas.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not great for writes&lt;/strong&gt;&lt;br&gt;
This pattern doesn’t apply cleanly to commands or state changes, where running two implementations could mutate data differently.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;Use Light/Dark Mode for &lt;strong&gt;complex refactors of query logic&lt;/strong&gt;, especially when the stakes are high and confidence is low. It gives you a way to ship change safely, with real-time validation and full control over risk.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was originally posted at my &lt;a href="https://www.eventuallyconsistent.xyz" rel="noopener noreferrer"&gt;blog&lt;/a&gt;, visit for more technical posts around .NET.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>dotnet</category>
      <category>refactoring</category>
      <category>softwareengineering</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>Level Up Your Integration Tests in .NET: Record, Replay, Relax</title>
      <dc:creator>Giannis Georgopoulos</dc:creator>
      <pubDate>Mon, 05 May 2025 07:04:41 +0000</pubDate>
      <link>https://dev.to/georgopoulosgiannis/level-up-your-integration-tests-in-net-record-replay-relax-2m5c</link>
      <guid>https://dev.to/georgopoulosgiannis/level-up-your-integration-tests-in-net-record-replay-relax-2m5c</guid>
      <description>&lt;h3&gt;
  
  
  Sick of flaky integration tests?
&lt;/h3&gt;

&lt;p&gt;You run your tests once — they pass. Run them again — they fail. Maybe the third-party API timed out. Or the response changed. Or your internet blinked. Integration tests should give you confidence, not stress.&lt;/p&gt;

&lt;p&gt;That’s where &lt;strong&gt;deterministic integration testing&lt;/strong&gt; comes in. Imagine recording your API interactions once and replaying them forever. Offline, fast, and predictable.&lt;/p&gt;

&lt;p&gt;No network. No surprises.&lt;/p&gt;

&lt;p&gt;That’s exactly what &lt;a href="https://github.com/GeorgopoulosGiannis/Vcr.HttpRecorder" rel="noopener noreferrer"&gt;&lt;code&gt;Vcr.HttpRecorder&lt;/code&gt;&lt;/a&gt; does.&lt;/p&gt;

&lt;p&gt;It’s inspired by the original Ruby VCR gem, revived for .NET and brought back to life with new features and bug fixes in &lt;a href="https://github.com/GeorgopoulosGiannis/Vcr.HttpRecorder" rel="noopener noreferrer"&gt;my fork&lt;/a&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What Is Deterministic Integration Testing?
&lt;/h3&gt;

&lt;p&gt;In traditional integration tests, you call real services. Maybe a payment gateway, an external API, or even another microservice. But real services are unpredictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;They might be slow or down.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Their responses might change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They could rate-limit you.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes your tests flaky and slow. Worse, you can’t run them offline, and reproducing issues becomes a headache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic integration testing&lt;/strong&gt; solves this by recording the HTTP interactions during a real test run, just once, and saving them. On subsequent runs, instead of hitting the real service, it replays the recorded response. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your tests always get the same response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They run faster.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can run them offline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They’re &lt;em&gt;finally&lt;/em&gt; reliable.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One real run to record&lt;br&gt;&lt;br&gt;
All future runs are just playback&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This gives you &lt;strong&gt;the confidence of integration testing&lt;/strong&gt; with &lt;strong&gt;much more speed and reliability, though not quite as fast as unit tests, it’s a massive improvement&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Not Just Use WireMock?
&lt;/h3&gt;

&lt;p&gt;If you've done any HTTP-based integration testing in .NET, you've probably run into tools like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WireMock.Net&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MockHttpMessageHandler&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TestServers&lt;/strong&gt; and custom in-memory APIs&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They’re powerful but often overkill. You have to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Define expected requests manually&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mock responses yourself&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep your mocks in sync with reality&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's easy to end up testing &lt;em&gt;your mocks&lt;/em&gt; instead of the real integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vcr.HttpRecorder flips this around.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You &lt;strong&gt;run the test once&lt;/strong&gt;, and it records the actual request and response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No need to handcraft mocks or fake responses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next runs just replay that real interaction. Safely and deterministically.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can plug it into a specific &lt;code&gt;HttpClient&lt;/code&gt; with a simple handler — &lt;em&gt;or&lt;/em&gt; skip the plumbing entirely and have it &lt;strong&gt;automatically intercept all HTTP clients&lt;/strong&gt; in your test environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Started (The Easy Way)
&lt;/h3&gt;

&lt;p&gt;The easiest way to use &lt;code&gt;Vcr.HttpRecorder&lt;/code&gt; is to wrap your test in a recording context and let it &lt;strong&gt;automatically intercept all HTTP clients&lt;/strong&gt; in your test environment. No manual plumbing, no special &lt;code&gt;HttpClient&lt;/code&gt; setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpRecorderContext&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpRecorderConfiguration&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HttpRecorderMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Automatically records or replays&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webAppFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithWebHostBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureTestServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpRecorderContextSupport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That’s it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;On the &lt;strong&gt;first run&lt;/strong&gt;, it records real HTTP interactions into cassette files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On &lt;strong&gt;subsequent runs&lt;/strong&gt;, it replays them. Fast and offline.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cassette files(.har) are stored next to your tests by default and can be versioned with your repo. Want to update them? Just delete and re-record.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prefer to wire it up manually? You can still use &lt;code&gt;HttpRecorderHandler&lt;/code&gt; directly with specific clients, but most of the time, the automatic context is all you need.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What’s New in My Fork
&lt;/h3&gt;

&lt;p&gt;The original &lt;code&gt;Vcr.HttpRecorder&lt;/code&gt; was a solid idea, but it hadn’t been maintained and i got no responses from maintainers. I forked it to fix bugs, modernize it, and make it practical for real-world integration testing.&lt;/p&gt;

&lt;p&gt;Here’s what I’ve added so far:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrent context support&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You can now run tests in parallel using &lt;code&gt;HttpRecorderConcurrentContext&lt;/code&gt;, and each test will get its own isolated recording scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.NET 6/7/8 compatibility&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Fixed support for newer .NET APIs like &lt;code&gt;HttpClient.PostAsJsonAsync&lt;/code&gt;, &lt;code&gt;GetFromJsonAsync&lt;/code&gt;, and others that were previously breaking the recorder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support for non-UTF8 responses&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The original version assumed all responses were UTF-8, which caused crashes for binary or other encoded payloads. That’s now fixed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coming Soon
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;**Cassette diffing&lt;/em&gt;* to understand what changed between recordings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional assertions&lt;/strong&gt; on captured requests/responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m building this based on actual needs from real-world projects. If you’ve got an idea or run into an edge case, open an issue or let’s talk!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/GeorgopoulosGiannis/Vcr.HttpRecorder" rel="noopener noreferrer"&gt;Check out the repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give it a star if it helped !&lt;/p&gt;

&lt;p&gt;This post was originally posted at my &lt;a href="https://www.eventuallyconsistent.xyz/level-up-your-integration-tests" rel="noopener noreferrer"&gt;blog&lt;/a&gt;, visit for more technical posts around .NET.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>dotnet</category>
      <category>testing</category>
      <category>softwareengineering</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building "Production-Grade" APIs in .NET</title>
      <dc:creator>Giannis Georgopoulos</dc:creator>
      <pubDate>Sun, 04 May 2025 10:49:26 +0000</pubDate>
      <link>https://dev.to/georgopoulosgiannis/building-production-grade-apis-in-net-3813</link>
      <guid>https://dev.to/georgopoulosgiannis/building-production-grade-apis-in-net-3813</guid>
      <description>&lt;p&gt;Many engineers build and deploy APIs into production.&lt;br&gt;&lt;br&gt;
So we have an API &lt;em&gt;running in production&lt;/em&gt;. Does that mean it’s truly &lt;strong&gt;production-grade&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;More often than not, the answer is &lt;strong&gt;no&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We write the code, test it locally (usually alone, on one machine, with one user), and proudly tell the business, &lt;em&gt;"Hey, it’s ready!"&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Maybe there’s even a QA environment where someone from product gives it a quick click-through and confirms, &lt;em&gt;"Looks good to me!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And then... reality checks in.&lt;/p&gt;

&lt;p&gt;You get a call on the weekend:&lt;br&gt;&lt;br&gt;
“Users can’t log in.”&lt;br&gt;&lt;br&gt;
Or worse:&lt;br&gt;&lt;br&gt;
“A customer placed an order, and it’s gone.”&lt;/p&gt;

&lt;p&gt;Now you're scrambling, thinking:&lt;br&gt;&lt;br&gt;
&lt;em&gt;“I wish I’d added logs there.”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;“Why didn’t we catch this earlier?”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;“How are we supposed to debug this in production?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If that scenario feels familiar, this post is for you.&lt;/p&gt;

&lt;p&gt;Let’s walk through what I consider the &lt;strong&gt;minimum bar&lt;/strong&gt; for an API to be truly &lt;strong&gt;production-grade&lt;/strong&gt;, and how to build one in .NET.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Qualifies as “Production-Grade”?
&lt;/h2&gt;

&lt;p&gt;Most APIs “work” during development. But &lt;strong&gt;production-grade&lt;/strong&gt; APIs are designed to behave predictably under pressure; when traffic spikes, something fails, or a customer is relying on your system to do its job.&lt;/p&gt;

&lt;p&gt;For me, production-grade doesn’t mean “it passes QA”, it means the system is built in a way that makes &lt;strong&gt;life easier for engineers and consumers&lt;/strong&gt; alike.&lt;/p&gt;

&lt;p&gt;It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The API is &lt;strong&gt;intuitive&lt;/strong&gt; to use. Consumers don’t have to ask how it works. They don’t open tickets asking why they got a &lt;code&gt;500&lt;/code&gt;. It’s predictable, self-documenting, and consistent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It’s &lt;strong&gt;observable&lt;/strong&gt;. You can tell what the system is doing, when it fails, and &lt;em&gt;why&lt;/em&gt;. You find out something’s wrong &lt;strong&gt;before&lt;/strong&gt; your customers do; no surprise calls at 10 PM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It’s &lt;strong&gt;resilient&lt;/strong&gt;. Every API that handles load will eventually fail; so you build in circuit breakers, retries, and rate limits &lt;strong&gt;before&lt;/strong&gt; you need them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It’s &lt;strong&gt;secure by default&lt;/strong&gt;. If your API is public, someone will try to break it. Don’t assume good will from your users. Protect your business. Especially in B2B software where one leak or exploit can damage your reputation permanently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It’s &lt;strong&gt;safe&lt;/strong&gt; for your users. No user data in logs. No stack traces exposed. If you break that trust, users will hate you, and you may land in legal trouble, too.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It’s &lt;strong&gt;automated and testable&lt;/strong&gt;. You don’t publish from your machine. Deployment should be a repeatable, automated pipeline with proper testing. Even if your business doesn’t release multiple times per day (like in medical or aerospace), you still benefit from CI/CD; because you always have &lt;strong&gt;a shippable, tested build&lt;/strong&gt; and fast feedback loops.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A "Production-Ready" Framework to Follow
&lt;/h2&gt;

&lt;p&gt;This mindset can be distilled into five key areas. Not as a checklist, but as a framework for designing APIs that last beyond dev and QA.&lt;/p&gt;

&lt;p&gt;That’s why I break it down into five core areas. A framework I use every time I design an API I expect to survive real-world use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9lfb8ofodni57tvwy7rt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9lfb8ofodni57tvwy7rt.png" alt="Core Areas of Production Grade APIs" width="480" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Developer Experience &amp;amp; Documentation
&lt;/h3&gt;

&lt;p&gt;Even the most reliable API will fail if it’s confusing to consume. Clean routes, consistent behavior, meaningful errors, versioning, and good documentation are essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Observability &amp;amp; Diagnostics
&lt;/h3&gt;

&lt;p&gt;Structured logging, traces, metrics, and health checks let you understand what’s happening in production, and debug issues when they inevitably arise.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Resilience &amp;amp; Stability
&lt;/h3&gt;

&lt;p&gt;Your API must handle real-world failures: retries, circuit breakers, timeouts, fallback strategies, and rate limiting help it stay functional under stress.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.Security &amp;amp; Safety
&lt;/h3&gt;

&lt;p&gt;Authentication, authorization, input validation, exception handling, and proper error reporting. All critical to protect user data and prevent abuse.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Deployment &amp;amp; Automation
&lt;/h3&gt;

&lt;p&gt;CI/CD pipelines, infrastructure as code, testing pyramid, smoke and load testing, and safe deployment practices (e.g., blue/green, canary). These ensure your API can be updated often without fear.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This post is part of my blog series: Building Production-Grade APIs in .NET.&lt;/em&gt;  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you enjoyed this article visit &lt;a href="https://www.eventuallyconsistent.xyz/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt; for more&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>api</category>
      <category>softwareengineering</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
