<?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: Anton Martyniuk</title>
    <description>The latest articles on DEV Community by Anton Martyniuk (@antonmartyniuk).</description>
    <link>https://dev.to/antonmartyniuk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1436921%2F88573c9a-3366-4969-9d21-ff47a6bd5c75.png</url>
      <title>DEV Community: Anton Martyniuk</title>
      <link>https://dev.to/antonmartyniuk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/antonmartyniuk"/>
    <language>en</language>
    <item>
      <title>All Types of Caching in .NET: When to Use Each (Deep Dive, Part 2)</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 03 Nov 2026 12:07:40 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/all-types-of-caching-in-net-when-to-use-each-deep-dive-part-2-3pbc</link>
      <guid>https://dev.to/antonmartyniuk/all-types-of-caching-in-net-when-to-use-each-deep-dive-part-2-3pbc</guid>
      <description>&lt;p&gt;In .NET, you have 6 ways to cache: IMemoryCache, IDistributedCache, Redis, HybridCache, FusionCache, and Output Cache.&lt;/p&gt;

&lt;p&gt;This is the 2nd part of the Caching in .NET series.&lt;/p&gt;

&lt;p&gt;Last week, in &lt;a href="https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part1" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; we explored:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where Each Cache Sits in a .NET App&lt;/li&gt;
&lt;li&gt;In-Memory Cache (IMemoryCache) (Part 1)&lt;/li&gt;
&lt;li&gt;Distributed Cache (IDistributedCache) with Redis (Part 1)&lt;/li&gt;
&lt;li&gt;HybridCache (Part 1)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, we will explore each caching option with pros, cons, pitfalls, and when to use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FusionCache&lt;/li&gt;
&lt;li&gt;Output Cache&lt;/li&gt;
&lt;li&gt;Which Cache Should You Choose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part2" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part2&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FusionCache
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ZiggyCreatures/FusionCache" rel="noopener noreferrer"&gt;FusionCache&lt;/a&gt; is an open-source hybrid cache that existed years before HybridCache did.&lt;/p&gt;

&lt;p&gt;It has the same L1 + L2 shape and the same stampede protection.&lt;br&gt;
It also has what HybridCache lacks: a backplane that syncs L1 across instances, fail-safe, factory timeouts, and eager refresh.&lt;/p&gt;

&lt;p&gt;FusionCache is the best caching option available in .NET today.&lt;/p&gt;

&lt;p&gt;To get started with FusionCache, add these packages:&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 ZiggyCreatures.FusionCache
dotnet add package ZiggyCreatures.FusionCache.Serialization.SystemTextJson
dotnet add package ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can configure it in the DI container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;redisConnection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Redis"&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;AddFusionCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDefaultEntryOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;FusionCacheEntryOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;IsFailSafeEnabled&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;FailSafeMaxDuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&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;FactorySoftTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;300&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;WithSerializer&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;FusionCacheSystemTextJsonSerializer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDistributedCache&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;RedisCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RedisCacheOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisConnection&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithBackplane&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;RedisBackplane&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RedisBackplaneOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisConnection&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three building blocks are registered here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Serializer&lt;/strong&gt; for the L2 payload. System.Text.Json is the default choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed cache&lt;/strong&gt; as L2. This is the same &lt;code&gt;RedisCache&lt;/code&gt; type from &lt;code&gt;IDistributedCache&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backplane&lt;/strong&gt; as the channel that tells other instances about changes. It uses &lt;a href="https://redis.io/docs/latest/develop/use-cases/pub-sub/dotnet/" rel="noopener noreferrer"&gt;Redis pub/sub&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The handler uses &lt;code&gt;IFusionCache&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;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetShipmentByNumberHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ShipmentsDbContext&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IFusionCache&lt;/span&gt; &lt;span class="n"&gt;cache&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;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;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetOrSetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"shipment:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;async&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;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapToResponse&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&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="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"shipments"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;You can remove items by key or a tag, exactly like in HybridCache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RemoveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"shipment:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is what happens after that call on the other app instances:&lt;/p&gt;

&lt;p&gt;The backplane message carries only the key and the action without a value.&lt;br&gt;
Instance B drops its L1 (memory) copy, and the next read on B goes to L2 (Redis) and then to the database.&lt;/p&gt;

&lt;p&gt;This is the direct fix for the biggest pitfall in HybridCache.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using FusionCache as HybridCache
&lt;/h3&gt;

&lt;p&gt;If your handlers already inject HybridCache, you don't have to rewrite them.&lt;br&gt;
FusionCache can register itself as the HybridCache 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="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;AddFusionCache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithSerializer&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;FusionCacheSystemTextJsonSerializer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDistributedCache&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;RedisCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RedisCacheOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisConnection&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithBackplane&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;RedisBackplane&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RedisBackplaneOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisConnection&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsHybridCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you continue using HybridCache in your classes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: through the HybridCache adapter you can only pass &lt;code&gt;HybridCacheEntryOptions&lt;/code&gt; per call. Fail-safe, timeouts, and eager refresh are configured once in the default entry options.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What Else FusionCache Gives You
&lt;/h3&gt;

&lt;p&gt;I will only name the features here, since this library deserves its own article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail-safe:&lt;/strong&gt; when the database is down and the factory throws, FusionCache serves the last known value instead of an error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft and hard timeouts:&lt;/strong&gt; if the factory takes longer than the soft timeout and a stale value exists, the stale value is returned and the factory keeps running in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eager refresh:&lt;/strong&gt; refresh an entry in the background before it expires, so hot keys never see a miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive caching:&lt;/strong&gt; the factory can set the TTL per entry based on what it loaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;FusionCache has the following advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backplane:&lt;/strong&gt; L1 caches stay in sync across instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience features:&lt;/strong&gt; fail-safe and timeouts keep your API answering when dependencies are slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drop-in for HybridCache:&lt;/strong&gt; the &lt;code&gt;AsHybridCache()&lt;/code&gt; adapter keeps your handlers on the Microsoft abstraction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mature:&lt;/strong&gt; in production since 2021, well documented, active maintainer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Here are the drawbacks of FusionCache:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Third-party dependency:&lt;/strong&gt; one more package to track and upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bigger configuration surface:&lt;/strong&gt; more options means more ways to misconfigure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backplane needs Redis pub/sub:&lt;/strong&gt; a managed Redis tier without pub/sub cannot run it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Output Cache
&lt;/h2&gt;

&lt;p&gt;Output Cache is middleware that stores the full HTTP response and serves it on the next matching request.&lt;/p&gt;

&lt;p&gt;On a hit, your handler and your database never run.&lt;br&gt;
This is the biggest win per line of code in ASP .NET Core, and I wrote a &lt;a href="https://antondevtips.com/blog/aspnetcore-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis" rel="noopener noreferrer"&gt;dedicated deep dive on Output Cache&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;During registration, you define named policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOutputCache&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="nf"&gt;AddPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipment-by-number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetVaryByRouteValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipmentNumber"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipments"&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;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;UseOutputCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the endpoint can use it by name:&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/shipments/{shipmentNumber}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GetShipmentByNumberHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;shipment&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&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;CacheOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipment-by-number"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SetVaryByRouteValue&lt;/code&gt; makes each shipment number its own cache entry.&lt;br&gt;
For list endpoints, &lt;code&gt;SetVaryByQuery("carrier", "status")&lt;/code&gt; does the same for query parameters.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;Tag("shipments")&lt;/code&gt; to quickly evict items from a cache.&lt;br&gt;
You can inject &lt;code&gt;IOutputCacheStore&lt;/code&gt; and evict by tag:&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;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateShipmentStatusHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ShipmentsDbContext&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IOutputCacheStore&lt;/span&gt; &lt;span class="n"&gt;outputCache&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;shipment&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;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpdateStatus&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;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&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;outputCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EvictByTagAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;By default, Output Cache stores responses in memory, which brings back the two-instances problem.&lt;br&gt;
For such cases, you will need Redis:&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 Microsoft.AspNetCore.OutputCaching.StackExchangeRedis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="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;AddStackExchangeRedisOutputCache&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;Configuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Redis"&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 every instance serves from the same cached responses, and &lt;code&gt;EvictByTagAsync&lt;/code&gt; on one instance evicts for all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important note:&lt;/strong&gt; ASP .NET Core also has the older Response Caching middleware (&lt;code&gt;UseResponseCaching&lt;/code&gt;), which relies on &lt;code&gt;Cache-Control&lt;/code&gt; headers sent by the client.&lt;br&gt;
It is outdated for APIs. Clients can bypass it, you cannot evict by tag, and it does not support Redis.&lt;br&gt;
Use Output Cache instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output Cache has the following advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing runs on a hit:&lt;/strong&gt; no handler, no serialization, no database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative:&lt;/strong&gt; one attribute-like call per endpoint, policies in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag-based eviction&lt;/strong&gt; that works across instances with the Redis store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Here are the drawbacks of Output Cache:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whole-response granularity:&lt;/strong&gt; you cache the entire payload or nothing. One changed field invalidates everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only GET and HEAD&lt;/strong&gt;, and only 200 responses, by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authenticated requests are not cached&lt;/strong&gt; by default. Requests with an &lt;code&gt;Authorization&lt;/code&gt; header skip the cache entirely.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Output Cache Pitfalls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Caching per-user data without varying by user.&lt;/strong&gt;&lt;br&gt;
If you override the default and cache authenticated responses, you must vary by the user, or user A receives user B's shipments:&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"my-shipments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetVaryByHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipments"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Varying by the &lt;code&gt;Authorization&lt;/code&gt; header works, but every token is its own entry, so the hit rate drops as the user base grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eviction lives in the write path.&lt;/strong&gt;&lt;br&gt;
Every handler that changes shipments must remember to evict the tag; otherwise, stale responses persist until they expire.&lt;br&gt;
Put eviction in one place, such as a domain event handler, rather than in each write handler.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part2" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part2&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>How to Optimize SQL Queries: 20 Proven Best Practices</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 30 Oct 2026 09:10:33 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-optimize-sql-queries-20-proven-best-practices-4n6e</link>
      <guid>https://dev.to/antonmartyniuk/how-to-optimize-sql-queries-20-proven-best-practices-4n6e</guid>
      <description>&lt;p&gt;A slow SQL query is one of the easiest ways to ruin a fast application.&lt;/p&gt;

&lt;p&gt;You can have a clean architecture, a great cache, and a powerful server - and still watch a page hang because one query scans a million rows without an index.&lt;/p&gt;

&lt;p&gt;Over the years, I have optimized hundreds of SQL queries across many production systems.&lt;/p&gt;

&lt;p&gt;Most of the wins come from the same small set of techniques, applied again and again.&lt;/p&gt;

&lt;p&gt;Some of them are obvious.&lt;br&gt;
Some of them go against advice you have probably heard.&lt;/p&gt;

&lt;p&gt;To make them easy to remember, I have grouped 20 proven techniques into 6 themes you can reach for whenever a query gets slow.&lt;/p&gt;

&lt;p&gt;In this post, we will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write index-friendly (sargable) queries&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Use indexes wisely&lt;/li&gt;
&lt;li&gt;Avoid functions on columns in WHERE&lt;/li&gt;
&lt;li&gt;Avoid leading wildcards in LIKE&lt;/li&gt;
&lt;li&gt;Match data types exactly&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetch only the data you need&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Stop using SELECT *&lt;/li&gt;
&lt;li&gt;Filter early&lt;/li&gt;
&lt;li&gt;Paginate with Keyset instead of OFFSET&lt;/li&gt;
&lt;li&gt;Query only what changed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplify joins and subqueries&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Reduce JOIN complexity&lt;/li&gt;
&lt;li&gt;Choose the right JOIN type&lt;/li&gt;
&lt;li&gt;Replace redundant subqueries with JOINs or CTEs&lt;/li&gt;
&lt;li&gt;Prefer EXISTS over IN&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design your schema for reads&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Normalize wisely&lt;/li&gt;
&lt;li&gt;Use materialized views&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make writes and transactions efficient&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Batch large operations&lt;/li&gt;
&lt;li&gt;Keep transactions short&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the database help you&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Read the execution plan&lt;/li&gt;
&lt;li&gt;Keep statistics up to date&lt;/li&gt;
&lt;li&gt;Use query hints sparingly&lt;/li&gt;
&lt;li&gt;Monitor and tune continuously&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All queries in this post were tested on the PostgreSQL database. The same ideas apply to other databases, though the exact syntax may differ.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-optimize-sql-queries-20-proven-best-practices" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-optimize-sql-queries-20-proven-best-practices&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Write Index-Friendly (Sargable) Queries
&lt;/h2&gt;

&lt;p&gt;An index only helps if your query lets the database use it.&lt;/p&gt;

&lt;p&gt;A "sargable" query (from Search ARGument ABLE) is one where the database can jump straight to the rows it needs through an index, instead of scanning the whole table.&lt;br&gt;
The four techniques below keep your queries sargable.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Use Indexes Wisely
&lt;/h3&gt;

&lt;p&gt;Indexes are the single biggest lever for read performance.&lt;/p&gt;

&lt;p&gt;An index is a sorted data structure that lets the database find rows without scanning the entire table, much like the index at the back of a book saves you from reading every page.&lt;/p&gt;

&lt;p&gt;Create indexes on the columns you filter, join, sort, and group by most often - the columns in your &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;, and &lt;code&gt;GROUP BY&lt;/code&gt; clauses.&lt;/p&gt;

&lt;p&gt;When several columns are used together, a single composite index that covers them is far better than separate single-column indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- A composite index for frequent filters on status and order date&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_status_order_date&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This index speeds up queries that filter on &lt;code&gt;status&lt;/code&gt;, and on &lt;code&gt;status&lt;/code&gt; together with &lt;code&gt;order_date&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Column order matters: an index on &lt;code&gt;(status, order_date)&lt;/code&gt; helps queries that filter by &lt;code&gt;status&lt;/code&gt; first, but not queries that filter by &lt;code&gt;order_date&lt;/code&gt; alone.&lt;/p&gt;

&lt;p&gt;You can also create a covering index that stores extra column values inside the index.&lt;br&gt;
This helps when a query only needs columns available in the index, so the database can avoid reading the actual table rows.&lt;/p&gt;

&lt;p&gt;It is especially useful for small, frequent lookup queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Cover a common read query without fetching rows from the table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_status_order_date_covering&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: indexes are not free. Every index must be updated on each insert, update, and delete, and it takes disk space. Index the columns your queries actually use, not every column.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a deeper look at index types and trade-offs, see &lt;a href="https://antondevtips.com/blog/optimizing-sql-performance-with-indexing-strategies-for-faster-queries" rel="noopener noreferrer"&gt;Optimizing SQL Performance with Indexing Strategies&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Avoid Functions on Columns in WHERE
&lt;/h3&gt;

&lt;p&gt;Wrapping a column in a function is one of the most common ways to accidentally disable an index.&lt;/p&gt;

&lt;p&gt;When you call a function on a column, the database has to compute that function for every row before it can compare it, so it cannot use the index on the raw column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Bad: the function on order_date prevents an index scan&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;EXTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;YEAR&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rewrite the condition to compare the raw column against a range. This keeps the query sargable and lets the index do its job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Good: a sargable range over the raw column&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="s1"&gt;'2026-01-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both queries return the same rows, but only the second can use an index on &lt;code&gt;order_date&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same rule applies to &lt;code&gt;LOWER(email)&lt;/code&gt;, &lt;code&gt;CAST(...)&lt;/code&gt;, and arithmetic on a column.&lt;br&gt;
If you often need to filter on a computed value, create an expression index for that exact expression instead.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Avoid Leading Wildcards in LIKE
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;LIKE&lt;/code&gt; pattern that starts with a wildcard cannot use a normal index.&lt;/p&gt;

&lt;p&gt;The database reads an index from left to right, so it needs to know the beginning of the value.&lt;br&gt;
A pattern like &lt;code&gt;'%son'&lt;/code&gt; hides the beginning and forces a full scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Bad: leading wildcard forces a full table scan&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%son'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Good: a known prefix allows an index range scan&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Anders%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you genuinely need to search inside text - a "contains" search - a normal B-tree index will not help.&lt;/p&gt;

&lt;p&gt;Reach for full-text search or a trigram index (the &lt;code&gt;pg_trgm&lt;/code&gt; extension in PostgreSQL) built for that job.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Match Data Types Exactly
&lt;/h3&gt;

&lt;p&gt;Comparing two different data types forces the database to convert one of them, and that conversion can silently disable an index.&lt;/p&gt;

&lt;p&gt;If a column is an &lt;code&gt;integer&lt;/code&gt; but you compare it to a string, or you join an &lt;code&gt;integer&lt;/code&gt; key to a &lt;code&gt;bigint&lt;/code&gt; key, the database adds an implicit cast - and the index on the original column may be skipped.&lt;/p&gt;

&lt;p&gt;Keep the types identical on both sides of every join and filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;log_id&lt;/span&gt;     &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_date&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;    &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define &lt;code&gt;logs.user_id&lt;/code&gt; to match the type of &lt;code&gt;users.id&lt;/code&gt; so the join uses the index on both sides.&lt;/p&gt;

&lt;p&gt;Matching types is a small detail at design time that saves you from a confusing, hard-to-spot scan later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetch Only the Data You Need
&lt;/h2&gt;

&lt;p&gt;The fastest data to process is the data you never read.&lt;/p&gt;

&lt;p&gt;Every column and every row you pull costs disk I/O, memory, and network time. These four techniques shrink the result set as early and as much as possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Stop Using SELECT *
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SELECT *&lt;/code&gt; pulls every column, including the ones you do not need.&lt;/p&gt;

&lt;p&gt;That means more data to read from disk, more to send over the network, and more memory to hold - all for columns your code throws away.&lt;/p&gt;

&lt;p&gt;It also blocks covering indexes, where the index alone can answer the query without touching the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Bad: fetches every column&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Good: only the columns you use&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listing columns explicitly is also safer. Your query will not silently change shape or break when someone adds or reorders a column.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Filter Early
&lt;/h3&gt;

&lt;p&gt;The smaller the dataset you work with, the faster everything downstream becomes.&lt;/p&gt;

&lt;p&gt;You may have heard the common advice:&lt;/p&gt;

&lt;p&gt;Apply your most selective filters to cut the number of rows before joins and aggregations have to process them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;  &lt;span class="s1"&gt;'2025-02-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; in most cases you do not have to place these filters by hand.&lt;br&gt;
A modern cost-based planner already pushes predicates down - it applies your &lt;code&gt;WHERE&lt;/code&gt; conditions as early as it can and reorders joins on its own.&lt;br&gt;
Rewriting the textual order of a &lt;code&gt;WHERE&lt;/code&gt; clause rarely changes the plan.&lt;/p&gt;

&lt;p&gt;What actually helps is giving the planner a selective, sargable filter and an index to apply it with.&lt;br&gt;
So focus on making the filter index-friendly, not on where you type it.&lt;/p&gt;
&lt;h3&gt;
  
  
  7. Paginate with Keyset instead of OFFSET - When you can
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;OFFSET&lt;/code&gt; looks like an easy way to implement pagination, but it gets slower the deeper you go.&lt;/p&gt;

&lt;p&gt;To return page 1,000, the database still reads and throws away every row before it.&lt;br&gt;
&lt;code&gt;OFFSET 100000&lt;/code&gt; means reading 100,000 rows just to skip them.&lt;/p&gt;

&lt;p&gt;Keyset pagination (also called seek pagination) remembers the last value you saw and seeks straight past it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Key-based pagination on an indexed column&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each page costs the same, whether it is page 2 or page 2,000, because the index jumps directly to &lt;code&gt;order_id &amp;gt; 1000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trade-off: keyset pagination gives you fast next- and previous-page navigation, but you can't implement random jumps to any page number.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Query Only What Changed
&lt;/h3&gt;

&lt;p&gt;Re-reading an entire table on every run is wasteful when only a few rows have changed.&lt;/p&gt;

&lt;p&gt;Track a watermark - the last point you processed - and fetch only the rows newer than it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fetch only records updated since the last run&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;modified_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2025-05-20 00:00'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This incremental pattern turns a full-table scan into a small, indexed range read.&lt;/p&gt;

&lt;p&gt;Index &lt;code&gt;modified_date&lt;/code&gt;, store the new high-water mark after each run, and your sync job stays fast even as the table grows.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-optimize-sql-queries-20-proven-best-practices" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-optimize-sql-queries-20-proven-best-practices&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>sql</category>
    </item>
    <item>
      <title>EF Core and Azure SQL Database: A Step-by-Step Guide</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Thu, 29 Oct 2026 13:36:27 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/ef-core-and-azure-sql-database-a-step-by-step-guide-360e</link>
      <guid>https://dev.to/antonmartyniuk/ef-core-and-azure-sql-database-a-step-by-step-guide-360e</guid>
      <description>&lt;p&gt;EF Core is the most popular way to work with a database in .NET.&lt;/p&gt;

&lt;p&gt;Azure SQL Database is the managed SQL Server you get in the cloud, with backups, patching, and high availability handled for you.&lt;/p&gt;

&lt;p&gt;Putting the two together is straightforward, but the cloud adds a few things you never deal with on a local SQL Server.&lt;/p&gt;

&lt;p&gt;Connections drop under load. The firewall blocks you by default. And a database password sitting in your connection string is a risk you want to remove before production.&lt;/p&gt;

&lt;p&gt;Today I want to present you with a complete guide to getting started with Azure SQL Database in .NET.&lt;/p&gt;

&lt;p&gt;In this post, we will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What Azure SQL Database is&lt;/li&gt;
&lt;li&gt;How to create an Azure SQL Database in the Azure Portal&lt;/li&gt;
&lt;li&gt;Connecting EF Core to Azure SQL&lt;/li&gt;
&lt;li&gt;Running EF Core migrations against Azure SQL&lt;/li&gt;
&lt;li&gt;Adding connection resilience for transient faults&lt;/li&gt;
&lt;li&gt;Setting up Azure SQL with .NET Aspire&lt;/li&gt;
&lt;li&gt;Passwordless authentication with Microsoft Entra ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/ef-core-and-azure-sql-database" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/ef-core-and-azure-sql-database&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Azure SQL Database
&lt;/h2&gt;

&lt;p&gt;Azure SQL Database is a fully managed database service built on the SQL Server engine.&lt;/p&gt;

&lt;p&gt;Because it is the same engine, EF Core talks to it through the exact provider you already use for SQL Server.&lt;br&gt;
You do not need a special "Azure" provider - only a connection string that points to the cloud.&lt;/p&gt;

&lt;p&gt;Azure manages the parts you do not want to: the operating system, patching, backups, and failover.&lt;br&gt;
You manage your schema and your data.&lt;/p&gt;

&lt;p&gt;You pick from two main purchasing models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Provisioned&lt;/strong&gt; - a fixed amount of compute that is always on. Predictable cost for steady workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless&lt;/strong&gt; - compute that scales automatically and pauses when idle. You pay per second of use, which is ideal for development and test environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, let's create one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an Azure SQL Database in the Azure Portal
&lt;/h2&gt;

&lt;p&gt;Follow these steps:&lt;/p&gt;

&lt;p&gt;1. Open the &lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;Azure Portal&lt;/a&gt;, search for "SQL databases", and click Create.&lt;/p&gt;

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

&lt;p&gt;2. On the next page, select your Azure subscription and click Create:&lt;/p&gt;

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

&lt;p&gt;3. Enter your database name and set up a logical SQL server to host the database:&lt;/p&gt;

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

&lt;p&gt;4. Create a new one, select a location, give it a unique name, set the identification method, and set an admin login and password.&lt;/p&gt;

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

&lt;p&gt;5. For the compute tier, choose Serverless while you are testing.&lt;br&gt;
It pauses when idle, so you only pay while you use it.&lt;/p&gt;

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

&lt;p&gt;6. After the database is created, you need to open the firewall. By default, Azure SQL blocks every connection.&lt;/p&gt;

&lt;p&gt;Go to the server's Networking settings, and add your client IP address so you can connect from your machine.&lt;br&gt;
Turn on "Allow Azure services and resources to access this server" so apps you deploy in Azure can connect to it.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Note: The firewall is the most common reason a first connection fails. If you see a "Cannot open server" error, check that your current IP is in the firewall rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the database ready, let's connect EF Core to it.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/ef-core-and-azure-sql-database" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/ef-core-and-azure-sql-database&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>azure</category>
    </item>
    <item>
      <title>How to Pass Any .NET Interview in 2026: A Complete Roadmap</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 27 Oct 2026 18:39:15 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-pass-any-net-interview-in-2026-a-complete-roadmap-h4n</link>
      <guid>https://dev.to/antonmartyniuk/how-to-pass-any-net-interview-in-2026-a-complete-roadmap-h4n</guid>
      <description>&lt;p&gt;Most .NET developers don't fail interviews because they aren't good enough.&lt;/p&gt;

&lt;p&gt;They fail because nobody ever showed them the full map of what they are being measured against.&lt;/p&gt;

&lt;p&gt;They learn whatever their current job needs, ship features for a few years, and assume that is enough.&lt;br&gt;
Then they walk into a senior interview and get asked about things they have never had a reason to touch.&lt;/p&gt;

&lt;p&gt;A senior interview covers a broad range of topics: the C# runtime, async, ASP .NET Core internals, EF Core, security, system design, and how you reason about trade-offs.&lt;br&gt;
Miss a few of those areas, and a strong engineer can look unprepared.&lt;/p&gt;

&lt;p&gt;The good news is that the surface is not infinite.&lt;br&gt;
It is a known map, and you can cover it on purpose.&lt;/p&gt;

&lt;p&gt;I have spent more than 13 years building production .NET systems, I am a Microsoft MVP, and I have helped tens of thousands of developers level up through my newsletter.&lt;/p&gt;

&lt;p&gt;This is the exact roadmap I would follow today to pass a .NET interview at any level - and to keep advancing once you are already a Senior.&lt;/p&gt;

&lt;p&gt;In this post, we will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 1: Know your target and where you stand today&lt;/li&gt;
&lt;li&gt;Step 2: Master the technical foundation (the full map)&lt;/li&gt;
&lt;li&gt;Step 3: Build a portfolio that proves senior thinking&lt;/li&gt;
&lt;li&gt;Step 4: Turn your GitHub into your portfolio&lt;/li&gt;
&lt;li&gt;Step 5: Make LinkedIn bring interviews to you&lt;/li&gt;
&lt;li&gt;Step 6: Win the interview itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-pass-any-dotnet-interview-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-pass-any-dotnet-interview-in-2026&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Know your target and where you stand today
&lt;/h2&gt;

&lt;p&gt;You cannot prepare for a target you have not defined.&lt;/p&gt;

&lt;p&gt;A mid-level interview and a senior interview measure different things, and a lead interview shifts the bar again.&lt;/p&gt;

&lt;p&gt;If you aim at the wrong level, you either over-prepare on trivia or walk in under-prepared on the things that actually matter.&lt;/p&gt;

&lt;p&gt;Here is what each level is really measured on - and the signal an interviewer is actually watching for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mid-level:&lt;/strong&gt; Can you write correct, clean code, use the framework properly, and finish a feature without constant hand-holding? &lt;em&gt;What they see:&lt;/em&gt; you reach for the right tool and your code works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior:&lt;/strong&gt; Can you make decisions and defend them? Senior interviews probe trade-offs, failure modes, performance, and whether your design survives real load and a real team. &lt;em&gt;What they see:&lt;/em&gt; you name the trade-off before anyone asks for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead and Staff:&lt;/strong&gt; Can you own scope beyond a single service, mentor others, and design systems across boundaries? &lt;em&gt;What they see:&lt;/em&gt; you scope the problem, not just the solution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters even more in the AI era.&lt;br&gt;
As AI writes most of the code, interviews shift from writing code to judging it.&lt;br&gt;
The edge is no longer how fast you produce a CRUD endpoint - it is what tool and approach you would use, whether you can spot the bug, the security hole, or the wrong abstraction in code an AI just generated.&lt;/p&gt;

&lt;p&gt;If you are already senior and want to advance, the trap is different.&lt;br&gt;
You have deep knowledge in the few areas your current job covers, but quite a few gaps elsewhere.&lt;/p&gt;

&lt;p&gt;List the topics you quietly hope won't come up.&lt;br&gt;
That list is your study plan.&lt;br&gt;
Moving up means turning those gaps into strengths and learning to talk about architecture and trade-offs, not just code.&lt;/p&gt;

&lt;p&gt;Once you know the target, be brutally honest about where you stand against it.&lt;/p&gt;

&lt;p&gt;Most developers overestimate the areas they use daily and are unaware of their blind spots.&lt;br&gt;
The async questions feel easy, then a single question about &lt;code&gt;ValueTask&lt;/code&gt; or the garbage collector exposes a gap they never knew existed.&lt;/p&gt;

&lt;p&gt;So run a five-minute self-audit. For each area, answer one question out loud, with no notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C#:&lt;/strong&gt; can you explain when a &lt;code&gt;struct&lt;/code&gt; and &lt;code&gt;record&lt;/code&gt; is a better choice than a &lt;code&gt;class&lt;/code&gt;, and what boxing actually costs?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async:&lt;/strong&gt; can you explain what &lt;code&gt;ConfigureAwait(false)&lt;/code&gt; does, and when it stops mattering?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASP .NET Core:&lt;/strong&gt; can you explain how model binding turns an HTTP request into your method parameters?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EF Core:&lt;/strong&gt; can you explain why a specific query is slow, and how you would prove it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture:&lt;/strong&gt; can you defend one design decision from your last project, with the trade-off you accepted?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; can you explain the difference between authentication and authorization with a concrete example?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; can you explain what your system does when a dependency it calls goes down?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any question you stumble on is a gap. That is where your preparation time should go - not on the areas that already feel easy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The fastest way to waste preparation time is to keep studying what you already know. Find the gaps first, then study those.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want an honest baseline before you start, take the free &lt;a href="https://antondevtips.com/dotnet-developer-level-test" rel="noopener noreferrer"&gt;.NET Developer Interview Test&lt;/a&gt; created by me.&lt;br&gt;
It's a real interview run as tests; it adapts to how you answer the questions, increasing or decreasing their difficulty.&lt;/p&gt;

&lt;p&gt;It scores you from Junior to Senior+ across the main areas and tells you exactly where to focus.&lt;br&gt;
Once completed, you will receive a free report on your strengths and weaknesses and the knowledge gaps you need to fill.&lt;br&gt;
In the report, you will find an explanation for each correct answer.&lt;/p&gt;

&lt;p&gt;Once you know the target and your gaps, the next step is closing them - and that starts with the technical foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Master the technical foundation (the full map)
&lt;/h2&gt;

&lt;p&gt;This is the part most people get wrong.&lt;br&gt;
They study random videos and scattered blog posts and hope the topics eventually add up to a complete picture.&lt;/p&gt;

&lt;p&gt;They never do.&lt;br&gt;
A senior .NET interview draws from a surprisingly fixed set of areas.&lt;/p&gt;

&lt;p&gt;Learn these on purpose, and very little will surprise you. For each area I have added the &lt;strong&gt;senior signal&lt;/strong&gt; - the failure mode you are expected to explain, not just the topics you are expected to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C# language and runtime:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Value vs reference types, boxing and unboxing, strings and immutability, generics and variance, records, pattern matching, and nullable reference types.&lt;/li&gt;
&lt;li&gt;Then how it actually runs: the CLR, the JIT, tiered compilation, and AOT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior signal:&lt;/strong&gt; explain why a &lt;code&gt;record&lt;/code&gt; that holds a &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; still breaks value equality - the generated &lt;code&gt;Equals&lt;/code&gt; compares the list by reference, not by its contents.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async, threading, and memory:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; and the state machine behind it, &lt;code&gt;Task&lt;/code&gt; vs &lt;code&gt;ValueTask&lt;/code&gt;, &lt;code&gt;ConfigureAwait&lt;/code&gt;, and cancellation tokens.&lt;/li&gt;
&lt;li&gt;Then concurrency: locks, &lt;code&gt;SemaphoreSlim&lt;/code&gt;, and channels.&lt;/li&gt;
&lt;li&gt;Then memory: the garbage collector, the large object heap, &lt;code&gt;IDisposable&lt;/code&gt;, and &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior signal:&lt;/strong&gt; explain why &lt;code&gt;async void&lt;/code&gt; hides exceptions until they crash the process, and why blocking on &lt;code&gt;.Result&lt;/code&gt; or &lt;code&gt;.Wait()&lt;/code&gt; can deadlock.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASP .NET Core:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The host and the request pipeline, middleware, filters, model binding, Minimal APIs vs controllers, and dependency injection lifetimes (transient, scoped, singleton).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior signal:&lt;/strong&gt; explain why the order of &lt;code&gt;UseAuthentication&lt;/code&gt; and &lt;code&gt;UseAuthorization&lt;/code&gt; matters, and what quietly breaks if you swap them.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data and EF Core:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Change tracking, relationships, loading strategies, migrations, concurrency handling, and query performance.&lt;/li&gt;
&lt;li&gt;This is where a lot of senior interviews dig in, because it is where production systems hurt.&lt;/li&gt;
&lt;li&gt;A good warm-up is &lt;a href="https://antondevtips.com/blog/top-10-mistakes-developers-make-in-ef-core" rel="noopener noreferrer"&gt;Top 10 Mistakes Developers Make in EF Core&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior signal:&lt;/strong&gt; explain how lazy loading turns one query into an N+1 storm, and when a projection or &lt;code&gt;AsNoTracking&lt;/code&gt; is the fix.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API design and architecture:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;REST done properly, versioning, validation, and how to structure an application so it scales with the team.&lt;/li&gt;
&lt;li&gt;Two solid references are &lt;a href="https://antondevtips.com/blog/best-practices-for-building-rest-apis" rel="noopener noreferrer"&gt;Best Practices for Building REST APIs&lt;/a&gt; and &lt;a href="https://antondevtips.com/blog/n-layered-vs-clean-vs-vertical-slice-architecture" rel="noopener noreferrer"&gt;N-Layered vs Clean vs Vertical Slice Architecture&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior signal:&lt;/strong&gt; explain why a non-idempotent &lt;code&gt;POST&lt;/code&gt; double-charges a customer when the client retries, and how an idempotency key prevents it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security and identity:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Authentication vs authorization, JWT and refresh tokens, cookies, ASP .NET Core Identity, OAuth 2.0 and OpenID Connect, and CORS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior signal:&lt;/strong&gt; explain why a JWT must be validated for signature, issuer, audience, and expiry - not just decoded - and why long-lived access tokens are risky without refresh-token rotation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance and reliability:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Caching layers, output caching, rate limiting, structured logging, health checks, and observability with OpenTelemetry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior signal:&lt;/strong&gt; explain how retries without a circuit breaker turn a small outage into a full one, and how a cache stampede hammers the database when a hot key expires.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each area, train yourself to answer the "why," not just the "how."&lt;/p&gt;

&lt;p&gt;Anyone can recite that scoped services live for one request.&lt;br&gt;
A senior explains what breaks when you inject a scoped service into a singleton, and why.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Interviewers rarely want trivia. They want to see that you understand the trade-off behind a feature and can reason about it under pressure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is exactly the surface my &lt;a href="https://antondevtips.com/dotnet-senior-playbook" rel="noopener noreferrer"&gt;.NET Senior Playbook&lt;/a&gt; was built to map.&lt;br&gt;
It is 800+ questions across 54 chapters covering every area above, with a test after each chapter so you prove a topic you're stuck on instead of assuming it did.&lt;/p&gt;

&lt;p&gt;Knowing the material is half the battle. The other half is proving it - and that starts with what you build.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-pass-any-dotnet-interview-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-pass-any-dotnet-interview-in-2026&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>career</category>
    </item>
    <item>
      <title>All Types of Caching in .NET: When to Use Each (Deep Dive, Part 1)</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 27 Oct 2026 12:08:00 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/all-types-of-caching-in-net-when-to-use-each-deep-dive-part-1-24ej</link>
      <guid>https://dev.to/antonmartyniuk/all-types-of-caching-in-net-when-to-use-each-deep-dive-part-1-24ej</guid>
      <description>&lt;p&gt;Your API is slow and Claude recommends you to add caching.&lt;/p&gt;

&lt;p&gt;It adds &lt;code&gt;IMemoryCache&lt;/code&gt; to the shipment tracking API, and the p95 latency drops from 120 ms to 5 ms.&lt;/p&gt;

&lt;p&gt;Everyone is happy.&lt;/p&gt;

&lt;p&gt;Then they scale to two instances.&lt;/p&gt;

&lt;p&gt;A customer updates the delivery address, refreshes the page, and sees the old one.&lt;br&gt;
Refreshes again and sees the new one. One more refresh, and the old address is back.&lt;/p&gt;

&lt;p&gt;Nothing is broken. Each instance has its own memory, and only one of them knows that the address has changed.&lt;/p&gt;

&lt;p&gt;Caching is the cheapest performance win in .NET, and one of the easiest things to get wrong.&lt;/p&gt;

&lt;p&gt;The first and most important rule is: do not add caching to hide your slow database queries.&lt;br&gt;
It's like fixing symptoms.&lt;/p&gt;

&lt;p&gt;First, fix your database queries (the root cause), and only then add caching to increase your throughput and make your API even faster.&lt;/p&gt;

&lt;p&gt;In .NET, you have 6 ways to cache: IMemoryCache, IDistributedCache, Redis, HybridCache, FusionCache, and Output Cache.&lt;/p&gt;

&lt;p&gt;In this post, we will explore each caching option with pros, cons, pitfalls, and when to use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where Each Cache Sits in a .NET App&lt;/li&gt;
&lt;li&gt;In-Memory Cache (IMemoryCache)&lt;/li&gt;
&lt;li&gt;Distributed Cache (IDistributedCache) with Redis&lt;/li&gt;
&lt;li&gt;HybridCache&lt;/li&gt;
&lt;li&gt;FusionCache (Part 2)&lt;/li&gt;
&lt;li&gt;Output Cache (Part 2)&lt;/li&gt;
&lt;li&gt;Which Cache Should You Choose (Part 2)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;

&lt;p&gt;It's a very long newsletter (no one will read such a long article), so I will release PART 2 next week.&lt;/p&gt;



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part1" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part1&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Where Each Cache Sits in a .NET App
&lt;/h2&gt;

&lt;p&gt;Every cache in .NET sits somewhere on the path between an HTTP request and your database.&lt;/p&gt;

&lt;p&gt;Some sit in front of your code and cache whole HTTP responses.&lt;br&gt;
Others sit inside your handlers and cache .NET objects, either in the memory of one process or in an external store shared by every instance.&lt;/p&gt;

&lt;p&gt;Here is the full picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP request
   │
   ▼
Output Cache (whole HTTP response) ──miss──┐
                                           ▼
Endpoint + Handler
   │
   ▼
L1: in-memory (IMemoryCache / HybridCache / FusionCache) ──miss──┐
                                                                 ▼
L2: distributed (IDistributedCache over Redis) ──miss──┐
                                                       ▼
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You need to know these three terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;L1 cache&lt;/strong&gt; is the memory of the current process. It is the fastest cache you can have because there is no network request and no deserialization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L2 cache&lt;/strong&gt; is an external store like Redis, shared by all instances of your app. Slower than L1 (but much faster than a database) and every instance sees the same data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache stampede&lt;/strong&gt; happens when many requests miss the cache at the same moment and all of them hit the database for the same key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we will explore the shipments domain and we will cache shipments.&lt;/p&gt;

&lt;p&gt;Here is the Minimal API endpoint that calls a handler, and the handler queries EF Core directly:&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/shipments/{shipmentNumber}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GetShipmentByNumberHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;shipment&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetShipmentByNumberHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ShipmentsDbContext&lt;/span&gt; &lt;span class="n"&gt;dbContext&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;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;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapToResponse&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query is already fast. We have indexed on the &lt;code&gt;Shipment.Number&lt;/code&gt; field and we also select only the necessary fields.&lt;/p&gt;

&lt;p&gt;Let's add a cache to this query.&lt;/p&gt;

&lt;h2&gt;
  
  
  In-Memory Cache (IMemoryCache)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;IMemoryCache&lt;/code&gt; is a key-value store that lives in the memory of your application process.&lt;/p&gt;

&lt;p&gt;You store any .NET object in it, and you get the same object reference back.&lt;br&gt;
When you get items from an IMemoryCache, they don't require serialization and you don't need any network requests.&lt;/p&gt;

&lt;p&gt;To use &lt;code&gt;IMemoryCache&lt;/code&gt; in your application, add it into the DI container:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;In the handler, we will use the &lt;a href="https://antondevtips.com/blog/how-to-implement-caching-strategies-in-dotnet" rel="noopener noreferrer"&gt;cache-aside strategy&lt;/a&gt; on top of the database query:&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;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GetShipmentByNumberHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ShipmentsDbContext&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IMemoryCache&lt;/span&gt; &lt;span class="n"&gt;cache&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;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;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrCreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"shipment:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AbsoluteExpirationRelativeToNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetSize&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapToResponse&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetOrCreateAsync&lt;/code&gt; first checks the cache, calls the database if the item is not found in the cache, and stores the result returned by the factory method.&lt;br&gt;
The entry expires 10 minutes after it was written.&lt;/p&gt;

&lt;p&gt;This is the fastest cache you can have in .NET.&lt;br&gt;
A hit is a dictionary lookup that returns a reference. There is nothing to deserialize.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IMemoryCache has the following advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero infrastructure:&lt;/strong&gt; no Redis, no connection strings, nothing to deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fastest possible reads:&lt;/strong&gt; a hit costs nanoseconds and allocates nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any object:&lt;/strong&gt; you can cache types that are not serializable, including compiled regexes or large immutable lookup tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Here are the drawbacks of IMemoryCache:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-process only:&lt;/strong&gt; two instances of your API have two separate caches that never talk to each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lost on restart:&lt;/strong&gt; every deployment starts with a cold cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shares memory with your app:&lt;/strong&gt; a cache that grows without a limit can cause memory issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  IMemoryCache Pitfalls
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;There is no size limit by default.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;SetSize(1)&lt;/code&gt; in the code above does nothing until you set a &lt;code&gt;SizeLimit&lt;/code&gt; on the cache itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMemoryCache&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;SizeLimit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The size unit is whatever you decide. Here, each shipment counts as 1, so the cache can hold at most 10,000 shipments.&lt;br&gt;
Once you set &lt;code&gt;SizeLimit&lt;/code&gt;, every entry must call &lt;code&gt;SetSize&lt;/code&gt;, or the cache throws when you add it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cache hands out the same reference every time.&lt;/strong&gt;&lt;br&gt;
If a caller mutates the object it got from the cache, every later caller sees the mutation.&lt;br&gt;
This is a classic source of bugs that only appear under load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrCreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// every next request now gets a shipment with no items.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache immutable records, or return a copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no way to remove entries by prefix or tag.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;IMemoryCache&lt;/code&gt; has &lt;code&gt;Remove(key)&lt;/code&gt; and nothing else.&lt;br&gt;
If you need to drop all cached shipments for an order, you need to store all the keys to be able to delete them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GetOrCreateAsync has no stampede protection.&lt;/strong&gt;&lt;br&gt;
If 100 requests miss the same key at the same time, 100 factories run and 100 database queries execute.&lt;br&gt;
&lt;code&gt;IMemoryCache&lt;/code&gt; does not lock per key. You have to add a &lt;code&gt;SemaphoreSlim&lt;/code&gt; yourself (bad!) or use a cache that does it for you.&lt;/p&gt;

&lt;p&gt;When you scale to several instances, you need a store they all share.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part1" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/all-types-of-caching-in-dotnet-and-when-to-use-each-part1&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>caching</category>
    </item>
    <item>
      <title>Claude Skills for .NET: Teach AI to Build Features Your Way</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:17:53 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/claude-skills-for-net-teach-ai-to-build-features-your-way-53f4</link>
      <guid>https://dev.to/antonmartyniuk/claude-skills-for-net-teach-ai-to-build-features-your-way-53f4</guid>
      <description>&lt;p&gt;Every developer who uses AI has felt this.&lt;/p&gt;

&lt;p&gt;You open a new chat.&lt;br&gt;
You explain your architecture.&lt;br&gt;
You explain your file naming.&lt;br&gt;
You explain that you do not use repositories, prefer a rich domain model, and that your handlers and services return a &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The AI writes good code.&lt;/p&gt;

&lt;p&gt;The next day, you open another chat, and you explain it all over again.&lt;/p&gt;

&lt;p&gt;This is the hidden tax of AI-assisted development.&lt;br&gt;
You repeat the same instructions in every session for every feature.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://antondevtips.com/blog/how-to-build-production-ready-projects-with-claude-code#step-4-generate-the-backend-specification" rel="noopener noreferrer"&gt;CLAUDE.md&lt;/a&gt; file helps; it's added to every session, but you can't make it with a lot of instructions (it should be 100 lines or less).&lt;br&gt;
It holds the facts about your project and overall architecture, not the step-by-step recipe for every kind of task you do.&lt;/p&gt;

&lt;p&gt;There is a better way: &lt;strong&gt;skills&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A skill packages a set of instructions, and even example files, that Claude loads only when it needs them.&lt;br&gt;
You write the recipe once, and Claude follows it every time, in every session.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Skills also work if you are using the OpenAI Codex agent&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this post, we will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What Claude skills are and why they beat repeating yourself&lt;/li&gt;
&lt;li&gt;The building blocks of a skill&lt;/li&gt;
&lt;li&gt;When a skill should trigger, and when it should not&lt;/li&gt;
&lt;li&gt;How to create a skill with the Skill Creator&lt;/li&gt;
&lt;li&gt;Where your skills live: Claude Code, Desktop, and CoWork&lt;/li&gt;
&lt;li&gt;How to encode your project's conventions as skills&lt;/li&gt;
&lt;li&gt;Skills for gRPC, GraphQL, messaging, and new services&lt;/li&gt;
&lt;li&gt;The ready-made .NET skills published by Microsoft&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/creating-claude-skills-for-dotnet-apps" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/creating-claude-skills-for-dotnet-apps&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What Are Claude Skills (and Why They Beat Repeating Yourself)
&lt;/h2&gt;

&lt;p&gt;A skill is a folder with instructions that Claude reads when a task matches.&lt;br&gt;
it's placed in your repository root: &lt;code&gt;.claude/skills/*&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;At its simplest, a skill is a single file called &lt;code&gt;SKILL.md&lt;/code&gt;.&lt;br&gt;
It has a short header that tells Claude what the skill does and when to use it, followed by the instructions themselves.&lt;/p&gt;

&lt;p&gt;Here is a tiny skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create-endpoint&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create a new ASP.NET Core Minimal API endpoint following our project conventions.&lt;/span&gt;
&lt;span class="s"&gt;Use whenever the user asks to add an endpoint, route, or API operation.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Create a Minimal API Endpoint&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Create the endpoint in &lt;span class="sb"&gt;`Features/&amp;lt;UseCase&amp;gt;/&amp;lt;UseCase&amp;gt;.Endpoint.cs`&lt;/span&gt;.
&lt;span class="p"&gt;2.&lt;/span&gt; Implement &lt;span class="sb"&gt;`IApiEndpoint`&lt;/span&gt; and map the route from &lt;span class="sb"&gt;`RouteConsts`&lt;/span&gt;.
&lt;span class="p"&gt;3.&lt;/span&gt; Call the handler, and convert a failed &lt;span class="sb"&gt;`Result&amp;lt;T&amp;gt;`&lt;/span&gt; with &lt;span class="sb"&gt;`ToProblem()`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a complete, working skill.&lt;/p&gt;

&lt;p&gt;When you ask Claude to "add an endpoint for canceling a shipment," it reads this skill and follows your steps, instead of inventing its own structure.&lt;/p&gt;

&lt;p&gt;The difference between a skill and a one-off prompt is reuse.&lt;/p&gt;

&lt;p&gt;A prompt lives and dies in a single chat session.&lt;br&gt;
A skill is written once and then reused, shared, and version-controlled like any other file in your project.&lt;/p&gt;

&lt;p&gt;This means every team member can use it.&lt;/p&gt;

&lt;p&gt;Skills also keep your context small through progressive disclosure.&lt;/p&gt;

&lt;p&gt;Claude always sees the skill's &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;, which is about one line of text.&lt;br&gt;
It loads the full &lt;code&gt;SKILL.md&lt;/code&gt; only when your task matches that description.&lt;br&gt;
And it opens any bundled files only when it actually needs them.&lt;/p&gt;

&lt;p&gt;This means you can have dozens of skills installed without flooding the model with text it does not need right now.&lt;br&gt;
The skill remains quiet until it is useful.&lt;/p&gt;

&lt;p&gt;Now that you know what a skill is, let's look at what goes inside one.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Building Blocks of a Skill
&lt;/h2&gt;

&lt;p&gt;Every skill is a folder. The only required file is &lt;code&gt;SKILL.md&lt;/code&gt;.&lt;br&gt;
Everything else is optional.&lt;/p&gt;

&lt;p&gt;Here is the anatomy of a fuller skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create-feature/
├── SKILL.md            (required: instructions + when to use)
├── references/         (optional: detailed docs loaded on demand)
│   └── architecture.md
├── scripts/            (optional: scripts Claude can run)
│   └── scaffold.ps1
└── assets/             (optional: files used in the output)
    └── Endpoint.template.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The &lt;code&gt;SKILL.md&lt;/code&gt; file&lt;/strong&gt; has two parts.&lt;/p&gt;

&lt;p&gt;The first part is the YAML frontmatter at the top, between the &lt;code&gt;---&lt;/code&gt; markers. Two fields matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; - a short identifier in kebab-case, like &lt;code&gt;create-feature&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt; - one or two sentences that tell Claude what the skill does and when to use it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the most important line in the skill because it determines when the skill triggers. We will cover that in the next section.&lt;/p&gt;

&lt;p&gt;The second part is the Markdown body. This is where you write the actual instructions: the steps, the rules, the examples, and the things to avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;references/&lt;/code&gt; folder&lt;/strong&gt; holds detailed documents that Claude reads only when needed. Keep &lt;code&gt;SKILL.md&lt;/code&gt; short and focused, and push long material - a full coding standard, an API reference, a list of edge cases - into separate files.&lt;br&gt;
Claude opens them on demand, so your main instructions stay easy to follow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;scripts/&lt;/code&gt; folder&lt;/strong&gt; holds code Claude can run instead of writing it from scratch.&lt;br&gt;
A PowerShell or bash script that scaffolds a folder, or a small script that formats output, belongs here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;assets/&lt;/code&gt; folder&lt;/strong&gt; holds files used in the result, like a project template, a Dockerfile, or a &lt;code&gt;.cshtml&lt;/code&gt; view.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: Keep &lt;code&gt;SKILL.md&lt;/code&gt; short, ideally a few hundred lines at most. If it grows large, that is a signal to move the details into &lt;code&gt;references/&lt;/code&gt; and point to them from the body. A focused skill is easier for Claude to follow than one giant wall of rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A skill is only useful if Claude opens it at the right moment. Let's look at what controls that.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/creating-claude-skills-for-dotnet-apps" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/creating-claude-skills-for-dotnet-apps&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>ai</category>
    </item>
    <item>
      <title>10 Tools Every .NET Developer Should Use in 2026</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 23 Oct 2026 04:41:23 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/10-tools-every-net-developer-should-use-in-2026-5hj0</link>
      <guid>https://dev.to/antonmartyniuk/10-tools-every-net-developer-should-use-in-2026-5hj0</guid>
      <description>&lt;p&gt;The developer toolkit looks very different in 2026 than it did just two years ago, or even a year ago.&lt;/p&gt;

&lt;p&gt;AI is no longer a side feature you tap into occasionally.&lt;br&gt;
It sits at the center of how you write code, design systems, run meetings, and learn new technologies.&lt;/p&gt;

&lt;p&gt;Today, I want to tell you about the 10 tools I actually use with great success in 2026.&lt;br&gt;
And can definitely recommend them to .NET developers, architects, and tech leads to help ship faster, think more clearly, and stop wasting hours on work that machines now handle better.&lt;/p&gt;

&lt;p&gt;I receive many LinkedIn messages from people asking what tools I use to be more productive.&lt;br&gt;
That's why I decided to create this newsletter issue.&lt;/p&gt;

&lt;p&gt;If you only adopt a few of them this year, you will feel the difference in your daily workflow almost immediately.&lt;/p&gt;

&lt;p&gt;P.S.: I'm not sponsored by any vendors of these tools to write this issue.&lt;/p&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/10-tools-that-will-make-developers-more-productive-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/10-tools-that-will-make-developers-more-productive-in-2026&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Coding Assistants
&lt;/h2&gt;

&lt;p&gt;The first wave of AI coding tools focused on autocompletion (like GitHub Copilot).&lt;br&gt;
They suggested the next line or two of code based on what you just typed.&lt;/p&gt;

&lt;p&gt;Today, you need tools that reason about your entire codebase, understand your architecture, and can execute multi-step tasks without constant hand-holding.&lt;br&gt;
And most importantly, they produce high-quality code.&lt;/p&gt;

&lt;p&gt;For a long time, GitHub Copilot was the default AI coding assistant. But many developers have moved on.&lt;/p&gt;

&lt;p&gt;Copilot agent is fine, but it has the following drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's sitting in your IDE and is limited in what it can do on your PC&lt;/li&gt;
&lt;li&gt;It has lower limits on using AI models (Claude, OpenAI) - then if you use these directly&lt;/li&gt;
&lt;li&gt;Overall, the features and quality are lower than the competitors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, there are two better AI agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude (best)&lt;/li&gt;
&lt;li&gt;OpenAI Codex&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Claude Code
&lt;/h2&gt;

&lt;p&gt;It's an agentic coding tool that runs directly from your terminal or Desktop.&lt;br&gt;
You describe what you want, and it works through the problem step by step.&lt;br&gt;
It reads your files (on your PC, not just the solution), understands the context, makes changes, runs tests, and reports back.&lt;/p&gt;

&lt;p&gt;Here is what makes &lt;a href="https://claude.com" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; stand out for .NET developers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before Claude writes any code, you can ask it to make a plan first.&lt;br&gt;
It reads your project, thinks through the task, and presents a clear, step-by-step plan.&lt;/p&gt;

&lt;p&gt;You review the plan, approve it, and only then does Claude start changing files.&lt;/p&gt;

&lt;p&gt;This is a huge time-saver for bigger tasks.&lt;br&gt;
You catch wrong assumptions early, before any line of code is written.&lt;/p&gt;

&lt;p&gt;And what I really like is how Claude surveys me with different questions and recommended options each time I create a plan.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Quality of produced code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where Claude Code really stands out.&lt;/p&gt;

&lt;p&gt;It reads your &lt;a href="https://antondevtips.com/blog/how-to-build-production-ready-projects-with-claude-code#step-4-generate-the-backend-specification" rel="noopener noreferrer"&gt;CLAUDE.md&lt;/a&gt; file at the root of the project and follows the rules it defines.&lt;br&gt;
For .NET projects, that means it respects your Vertical Slice structure, uses &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; instead of throwing exceptions, applies primary constructors, and skips patterns you told it to avoid, like Repository or MediatR.&lt;/p&gt;

&lt;p&gt;The most intelligent model is Claude Opus 4.7.&lt;br&gt;
It is the most capable AI model for writing code in the market.&lt;/p&gt;

&lt;p&gt;When using Opus, you spend less time cleaning up after the model and more time reviewing real logic.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Writing code in a separate branch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code can create a Git worktree and work on a separate branch in the background.&lt;/p&gt;

&lt;p&gt;You keep coding on your main branch.&lt;br&gt;
Claude builds a feature, fixes a bug, or runs a refactor on its own branch.&lt;br&gt;
When it's done, you review the diff and merge it as you would any other pull request.&lt;/p&gt;

&lt;p&gt;This is perfect for long-running tasks. You don't have to stop your own work to wait for the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code reviews&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code can review your current diff or a full pull request.&lt;br&gt;
It looks for real bugs, security issues, missing tests, and broken conventions.&lt;/p&gt;

&lt;p&gt;You can run a quick review locally with the &lt;code&gt;/code-review&lt;/code&gt; skill, or trigger &lt;code&gt;/ultrareview&lt;/code&gt; for a multi-agent cloud review on harder pull requests.&lt;br&gt;
Several specialized agents read the code in parallel and give you findings as inline comments on the PR.&lt;/p&gt;

&lt;p&gt;It's like having a senior reviewer on call, every hour of the day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Skill is a small set of instructions you write once, and Claude loads on demand.&lt;/p&gt;

&lt;p&gt;You can think of Skills as smart commands tuned for your team. Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A skill that drafts pull request descriptions in your team's format&lt;/li&gt;
&lt;li&gt;A skill that scaffolds a new feature folder following your Vertical Slice rules&lt;/li&gt;
&lt;li&gt;A skill that runs your release checklist before a deploy&lt;/li&gt;
&lt;li&gt;A skill that writes integration tests using Testcontainers and Respawn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anyone on the team can use a Skill without having to remember the steps.&lt;br&gt;
The Skill remembers them.&lt;/p&gt;

&lt;p&gt;You can easily build your own skills from any session with a built-in Claude skill creator tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connectors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code connects to the systems you already use: GitHub, Jira, Slack, Figma, Google Drive, Notion, MongoDB, Postgres, AWS, and many more.&lt;/p&gt;

&lt;p&gt;You can ask things like "show me the failing tests on PR 42" or "Create a JIRA ticket from this bug," and Claude pulls the data and acts on it.&lt;br&gt;
The agent stops being a code-only tool and starts working with your whole stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Co Work&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cowork is not just about coding.&lt;br&gt;
It gives Claude access to your PC (where you allow it, and it's pretty secure), so it can do almost any work you do at your desk.&lt;/p&gt;

&lt;p&gt;You can ask Claude to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Word document with a project proposal&lt;/li&gt;
&lt;li&gt;Generate a PDF report from your meeting notes&lt;/li&gt;
&lt;li&gt;Build an Excel file with a budget, a roadmap, or a backlog&lt;/li&gt;
&lt;li&gt;Organize your local documentation folder and rename files in a consistent way&lt;/li&gt;
&lt;li&gt;Write a Business Requirements Document (BRD) for a new feature&lt;/li&gt;
&lt;li&gt;Prepare a slide deck from your latest design notes&lt;/li&gt;
&lt;li&gt;Fill out forms, rename screenshots, sort downloads, clean up your desktop&lt;/li&gt;
&lt;li&gt;Delegate any task in your web browser. Like book 10 sits for a conference for your team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For tech leads and architects, this is a real shift.&lt;/p&gt;

&lt;p&gt;And the best part is that Cowork also supports remote sessions.&lt;br&gt;
You can control Claude from your phone, check what it's doing, kick off new tasks, or review results while you're away from your laptop.&lt;/p&gt;

&lt;p&gt;And you can also schedule tasks to run while you sleep; you just need your PC on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Models&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Code uses the Claude 4.X model family. You pick the right model for the job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Opus 4.7&lt;/strong&gt; — the most capable model. Best for hard tasks like complex refactors, big architecture changes, tricky debugging, and any work where quality matters more than speed. This is the model I use for the heavy lifting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Sonnet 4.6&lt;/strong&gt; — the balanced default. Fast enough for everyday coding, smart enough for most feature work. Great choice when you want speed and quality together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Haiku 4.5&lt;/strong&gt; — the fastest and cheapest model. Best for quick edits, simple lookups, small fixes, and high-volume tasks where you don't need deep reasoning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can use Claude as a plugin in Rider and VS Code, but honestly, they have limited capabilities compared to Claude Desktop.&lt;br&gt;
This is why I recommend using Claude desktop, it works better and has way more features.&lt;/p&gt;

&lt;p&gt;I wrote a &lt;a href="https://antondevtips.com/blog/how-to-build-production-ready-projects-with-claude-code" rel="noopener noreferrer"&gt;detailed guide&lt;/a&gt; on using Claude for production development.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pricing:&lt;/strong&gt; Free tier available on claude.ai (doesn't include Claude Code). Paid plans start at Pro $20/month (or $17/month on the annual plan), with Max at $100/month and $200/month for heavier usage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Download Claude Desktop &lt;a href="https://claude.com/download" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/10-tools-that-will-make-developers-more-productive-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/10-tools-that-will-make-developers-more-productive-in-2026&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Rate Limiting in ASP.NET Core: All 4 Algorithms Explained With Code</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 20 Oct 2026 11:08:36 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/rate-limiting-in-aspnet-core-all-4-algorithms-explained-with-code-33fa</link>
      <guid>https://dev.to/antonmartyniuk/rate-limiting-in-aspnet-core-all-4-algorithms-explained-with-code-33fa</guid>
      <description>&lt;p&gt;One misbehaving client is enough to take an API down.&lt;/p&gt;

&lt;p&gt;It doesn't have to be an attacker.&lt;/p&gt;

&lt;p&gt;A mobile app with a bug in its retry loop.&lt;br&gt;
A partner integration that polls your endpoint every 100 milliseconds.&lt;br&gt;
A script a colleague wrote to "quickly export the data".&lt;/p&gt;

&lt;p&gt;Every one of those requests hits your database, your cache and your thread pool exactly like a legitimate one.&lt;/p&gt;

&lt;p&gt;Rate limiting is the guardrail that prevents a single caller from consuming everything.&lt;/p&gt;

&lt;p&gt;You probably know that ASP .NET Core has a built-in rate limiter.&lt;br&gt;
Do you know the four different algorithms and how each one behaves under the same traffic conditions?&lt;/p&gt;

&lt;p&gt;After reading, you will be able to pick the right algorithm for each endpoint and wire it up in minutes.&lt;/p&gt;

&lt;p&gt;In this post, we will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why Rate Limiting Matters and How It Works in ASP .NET Core&lt;/li&gt;
&lt;li&gt;Fixed Window Limiter&lt;/li&gt;
&lt;li&gt;Sliding Window Limiter&lt;/li&gt;
&lt;li&gt;Token Bucket Limiter&lt;/li&gt;
&lt;li&gt;Concurrency Limiter&lt;/li&gt;
&lt;li&gt;Choosing the Right Algorithm&lt;/li&gt;
&lt;li&gt;Partitioning by IP, User, and API Key&lt;/li&gt;
&lt;li&gt;Handling Rejected Requests and Retry-After&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/aspnet-core-rate-limiting-algorithms-explained" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/aspnet-core-rate-limiting-algorithms-explained&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Rate Limiting Matters and How It Works in ASP .NET Core
&lt;/h2&gt;

&lt;p&gt;Rate limiting is a rule that says: "this caller may do this much work in this much time, and no more".&lt;/p&gt;

&lt;p&gt;It does three jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protects from abuse:&lt;/strong&gt; brute-force logins, scraping, and denial-of-service floods are cut off before they reach your handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protects from accidents:&lt;/strong&gt; a broken retry loop is stopped before it takes the database down for everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforces fair share:&lt;/strong&gt; a free tier gets a small allowance, a paid tier a bigger one, and one noisy client cannot starve the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since .NET 7, rate limiting is built into ASP .NET Core.&lt;/p&gt;

&lt;p&gt;The algorithms live in &lt;code&gt;System.Threading.RateLimiting&lt;/code&gt;, the middleware in &lt;code&gt;Microsoft.AspNetCore.RateLimiting&lt;/code&gt;.&lt;br&gt;
Both ship with the framework, so you don't need to install any NuGet packages.&lt;/p&gt;

&lt;p&gt;Wiring it up takes three steps: register policies, add middleware and attach a policy to an endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Threading.RateLimiting&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;Microsoft.AspNetCore.RateLimiting&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;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="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;AddAuthorization&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;AddRateLimiter&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;RejectionStatusCode&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;Status429TooManyRequests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Policies are registered here, one per section below&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;UseAuthentication&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;UseAuthorization&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;UseRateLimiter&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;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/auth/login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LoginEndpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireRateLimiting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AddRateLimiter&lt;/code&gt; registers the options object that holds every named policy.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UseRateLimiter&lt;/code&gt; adds the middleware that checks each request against the policy attached to its endpoint.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RequireRateLimiting("login")&lt;/code&gt; attaches a policy middleware by name.&lt;/p&gt;

&lt;p&gt;Endpoints without a policy are not limited at all.&lt;/p&gt;

&lt;p&gt;Middleware order matters.&lt;br&gt;
&lt;code&gt;UseRateLimiter&lt;/code&gt; sits after &lt;code&gt;UseAuthentication&lt;/code&gt;, so the user's claims are populated when the limiter runs, which makes per-user limits possible later.&lt;/p&gt;

&lt;p&gt;Now let's explore the Rate Limiting algorithms.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fixed Window Limiter
&lt;/h2&gt;

&lt;p&gt;Imagine a bakery that sells 100 buns per hour.&lt;/p&gt;

&lt;p&gt;At 9:00, the counter resets to 100. When the 100th bun is served, the door closes until 10:00, when the counter resets.&lt;/p&gt;

&lt;p&gt;It does not matter whether the buns sold out in five minutes or across the hour. Only the clock brings buns back.&lt;/p&gt;

&lt;p&gt;That is the fixed window limiter: a counter and a reset time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRateLimiter&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;RejectionStatusCode&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;Status429TooManyRequests&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="nf"&gt;AddFixedWindowLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limiter&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;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PermitLimit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Window&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&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;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueueLimit&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;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueueProcessingOrder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QueueProcessingOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OldestFirst&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/auth/login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;LoginRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LoginHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unauthorized&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;RequireRateLimiting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PermitLimit&lt;/code&gt; is how many requests the window allows. &lt;code&gt;Window&lt;/code&gt; is how long until the counter resets.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;QueueLimit&lt;/code&gt; controls what happens after the requests are gone.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;0&lt;/code&gt;, the request is rejected immediately.&lt;br&gt;
With &lt;code&gt;10&lt;/code&gt;, up to ten requests wait for the next window instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;QueueProcessingOrder&lt;/code&gt; determines whether the oldest or newest waiting request receives the first available permit.&lt;/p&gt;

&lt;p&gt;For login, rejecting immediately is the right call. Queueing a brute-force attempt only keeps a connection open for the attacker.&lt;/p&gt;

&lt;p&gt;Over time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00:00  window opens, 5 requests available
10:00:01  requests 1 to 5   -&amp;gt; 200 OK
10:00:02  request 6         -&amp;gt; 429 Too Many Requests
10:00:59  request 7         -&amp;gt; 429 Too Many Requests
10:01:00  window resets, 5 requests available again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simplicity has a well-known weak spot: the window boundary.&lt;/p&gt;

&lt;p&gt;A client can send 5 requests at 10:00:59 and another 5 at 10:01:00.&lt;br&gt;
That is 10 requests in two seconds against a limit of "5 per minute".&lt;br&gt;
At the boundary, a fixed window lets through up to double the limit.&lt;/p&gt;

&lt;p&gt;For login, this is harmless. Ten password guesses instead of five changes nothing. For a downstream that can handle exactly 100 requests per minute, 200 in two seconds is a real problem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: the &lt;code&gt;AddFixedWindowLimiter&lt;/code&gt; helper creates one counter shared by every caller of the policy. With the code above, five failed logins from anyone lock the endpoint for the whole world. In the "Partitioning" section, I will show how to give every client its own counter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;When to use the fixed window:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Login, password reset, email sending:&lt;/strong&gt; cheap to count, and a small overshoot at the boundary does not matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple abuse caps:&lt;/strong&gt; "no more than N attempts, then wait" is easy to explain to users and support staff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory-sensitive scenarios:&lt;/strong&gt; the cheapest algorithm, one counter and one timestamp per partition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When not to use it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When the boundary burst matters:&lt;/strong&gt; a downstream with a hard cap, or a third-party API that bills per call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you want smooth traffic:&lt;/strong&gt; a fixed window does nothing to spread requests out inside the window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When the reset feels unfair:&lt;/strong&gt; a client that spreads 5 requests evenly over the minute still waits a full minute for the 6th.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the sliding window, we can fix the boundary problem.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/aspnet-core-rate-limiting-algorithms-explained" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/aspnet-core-rate-limiting-algorithms-explained&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>EF Core in Clean Architecture the Pragmatic Way</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Mon, 19 Oct 2026 04:34:47 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/ef-core-in-clean-architecture-the-pragmatic-way-27oa</link>
      <guid>https://dev.to/antonmartyniuk/ef-core-in-clean-architecture-the-pragmatic-way-27oa</guid>
      <description>&lt;p&gt;&lt;a href="https://antondevtips.com/blog/n-layered-vs-clean-vs-vertical-slice-architecture#clean-architecture" rel="noopener noreferrer"&gt;Clean Architecture&lt;/a&gt; is one of the most popular architectural styles in .NET today.&lt;/p&gt;

&lt;p&gt;Clean Architecture aims to separate the application's concerns into distinct layers, promoting high cohesion and low coupling.&lt;/p&gt;

&lt;p&gt;In the classic implementation you need to hide EF Core behind a &lt;strong&gt;Repository&lt;/strong&gt; so the Application layer never depends on the ORM.&lt;/p&gt;

&lt;p&gt;I followed that advice for years.&lt;br&gt;
But after many production projects, I changed my mind and adopted a more pragmatic approach to implementing Clean Architecture.&lt;/p&gt;

&lt;p&gt;In this post, I want to explain why I no longer wrap EF Core in a repository inside Clean Architecture, and why I think most teams should do the same.&lt;br&gt;
We will look at what the original Clean Architecture really says and how a pragmatic approach keeps the same boundaries in the code.&lt;/p&gt;

&lt;p&gt;In this post, we will explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Traditional Clean Architecture with a Repository&lt;/li&gt;
&lt;li&gt;The Hidden Cost of Repositories in Clean Architecture&lt;/li&gt;
&lt;li&gt;Pragmatic Clean Architecture: Use EF Core Directly in Application Use Cases&lt;/li&gt;
&lt;li&gt;What if We Switch a Database Later?&lt;/li&gt;
&lt;li&gt;When You Might Still Need a Custom Repository&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/ef-core-in-clean-architecture-the-pragmatic-way" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/ef-core-in-clean-architecture-the-pragmatic-way&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Traditional Clean Architecture with a Repository
&lt;/h2&gt;

&lt;p&gt;Clean Architecture splits an application into four layers, each with a clear role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain&lt;/strong&gt; — entities, value objects, and core business rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application&lt;/strong&gt; — use cases, commands, queries, and handlers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure&lt;/strong&gt; — implementations of external concerns: database, cache, message bus, email, identity provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Presentation&lt;/strong&gt; — the entry point: Web API, gRPC, GraphQL, MVC, Blazor, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key rule is the Dependency Rule: dependencies point inwards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain knows nothing about other layers&lt;/li&gt;
&lt;li&gt;Application depends only on the Domain.&lt;/li&gt;
&lt;li&gt;Infrastructure and Presentation depend on Application and Domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the traditional setup, the Application layer defines the Repository contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IShipmentRepository&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;Shipment&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExistsForOrderAsync&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;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IUnitOfWork&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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;Infrastructure implements it on top of EF Core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShipmentRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ShipmentsDbContext&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="n"&gt;IShipmentRepository&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="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExistsForOrderAsync&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;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;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="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnitOfWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ShipmentsDbContext&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="n"&gt;IUnitOfWork&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;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A use case handler injects the abstractions:&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;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateShipmentHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IShipmentRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IUnitOfWork&lt;/span&gt; &lt;span class="n"&gt;unitOfWork&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;CreateShipmentHandler&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="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;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CreateShipmentCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExistsForOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&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;cancellationToken&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;exists&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;ShipmentErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AlreadyExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&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="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapToShipment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;unitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&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;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapToResponse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Dependency Rule is satisfied, and you can switch the implementation in tests. So far, so good.&lt;br&gt;
But...&lt;/p&gt;
&lt;h2&gt;
  
  
  The Hidden Cost of Repositories in Clean Architecture
&lt;/h2&gt;

&lt;p&gt;I have built many systems with this pattern. Here is what tends to happen as the project grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A repository is an abstraction over an abstraction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EF Core's &lt;code&gt;DbContext&lt;/code&gt; already implements both the &lt;a href="https://antondevtips.com/blog/why-you-dont-need-a-repository-in-ef-core" rel="noopener noreferrer"&gt;Repository pattern&lt;/a&gt; and the &lt;a href="https://antondevtips.com/blog/why-you-dont-need-a-repository-in-ef-core" rel="noopener noreferrer"&gt;Unit of Work&lt;/a&gt; pattern.&lt;br&gt;
This is stated in the official &lt;code&gt;DbContext&lt;/code&gt; summary. When you put &lt;code&gt;IShipmentRepository&lt;/code&gt; on top of it, you are wrapping a wrapper.&lt;/p&gt;

&lt;p&gt;When we create a repository over EF Core, we create an abstraction over an abstraction, leading to over-engineered solutions.&lt;/p&gt;

&lt;p&gt;Each &lt;code&gt;DbSet&amp;lt;TEntity&amp;gt;&lt;/code&gt; in your DbContext represents a collection of entities, just like a typical repository.&lt;/p&gt;

&lt;p&gt;It allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query data using LINQ&lt;/li&gt;
&lt;li&gt;Add, update, and remove entities&lt;/li&gt;
&lt;li&gt;Project data to other types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Method explosion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What starts as four methods becomes 20:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IShipmentRepository&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;Shipment&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByOrderIdAsync&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByCarrierAsync&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetActiveAsync&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByStatusAsync&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCreatedBetweenAsync&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetWithItemsAsync&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="c1"&gt;// ... many more&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every new feature adds a new method.&lt;br&gt;
After a year, no one knows whether the method they need already exists, so they add another one that does almost the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cross-entity queries have no right place.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What if a feature needs a shipment, its order, plus the customer's address?&lt;br&gt;
Where does that method live? &lt;code&gt;IShipmentRepository&lt;/code&gt;? &lt;code&gt;IOrderRepository&lt;/code&gt;? A new &lt;code&gt;IShipmentOrderRepository&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Includes and projections leak or duplicate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some callers need &lt;code&gt;Include(x =&amp;gt; x.Items)&lt;/code&gt;, some do not.&lt;br&gt;
Some want a &lt;code&gt;ShipmentResponse&lt;/code&gt; projection, and some want the full entity.&lt;br&gt;
You end up with &lt;code&gt;GetByIdWithItemsAsync&lt;/code&gt;, &lt;code&gt;GetByIdSlimAsync&lt;/code&gt;, &lt;code&gt;GetByIdForReportAsync&lt;/code&gt; — or you load too much data on every call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Mocked-repository tests give false confidence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mocking &lt;code&gt;IShipmentRepository&lt;/code&gt; is fast, but it does not test how EF Core translates your LINQ-to-SQL queries, whether your &lt;code&gt;Include&lt;/code&gt; clauses work, or whether your database uniqueness constraints are enforced.&lt;br&gt;
The exact things that break in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Slower feature delivery.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every new use case touches three files: the repository interface, the repository implementation, and the handler.&lt;br&gt;
For a single new query, that is a lot of ceremony.&lt;/p&gt;

&lt;p&gt;Now, let's look at a pragmatic approach to using EF Core in Clean Architecture.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/ef-core-in-clean-architecture-the-pragmatic-way" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/ef-core-in-clean-architecture-the-pragmatic-way&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
    </item>
    <item>
      <title>How to Structure Production Apps with Vertical Slice Architecture in .NET in 2026</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Sat, 17 Oct 2026 08:16:35 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-structure-production-apps-with-vertical-slice-architecture-in-net-in-2026-3o7p</link>
      <guid>https://dev.to/antonmartyniuk/how-to-structure-production-apps-with-vertical-slice-architecture-in-net-in-2026-3o7p</guid>
      <description>&lt;p&gt;Vertical Slice Architecture (VSA) is one of the most popular ways to structure .NET projects today.&lt;/p&gt;

&lt;p&gt;It structures an application by features instead of technical layers.&lt;/p&gt;

&lt;p&gt;In a traditional layered project, you have a Controllers folder, a Services folder, and a Repositories folder (and probably way more).&lt;br&gt;
To change one feature, you jump between three or four folders (or even projects).&lt;/p&gt;

&lt;p&gt;In VSA, every feature is a single slice.&lt;br&gt;
The endpoint, business logic, validation, and data access for that feature live next to each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This brings several benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High cohesion within a feature&lt;/li&gt;
&lt;li&gt;Loose coupling between features&lt;/li&gt;
&lt;li&gt;Faster navigation in the codebase&lt;/li&gt;
&lt;li&gt;Independent changes to one feature do not take effect on the others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have built many systems with VSA and shipped them to production, and over the years, my layout has settled into a single shape that I now reuse in every new project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this post, we will explore:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How I Structure my Projects with Vertical Slice Architecture&lt;/li&gt;
&lt;li&gt;The Shared folder within a module&lt;/li&gt;
&lt;li&gt;Auto-registration of endpoints, and handlers&lt;/li&gt;
&lt;li&gt;Cross-slice side effects via events&lt;/li&gt;
&lt;li&gt;Inter-module communication via PublicApi&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-structure-production-apps-with-vertical-slice-architecture-in-dotnet-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-structure-production-apps-with-vertical-slice-architecture-in-dotnet-in-2026&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Structure my Projects with Vertical Slice Architecture
&lt;/h2&gt;

&lt;p&gt;There are many ways to structure a slice.&lt;br&gt;
In all of them, each vertical slice is placed in its own folder named after the use case.&lt;/p&gt;

&lt;p&gt;Here are the most popular ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Each class for a feature is in a separate file name&lt;/strong&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%2Fbsfhthwtcqwx4dvt6lx0.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%2Fbsfhthwtcqwx4dvt6lx0.png" alt="Screenshot_1" width="493" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Single file per slice with nested classes&lt;/strong&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;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateShipment&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Response&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;Validator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AbstractValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;MapEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebApplication&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&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;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/shipments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Handle&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;static&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;IResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Separate file with each concern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This code structure combines the advantages of the first two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You put the request and response modules together in the endpoint file&lt;/li&gt;
&lt;li&gt;You don't have too many files, as concerns are separated&lt;/li&gt;
&lt;li&gt;You don't have a static file that nests other classes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the exact approach I use in production.&lt;/p&gt;

&lt;p&gt;Each slice is a folder named after the use case.&lt;br&gt;
Inside the folder, I keep four files, each focused on one concern.&lt;/p&gt;

&lt;p&gt;For the "Create Shipment" use case, the layout looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Features/
└── CreateShipment/
    ├── CreateShipment.Endpoint.cs
    ├── CreateShipment.Handler.cs
    ├── CreateShipment.Mapping.cs
    └── CreateShipment.Validators.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a slice raises events that other modules react to, I add an Events subfolder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Features/
└── CreateShipment/
    ├── CreateShipment.Endpoint.cs
    ├── CreateShipment.Handler.cs
    ├── CreateShipment.Mapping.cs
    ├── CreateShipment.Validators.cs
    └── Events/
        ├── ShipmentCreatedEvent.cs
        ├── UpdateStockEventHandler.cs
        └── CreateCarrierEventHandler.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The naming convention uses a dot suffix: &lt;code&gt;{Slice}.Endpoint.cs&lt;/code&gt;, &lt;code&gt;{Slice}.Handler.cs&lt;/code&gt;, and so on.&lt;br&gt;
This keeps files easy to find in the IDE search and easy to scan in the file tree.&lt;/p&gt;

&lt;p&gt;I also extract cross-cutting concerns, such as validators and mappers, into separate files, so I don't clutter the main file with too many classes.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Endpoint File
&lt;/h3&gt;

&lt;p&gt;The endpoint file contains the request, response records and the Minimal API endpoint class.&lt;/p&gt;

&lt;p&gt;The endpoint is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsing the HTTP request&lt;/li&gt;
&lt;li&gt;Running validation&lt;/li&gt;
&lt;li&gt;Calling the handler&lt;/li&gt;
&lt;li&gt;Translating the handler result into an HTTP response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is &lt;code&gt;CreateShipment.Endpoint.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;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CreateShipmentRequest&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;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Address&lt;/span&gt; &lt;span class="n"&gt;Address&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;Carrier&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;ReceiverEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentItemRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&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;CreateShipmentApiEndpoint&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IApiEndpoint&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;MapEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebApplication&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&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;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RouteConsts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Handle&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;static&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;IResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&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;CreateShipmentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateShipmentRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ICreateShipmentHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;validationResult&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;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;validationResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidationProblem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validationResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDictionary&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsError&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToProblem&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;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint class implements a small marker interface called &lt;code&gt;IApiEndpoint&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IApiEndpoint&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;MapEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebApplication&lt;/span&gt; &lt;span class="n"&gt;app&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 interface lets me discover and register all endpoints in a single line at startup (more on that later).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;response.Errors.ToProblem()&lt;/code&gt; is an extension method that maps error types (Conflict, NotFound, Validation, and so on) to the appropriate HTTP status codes.&lt;/p&gt;

&lt;p&gt;Validation runs explicitly in the endpoint, before the handler is called.&lt;br&gt;
I &lt;a href="https://antondevtips.com/blog/refactoring-a-modular-monolith-without-mediatr-in-dotnet" rel="noopener noreferrer"&gt;do not use a MediatR pipeline behavior&lt;/a&gt;, like in many implementations.&lt;br&gt;
The validation flow stays simple and easy to follow.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Handler File
&lt;/h3&gt;

&lt;p&gt;I implement the business logic inside the Handler class.&lt;/p&gt;

&lt;p&gt;I use a &lt;a href="https://antondevtips.com/blog/refactoring-a-modular-monolith-without-mediatr-in-dotnet" rel="noopener noreferrer"&gt;manual handler pattern&lt;/a&gt;, which is a simple class without an interface.&lt;/p&gt;

&lt;p&gt;I do not use MediatR. Instead, I have a small marker interface called &lt;code&gt;IHandler&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every handler class in the project extends &lt;code&gt;IHandler&lt;/code&gt;.&lt;br&gt;
This lets me auto-register handlers with assembly scanning.&lt;/p&gt;

&lt;p&gt;Here is &lt;code&gt;CreateShipment.Handler.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;internal&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateShipmentHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ShipmentsDbContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IStockModuleApi&lt;/span&gt; &lt;span class="n"&gt;stockApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IEventPublisher&lt;/span&gt; &lt;span class="n"&gt;eventPublisher&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;CreateShipmentHandler&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;IHandler&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;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CreateShipmentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;shipmentExists&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;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="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;shipmentExists&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;"Shipment for order '{OrderId}' already exists"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&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;ShipmentErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AlreadyExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&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;stockRequest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateCheckStockRequest&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stockResponse&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;stockApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckStockAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stockRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;stockResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccess&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;"Stock check failed: {@Errors}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stockResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errors&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;stockResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errors&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;shipmentNumber&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;Faker&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Commerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ean8&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;shipment&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="nf"&gt;MapToShipment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&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;"Created shipment: {@Shipment}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shipment&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;shipmentCreatedEvent&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;ShipmentCreatedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&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;eventPublisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipmentCreatedEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapToResponse&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;static&lt;/span&gt; &lt;span class="n"&gt;CheckStockRequest&lt;/span&gt; &lt;span class="nf"&gt;CreateCheckStockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateShipmentRequest&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;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CheckStockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ProductStock&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;Product&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;Quantity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how simple the structure is.&lt;br&gt;
No extra interfaces, no commands, no command handlers, no mediator, no magic navigation to implementation, just a direct call to an exact class.&lt;/p&gt;

&lt;p&gt;Instead of throwing exceptions for expected errors, the handler uses a &lt;a href="https://antondevtips.com/blog/how-to-replace-exceptions-with-result-pattern-in-dotnet" rel="noopener noreferrer"&gt;Result Pattern&lt;/a&gt; and returns &lt;code&gt;Result&amp;lt;ShipmentResponse&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Errors for the module live in a single static class, so they stay consistent and easy to find:&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;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShipmentErrors&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;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ErrorPrefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Shipments"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="nf"&gt;NotFound&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;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ErrorPrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotFound&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="s"&gt;$"Shipment with number '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;' not found"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="nf"&gt;AlreadyExists&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;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Conflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ErrorPrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AlreadyExists&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="s"&gt;$"Shipment for order '&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="s"&gt;' already exists"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cross-module calls go through public APIs.&lt;/strong&gt;&lt;br&gt;
The handler does not reference any internals of the Stocks module.&lt;br&gt;
It only uses &lt;code&gt;IStockModuleApi&lt;/code&gt;, which is exposed by a separate &lt;code&gt;Modules.Stocks.PublicApi&lt;/code&gt; project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Events fire after the save.&lt;/strong&gt;&lt;br&gt;
Once the shipment is persisted, the handler publishes a &lt;code&gt;ShipmentCreatedEvent&lt;/code&gt;.&lt;br&gt;
Other slices and other modules can react to this event without the handler knowing about them.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Mapping and Validation Concerns
&lt;/h3&gt;

&lt;p&gt;I do all object mapping manually, in static extension methods (I don't use mapping libraries like AutoMapper, Mapster or Mapperly).&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;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateShipmentMappingExtensions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Shipment&lt;/span&gt; &lt;span class="nf"&gt;MapToShipment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;CreateShipmentRequest&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="c1"&gt;// Mapping code omitted for brevity&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ShipmentResponse&lt;/span&gt; &lt;span class="nf"&gt;MapToResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;Shipment&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Mapping code omitted for brevity&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Manual mapping has two big advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is explicit: you can navigate to it directly and know exactly what happens&lt;/li&gt;
&lt;li&gt;It has no reflection overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a mapping is reused across multiple slices, I move it into the &lt;code&gt;Shared/&lt;/code&gt; folder of the module instead of duplicating it.&lt;/p&gt;

&lt;p&gt;For validation, I use FluentValidation.&lt;br&gt;
I keep all validators for a slice in one file:&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;CreateShipmentRequestValidator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AbstractValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateShipmentRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateShipmentRequestValidator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shipment&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="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Carrier&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReceiverEmail&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cascade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CascadeMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Address must not be null"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetValidator&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;AddressValidator&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddressValidator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AbstractValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AddressValidator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;address&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="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;address&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="nf"&gt;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zip&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;NotEmpty&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;
  
  
  The Shared Folder Within a Module
&lt;/h2&gt;

&lt;p&gt;Some things are needed by more than one slice in the same module (of a &lt;a href="https://antondevtips.com/blog/building-a-modular-monolith-with-vertical-slice-architecture-in-dotnet" rel="noopener noreferrer"&gt;Modular Monolith&lt;/a&gt;): route constants, errors and response shapes for related slices.&lt;br&gt;
To avoid duplication, I put them in a &lt;code&gt;Shared/&lt;/code&gt; folder inside the module's Features project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Features/
├── CreateShipment/
├── DispatchShipment/
├── GetShipmentByNumber/
└── Shared/
    ├── Errors/
    │   └── ShipmentErrors.cs
    ├── Requests/
    │   └── ShipmentItemRequest.cs
    ├── Responses/
    │   ├── ShipmentResponse.cs
    │   └── ShipmentItemResponse.cs
    └── Routes/
        └── RouteConsts.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For all the API endpoint names, I used a static class called &lt;code&gt;RouteConsts&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;internal&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RouteConsts&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;BaseRoute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/api/shipments"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;GetByNumber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BaseRoute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CancelShipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BaseRoute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/cancel/&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;DispatchShipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BaseRoute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/dispatch/&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... and so on&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote a detailed guide on how to share code and avoid code duplication in Vertical Slices. Read it &lt;a href="https://antondevtips.com/blog/how-to-avoid-code-duplication-in-vertical-slice-architecture-in-dotnet" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-structure-production-apps-with-vertical-slice-architecture-in-dotnet-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-structure-production-apps-with-vertical-slice-architecture-in-dotnet-in-2026&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>40 Lessons I Learned in 12 Years as a .NET Developer</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:17:39 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/40-lessons-i-learned-in-12-years-as-a-net-developer-56je</link>
      <guid>https://dev.to/antonmartyniuk/40-lessons-i-learned-in-12-years-as-a-net-developer-56je</guid>
      <description>&lt;p&gt;17 years ago, I wrote my first line of C# code (.NET Framework 3.5).&lt;br&gt;
It was in the IT school.&lt;/p&gt;

&lt;p&gt;12 years ago, I wrote my first application in .NET and pushed it to production.&lt;br&gt;
It worked, but it had bad code. I did not know it at the time, but I would spend the next few years learning how to improve.&lt;/p&gt;

&lt;p&gt;Over more than 12 years, I have shipped buggy releases.&lt;br&gt;
I have over-engineered features nobody asked for.&lt;br&gt;
I have argued about tabs and spaces in pull requests.&lt;/p&gt;

&lt;p&gt;I have debugged distributed systems on weekends, missed estimates, and watched "future-proof" architectures rot into legacy.&lt;/p&gt;

&lt;p&gt;Some lessons were painful.&lt;br&gt;
Others were priceless.&lt;/p&gt;

&lt;p&gt;None of them came from a textbook; they came from real projects, real bugs, and real teammates who pushed me to be better.&lt;/p&gt;

&lt;p&gt;Today, I want to share the 40 most important takeaways I have learned over my career.&lt;br&gt;
Let's dive in.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/40-lessons-i-learned-in-12-years-as-a-dotnet-developer" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/40-lessons-i-learned-in-12-years-as-a-dotnet-developer&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  0. The best code is the one you don't write
&lt;/h2&gt;

&lt;p&gt;Every line of code is a future liability.&lt;br&gt;
It needs to be read, tested, debugged, deployed, and maintained for years.&lt;/p&gt;

&lt;p&gt;The cheapest, safest, fastest code is the code that never gets shipped.&lt;/p&gt;

&lt;p&gt;Before writing anything new, I ask myself: do we really need this? Is there a simpler way? Can an existing tool, library, or feature solve the problem instead?&lt;/p&gt;

&lt;p&gt;Many projects fail because they have too much code.&lt;br&gt;
Less code means fewer bugs, faster onboarding, and less time spent maintaining things nobody uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. You're not paid to write code - you're paid to solve problems
&lt;/h2&gt;

&lt;p&gt;Code is just a tool. The real product is the value you deliver to the business and to the users.&lt;/p&gt;

&lt;p&gt;I have seen developers spend weeks polishing a feature nobody asked for, while a critical bug stayed in production.&lt;br&gt;
I have seen teams build complex frameworks when a simple script that runs in 5 minutes would have done the job.&lt;/p&gt;

&lt;p&gt;The best engineers I know start with the problem, not with the technology.&lt;br&gt;
They ask questions, understand the context, and only then choose the right tool. And sometimes that tool is no code at all.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/40-lessons-i-learned-in-12-years-as-a-dotnet-developer" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/40-lessons-i-learned-in-12-years-as-a-dotnet-developer&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>career</category>
    </item>
    <item>
      <title>EF Core Optimization Guide: A Step-By-Step Guide to Faster Queries</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 13 Oct 2026 11:07:50 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/ef-core-optimization-guide-a-step-by-step-guide-to-faster-queries-1lgm</link>
      <guid>https://dev.to/antonmartyniuk/ef-core-optimization-guide-a-step-by-step-guide-to-faster-queries-1lgm</guid>
      <description>&lt;p&gt;&lt;strong&gt;"EF Core is slow, don't use it."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I heard this from so many developers.&lt;/p&gt;

&lt;p&gt;And almost every time, the cause is the same: EF Core makes it very easy to write a query that loads far more data than you need, and then you blame it for speed.&lt;/p&gt;

&lt;p&gt;Integration tests don't catch this.&lt;/p&gt;

&lt;p&gt;In development, your queries complete in a few milliseconds for a database with a few hundred rows.&lt;br&gt;
Then production grows to 2 million rows and the same endpoint takes 30 seconds.&lt;/p&gt;

&lt;p&gt;I've spent a lot of time optimizing EF Core in production systems, and I know a set of techniques that work.&lt;/p&gt;

&lt;p&gt;Some of them you need to apply daily, others - when you need to make it faster, or based on your specific datasets.&lt;/p&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/ef-core-optimization-guide-step-by-step" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/ef-core-optimization-guide-step-by-step&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 0: Measure Before You Optimize
&lt;/h2&gt;

&lt;p&gt;All examples in this article use a shipment tracking domain:&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;Shipment&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;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="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;Number&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;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;Carrier&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;ReceiverEmail&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;ShipmentStatus&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;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;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;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="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;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;Address&lt;/span&gt; &lt;span class="n"&gt;Address&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&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;class&lt;/span&gt; &lt;span class="nc"&gt;ShipmentItem&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;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="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;Name&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;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;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;Guid&lt;/span&gt; &lt;span class="n"&gt;ShipmentId&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;Shipment&lt;/span&gt; &lt;span class="n"&gt;Shipment&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;blockquote&gt;
&lt;p&gt;Note: all queries were tested on PostgreSQL. The same ideas apply to SQL Server, MySQL, and Oracle, though the generated SQL and the exact limits differ.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most performance work goes wrong before a single line of code changes.&lt;/p&gt;

&lt;p&gt;A query feels slow, someone puts a cache in front of it, and the endpoint gets faster.&lt;/p&gt;

&lt;p&gt;The slow query is still there. It just runs less often, and it will hurt again the moment the cache misses or the data changes more frequently than you expected.&lt;/p&gt;

&lt;p&gt;So the first step is finding out what SQL your LINQ actually produces.&lt;/p&gt;

&lt;p&gt;Turn on query logging in &lt;strong&gt;development&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentsDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="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="nf"&gt;UseNpgsql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDevelopment&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;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="nf"&gt;LogTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableSensitiveDataLogging&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableDetailedErrors&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;&lt;code&gt;LogTo&lt;/code&gt; writes every generated SQL statement to the console.&lt;br&gt;
&lt;code&gt;EnableSensitiveDataLogging&lt;/code&gt; includes the actual parameter values, so you can copy the statement straight into your database client and run it.&lt;/p&gt;

&lt;p&gt;You can also enable EF Core logging through your logging configuration - set the &lt;code&gt;Microsoft.EntityFrameworkCore.Database.Command&lt;/code&gt; category to &lt;code&gt;Information&lt;/code&gt; and let your logging framework (Serilog, NLog) write the SQL to a file or a sink of your choice.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Note: never enable &lt;code&gt;EnableSensitiveDataLogging&lt;/code&gt; in production. It writes parameter values, including emails, addresses, and anything else your queries filter on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you have the SQL, run it with an execution plan (&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; in PostgreSQL, or the actual execution plan in SQL Server).&lt;br&gt;
The plan tells you whether the database is scanning an entire table or using an index, and where the time is actually spent.&lt;/p&gt;

&lt;p&gt;Let's explore the query plan for this small query (from the screenshot above) that finds a Shipment by a number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shipment&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Three numbers are worth checking every time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Does the database use a full table scan or an index?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many rows the database returns.&lt;/strong&gt; If a query returns 50,000 rows to build a page of 100, the fix is in your LINQ in EF Core query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many round-trips your code makes.&lt;/strong&gt; One request per loop iteration is a different problem from one slow request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For anything you plan to tune repeatedly, write a benchmark with BenchmarkDotNet so you have a number to compare against.&lt;/p&gt;

&lt;p&gt;Now that you can see what's actually happening, let's explore ways we can improve performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetch Less Data
&lt;/h2&gt;

&lt;p&gt;Almost every slow EF Core query is slow for the same reason: it moves more data out of the database than the code needs.&lt;/p&gt;

&lt;p&gt;The three optimizations in this step all attack that.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add the Right Indexes
&lt;/h3&gt;

&lt;p&gt;In EF Core (migrations), it automatically creates indexes for primary keys and foreign keys.&lt;br&gt;
It doesn't create indexes for the columns you filter and sort on.&lt;/p&gt;

&lt;p&gt;If your shipment list screen filters by status and orders by creation date, the database has to scan the table on every request:&lt;/p&gt;

&lt;p&gt;Add the index in your entity configuration (the database migration will create it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShipmentConfiguration&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEntityTypeConfiguration&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EntityTypeBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&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="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;HasKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsUnique&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;HasIndex&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="k"&gt;new&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;Status&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;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composite index on &lt;code&gt;(Status, CreatedAt)&lt;/code&gt; matches the shape of the query.&lt;/p&gt;

&lt;p&gt;Column order matters here.&lt;br&gt;
The database can use this index to filter by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Status&lt;/code&gt; alone&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Status&lt;/code&gt; and then sort by &lt;code&gt;CreatedAt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it can't use it to sort by &lt;code&gt;CreatedAt&lt;/code&gt; alone.&lt;/p&gt;

&lt;p&gt;Put the column you filter on for equality first, and the column you range-filter or sort on second.&lt;/p&gt;

&lt;p&gt;When most of your queries only care about a small slice of the table, a filtered index is cheaper to maintain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"Status\" NOT IN (4, 5)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This indexes only the shipments that are still in flight and skips the delivered and canceled rows, which make up most of the table over time.&lt;/p&gt;

&lt;p&gt;Remember that indexes aren't free.&lt;br&gt;
Every index has to be updated on every insert, update, and delete that touches its columns.&lt;br&gt;
Add indexes only for the query shapes you actually run.&lt;/p&gt;

&lt;p&gt;Indexes help the database find your rows quickly. The next step is making sure you ask for fewer of them.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Filter Early, in the Database
&lt;/h3&gt;

&lt;p&gt;This is the mistake I see most often from young developers (sometimes AI does this, duh!), and it usually hides behind a helper 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="c1"&gt;// Loads every shipment in the table&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shipments&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;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&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;pending&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shipments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Processing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The moment you call &lt;code&gt;ToListAsync&lt;/code&gt;, the query is executed, and the rest happens in memory in your application.&lt;/p&gt;

&lt;p&gt;The database sends every row over the network. EF Core materializes each row into an object. And then your code throws most of them away.&lt;/p&gt;

&lt;p&gt;Keep the filter inside the &lt;code&gt;IQueryable&lt;/code&gt; so it becomes a &lt;code&gt;WHERE&lt;/code&gt; clause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pending&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;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Processing&lt;/span&gt;
        &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same applies to sorting, grouping, and aggregation. &lt;code&gt;Count()&lt;/code&gt;, &lt;code&gt;Any()&lt;/code&gt;, &lt;code&gt;Sum()&lt;/code&gt;, and &lt;code&gt;Max()&lt;/code&gt; all translate to SQL when you call them on an &lt;code&gt;IQueryable&lt;/code&gt;.&lt;br&gt;
It all runs in memory when you call them after calling &lt;code&gt;ToListAsync&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Project Only the Columns You Need
&lt;/h3&gt;

&lt;p&gt;Loading a full entity means loading every column it maps to, including the ones your endpoint never returns.&lt;/p&gt;

&lt;p&gt;If a &lt;code&gt;Shipment&lt;/code&gt; has 10 columns and your list screen shows 4 of them, you're moving more than twice the data you need.&lt;/p&gt;

&lt;p&gt;Project into a record or class instead:&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;ShipmentResponse&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Number&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;Carrier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&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;shipments&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;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Processing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&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;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Carrier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF Core translates the &lt;code&gt;Select&lt;/code&gt; into a &lt;code&gt;SELECT&lt;/code&gt; list with exactly those five columns.&lt;/p&gt;

&lt;p&gt;Projections do three things at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They cut the size of data the database reads&lt;/li&gt;
&lt;li&gt;They cut the size of data you transfer over the network from the database to your app&lt;/li&gt;
&lt;li&gt;They skip change tracking entirely, because there's no entity to track.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is consistently the single biggest win available.&lt;br&gt;
Projecting only the required columns can take your query somewhat from 10 seconds to 40 milliseconds (when reading 10,000 or more rows).&lt;/p&gt;

&lt;p&gt;You can explore the benchmarks in the &lt;a href="https://antondevtips.com/blog/ef-core-query-optimization-from-30-seconds-to-30-milliseconds" rel="noopener noreferrer"&gt;optimization challenge&lt;/a&gt; I did before with EF Core.&lt;/p&gt;

&lt;p&gt;Projections also compose well with aggregation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;summaries&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;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ShipmentSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database computes the item count and quantity sum. No &lt;code&gt;ShipmentItem&lt;/code&gt; objects are created in your application at all.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/ef-core-optimization-guide-step-by-step" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/ef-core-optimization-guide-step-by-step&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
    </item>
  </channel>
</rss>
