<?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>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>
    <item>
      <title>Building File-Based Apps in .NET: A Complete Guide With Multi-File Support</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:39:16 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/building-file-based-apps-in-net-a-complete-guide-with-multi-file-support-118g</link>
      <guid>https://dev.to/antonmartyniuk/building-file-based-apps-in-net-a-complete-guide-with-multi-file-support-118g</guid>
      <description>&lt;p&gt;For years, even the smallest C# program required a solution file, a project file, and a folder structure.&lt;br&gt;
While Python and JavaScript developers could create a single file and run it in seconds, .NET developers had to create a solution and add a &lt;code&gt;csproj&lt;/code&gt; file just to test an idea.&lt;/p&gt;

&lt;p&gt;That changed with .NET 10, which introduced file-based apps.&lt;br&gt;
Now you can run a C# file directly with &lt;code&gt;dotnet run&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And in April 2026 in &lt;strong&gt;.NET 11 Preview 3&lt;/strong&gt; we received a support for using multiple files in file-based apps with the &lt;code&gt;#:include&lt;/code&gt; directive.&lt;/p&gt;

&lt;p&gt;This finally makes C# a real option for scripts, automation, internal tooling, and quick prototypes - without the overhead of a full project.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;What Are File-Based Apps in .NET&lt;/li&gt;
&lt;li&gt;Adding NuGet Packages and SDKs with Directives&lt;/li&gt;
&lt;li&gt;Splitting File-Based Apps Across Multiple Files&lt;/li&gt;
&lt;li&gt;Building an HTTP Health-Check Tool&lt;/li&gt;
&lt;li&gt;Building a Minimal API with EF Core and SQLite&lt;/li&gt;
&lt;li&gt;Converting a File-Based App to a Full Project&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/building-file-based-apps-in-dotnet-with-multi-file-support" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/building-file-based-apps-in-dotnet-with-multi-file-support&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What Are File-Based Apps in .NET
&lt;/h2&gt;

&lt;p&gt;Traditionally, every C# application required three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solution file (&lt;code&gt;*.sln&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Project file (&lt;code&gt;*.csproj&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Source code (&lt;code&gt;*.cs&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even for a 10-line script, you had to create a new solution, add a project with &lt;code&gt;dotnet new console&lt;/code&gt;, wait for the scaffolding to finish, and only then write your actual code.&lt;/p&gt;

&lt;p&gt;Starting with .NET 10, you can skip all of that.&lt;br&gt;
You can create a single &lt;code&gt;.cs&lt;/code&gt; file and run it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run main.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No project file. No solution file. Just one file.&lt;/p&gt;

&lt;p&gt;This puts C# on equal footing with Python, JavaScript, Node.js, and other scripting languages.&lt;br&gt;
For CLI utilities, automation tasks, and one-off tools, this completely changes the workflow.&lt;/p&gt;

&lt;p&gt;Let's see the simplest possible example. Create a file called &lt;code&gt;hello.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello from a file-based app!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Today is &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;dddd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MMMM&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yyyy&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run hello.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice there is no &lt;code&gt;Main&lt;/code&gt; method, no &lt;code&gt;class Program&lt;/code&gt;, no &lt;code&gt;using&lt;/code&gt; statement.&lt;br&gt;
File-based apps build on top of top-level statements (introduced in C# 9) and implicit usings, so you can write code as if you were in a script.&lt;/p&gt;

&lt;p&gt;Behind the scenes, the .NET CLI compiles your file in a temporary location and runs the resulting binary.&lt;br&gt;
The first run takes a moment, but subsequent runs are cached and fast.&lt;/p&gt;

&lt;p&gt;I've used this many times in the last year for small tasks I would have otherwise written in Python (or created a solution project):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quick data transformations on JSON or CSV files&lt;/li&gt;
&lt;li&gt;Calling internal APIs to verify a deployment&lt;/li&gt;
&lt;li&gt;Running test scripts&lt;/li&gt;
&lt;li&gt;Parsing log files to extract patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Adding NuGet Packages and SDKs with Directives
&lt;/h2&gt;

&lt;p&gt;A real script usually needs more than the BCL.&lt;br&gt;
You probably want JSON parsing, HTTP calls, or database access.&lt;/p&gt;

&lt;p&gt;File-based apps support special &lt;code&gt;#&lt;/code&gt; directives at the top of your file to declare dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;#:package&lt;/code&gt; Directive&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;#:package&lt;/code&gt; to reference any NuGet package:&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="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="n"&gt;Newtonsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="m"&gt;13.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Newtonsoft.Json&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;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Anton"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Year&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2026&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;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonConvert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Formatting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Indented&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The format is &lt;code&gt;#:package PackageName@Version&lt;/code&gt;.&lt;br&gt;
The .NET CLI restores the package on first run, just like it would for a regular project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;#:sdk&lt;/code&gt; Directive&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;#:sdk&lt;/code&gt; when you need a specific SDK, such as the Web SDK for ASP.NET 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="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt; &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Web&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Hello from a single-file API!"&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;By default, file-based apps use &lt;code&gt;Microsoft.NET.Sdk&lt;/code&gt; (the standard SDK).&lt;br&gt;
Switching to &lt;code&gt;Microsoft.NET.Sdk.Web&lt;/code&gt; unlocks ASP.NET Core types like &lt;code&gt;WebApplication&lt;/code&gt; without any project setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;#:project&lt;/code&gt; Directive&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you need to call into an existing project (for example, a shared class library), use &lt;code&gt;#:project&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="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="p"&gt;../&lt;/span&gt;&lt;span class="n"&gt;MyLibrary&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;MyLibrary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;csproj&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;MyLibrary&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;calculator&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;Calculator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is helpful when you have a class library and want to write a quick tool that uses it, without creating yet another console project.&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/building-file-based-apps-in-dotnet-with-multi-file-support" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/building-file-based-apps-in-dotnet-with-multi-file-support&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>softwaredevelopment</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Make AI Write High-Quality Code in .NET</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 06 Oct 2026 11:10:00 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-make-ai-write-high-quality-code-in-net-2d4l</link>
      <guid>https://dev.to/antonmartyniuk/how-to-make-ai-write-high-quality-code-in-net-2d4l</guid>
      <description>&lt;p&gt;AI writes .NET code faster than anyone on your team.&lt;/p&gt;

&lt;p&gt;It can also write code that looks good but can fall apart during careful review.&lt;/p&gt;

&lt;p&gt;I have spent years improving the quality of code I write, long before the AI was around.&lt;br&gt;
I learned to write code that is easy to read and maintain, and I have been refining my skills ever since.&lt;/p&gt;

&lt;p&gt;Now I'm doing the same by teaching AI agents to write code with the same quality I do.&lt;/p&gt;

&lt;p&gt;But most developers fail to get AI to write high-quality, clean code.&lt;br&gt;
The reason is simple. The AI followed the rules it could see, and it never saw yours.&lt;/p&gt;

&lt;p&gt;Most developers try to fix this by putting the rules in the prompt.&lt;br&gt;
I tried that for many months.&lt;/p&gt;

&lt;p&gt;The rules work for a few files, maybe a few iterations in a session.&lt;br&gt;
But as the session goes on, the context gets loaded with information, and your rules start to be ignored.&lt;/p&gt;

&lt;p&gt;So only two things actually work reliably that I want to show you today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static compiler guardrails. If bad code doesn't compile, the agent sees the error, fixes it, and you never review that mistake at all.&lt;/li&gt;
&lt;li&gt;A skill for reviewing code. It's being used in another session, so it never competes with the original task.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;Set Project-Wide Standards with Directory.Build.props&lt;/li&gt;
&lt;li&gt;Add Static Code Analysis Packages&lt;/li&gt;
&lt;li&gt;Enforce Coding Standards with .editorconfig&lt;/li&gt;
&lt;li&gt;Centralize Package Management&lt;/li&gt;
&lt;li&gt;Code Reviews: AI First, Human Second&lt;/li&gt;
&lt;li&gt;Why Coding Rules in Prompts Don't Work&lt;/li&gt;
&lt;li&gt;Build a Clean Code Review Skill for Claude&lt;/li&gt;
&lt;li&gt;Make Sure the Review Catches Every Issue&lt;/li&gt;
&lt;/ol&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-make-ai-write-high-quality-code-in-dotnet" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-make-ai-write-high-quality-code-in-dotnet&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Set Project-Wide Standards with Directory.Build.props
&lt;/h2&gt;

&lt;p&gt;Every .NET solution should start with a &lt;code&gt;Directory.Build.props&lt;/code&gt; file. This file defines project-wide settings that apply to all projects in your solution.&lt;/p&gt;

&lt;p&gt;Without this file, you end up duplicating the same configuration across multiple &lt;code&gt;.csproj&lt;/code&gt; files.&lt;br&gt;
When you want to change a setting, you have to update every project file by hand. This leads to inconsistencies and wasted time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Directory.Build.props&lt;/code&gt; solves this problem by centralizing configuration in a single file.&lt;br&gt;
You create this file in the same directory as your &lt;code&gt;.sln&lt;/code&gt; file, and MSBuild applies it to all projects in the solution.&lt;/p&gt;

&lt;p&gt;Here is the configuration I use for every new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;AnalysisLevel&amp;gt;&lt;/span&gt;latest&lt;span class="nt"&gt;&amp;lt;/AnalysisLevel&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;AnalysisMode&amp;gt;&lt;/span&gt;All&lt;span class="nt"&gt;&amp;lt;/AnalysisMode&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;TreatWarningsAsErrors&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/TreatWarningsAsErrors&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;CodeAnalysisTreatWarningsAsErrors&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/CodeAnalysisTreatWarningsAsErrors&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;EnforceCodeStyleInBuild&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/EnforceCodeStyleInBuild&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Here is what each setting does:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nullable:&lt;/strong&gt; Enables nullable reference types, which help prevent null reference exceptions. The compiler warns you when you might be using a null value incorrectly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ImplicitUsings:&lt;/strong&gt; Adds common namespace imports to every file. You don't need to write &lt;code&gt;using System;&lt;/code&gt; or &lt;code&gt;using System.Linq;&lt;/code&gt; anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AnalysisLevel:&lt;/strong&gt; Sets the code analysis level to the latest version. You get the most recent code quality checks from Microsoft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AnalysisMode:&lt;/strong&gt; Turns on all code analysis rules. This gives you the most feedback on code quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TreatWarningsAsErrors:&lt;/strong&gt; Stops compilation if there are any warnings. This forces you to fix issues right away instead of letting them pile up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CodeAnalysisTreatWarningsAsErrors:&lt;/strong&gt; Applies the same strict treatment to code analysis warnings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EnforceCodeStyleInBuild:&lt;/strong&gt; Runs code style checks during build, not just in the IDE. Your CI/CD pipeline will catch style violations.&lt;/p&gt;

&lt;p&gt;Now here is why this file matters more than ever.&lt;/p&gt;

&lt;p&gt;An AI agent doesn't read your standards. It runs &lt;code&gt;dotnet build&lt;/code&gt;, and it reads the output.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TreatWarningsAsErrors&lt;/code&gt; turns every analyzer rule into a build error. The agent detects the error, fixes it, and rebuilds. That loop runs before you ever open the diff.&lt;/p&gt;

&lt;p&gt;Without this one line, the same rules become warnings. Warnings can get ignored by agents.&lt;/p&gt;

&lt;p&gt;That is the pattern behind everything below. A rule the build enforces gets followed every time, and a rule that only lives in a document gets skipped as soon as the agent has busy context.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add Static Code Analysis Packages
&lt;/h2&gt;

&lt;p&gt;Code quality is something you need to care about from day 1.&lt;br&gt;
It's easier to follow the best coding practices than to fix them later.&lt;/p&gt;

&lt;p&gt;For this, we can use static code analysis.&lt;/p&gt;

&lt;p&gt;Static code analyzers examine your code without running it.&lt;br&gt;
They catch common mistakes, enforce coding standards, and find potential bugs before they reach production.&lt;/p&gt;

&lt;p&gt;Some of the analyzers can even catch code quality issues.&lt;br&gt;
They can detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too long or too complex methods&lt;/li&gt;
&lt;li&gt;Methods with too many parameters&lt;/li&gt;
&lt;li&gt;Too much nesting&lt;/li&gt;
&lt;li&gt;Unused code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The analyzers run during compilation, so you get feedback right away in your IDE and in your CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;Add these analyzer packages to your &lt;code&gt;Directory.Build.props&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Project&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Nullable&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/Nullable&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ImplicitUsings&amp;gt;&lt;/span&gt;enable&lt;span class="nt"&gt;&amp;lt;/ImplicitUsings&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;AnalysisLevel&amp;gt;&lt;/span&gt;latest&lt;span class="nt"&gt;&amp;lt;/AnalysisLevel&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;AnalysisMode&amp;gt;&lt;/span&gt;All&lt;span class="nt"&gt;&amp;lt;/AnalysisMode&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;TreatWarningsAsErrors&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/TreatWarningsAsErrors&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;CodeAnalysisTreatWarningsAsErrors&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/CodeAnalysisTreatWarningsAsErrors&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;EnforceCodeStyleInBuild&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/EnforceCodeStyleInBuild&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Meziantou.Analyzer"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"2.0.257"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;IncludeAssets&amp;gt;&lt;/span&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;span class="nt"&gt;&amp;lt;/IncludeAssets&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"SonarAnalyzer.CSharp"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"10.16.0.128591"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;IncludeAssets&amp;gt;&lt;/span&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;span class="nt"&gt;&amp;lt;/IncludeAssets&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Roslynator.Analyzers"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"4.14.1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;IncludeAssets&amp;gt;&lt;/span&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;span class="nt"&gt;&amp;lt;/IncludeAssets&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"xunit.analyzers"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.26.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;IncludeAssets&amp;gt;&lt;/span&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;span class="nt"&gt;&amp;lt;/IncludeAssets&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/PackageReference&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Here is what each analyzer provides:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SonarAnalyzer.CSharp:&lt;/strong&gt; Focuses on code quality, security holes, and code smells. It finds complex methods, duplicated code, and potential bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meziantou.Analyzer:&lt;/strong&gt; Catches performance issues, security holes, and incorrect API usage. It has hundreds of rules covering async/await patterns, LINQ usage, string handling, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roslynator.Analyzers:&lt;/strong&gt; Provides code analysis and refactoring suggestions. It helps you write cleaner, more idiomatic C# code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;xunit.analyzers:&lt;/strong&gt; Makes sure you write tests correctly. It catches common testing mistakes, such as missing assertions or wrong test attributes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IncludeAssets&lt;/code&gt; configuration makes sure that these analyzers run during compilation but don't get included in your published application.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;TreatWarningsAsErrors&lt;/code&gt; enabled, your build will fail if any analyzer finds an issue. This might seem strict, but it stops technical debt from piling up.&lt;/p&gt;

&lt;p&gt;You should always aim for zero warnings in your project. Many developers ignore warnings, but warnings often point at real problems that will cause bugs later.&lt;/p&gt;

&lt;p&gt;These four packages carry thousands of rules between them. That's thousands of rules an AI agent has to satisfy, and none of them cost you a single line in a prompt.&lt;/p&gt;

&lt;p&gt;For more information about code quality best practices, check out this &lt;a href="https://antondevtips.com/blog/best-practices-for-increasing-code-quality-in-dotnet-projects" rel="noopener noreferrer"&gt;article&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-make-ai-write-high-quality-code-in-dotnet" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-make-ai-write-high-quality-code-in-dotnet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>ai</category>
    </item>
    <item>
      <title>Getting Started With Dapr for Building Cloud-Native Microservices in .NET</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Sat, 03 Oct 2026 07:00:06 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/getting-started-with-dapr-for-building-cloud-native-microservices-in-net-2i06</link>
      <guid>https://dev.to/antonmartyniuk/getting-started-with-dapr-for-building-cloud-native-microservices-in-net-2i06</guid>
      <description>&lt;p&gt;Building microservices is hard.&lt;/p&gt;

&lt;p&gt;Microservices don't just come with scalability, they come with mandatory complexity you pay for upfront and forever.&lt;/p&gt;

&lt;p&gt;You need to handle service-to-service communication, message brokers, state management, secrets, and more.&lt;br&gt;
Each of these concerns requires its own library, configuration, and expertise.&lt;/p&gt;

&lt;p&gt;What if there were a runtime that provided all these capabilities out of the box, regardless of the infrastructure you use?&lt;/p&gt;

&lt;p&gt;That is exactly what &lt;a href="https://dapr.io/" rel="noopener noreferrer"&gt;Dapr&lt;/a&gt; does.&lt;/p&gt;

&lt;p&gt;Dapr (Distributed Application Runtime) is a portable, event-driven runtime that makes it easier to build resilient microservices.&lt;br&gt;
It provides a set of building block APIs for common distributed systems challenges: pub/sub messaging, service invocation, state management, secrets, and more.&lt;/p&gt;

&lt;p&gt;The best part? Dapr is infrastructure-agnostic.&lt;br&gt;
You can swap RabbitMQ for Kafka, or Redis for PostgreSQL, without changing a single line of application code.&lt;/p&gt;

&lt;p&gt;Dapr is a free and open-source project; you can see its GitHub repo &lt;a href="https://github.com/dapr/dapr" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And when you combine Dapr with .NET Aspire, you get a powerful local development experience where containers, sidecars, and services are orchestrated for you with a single command.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;What is Dapr and How It Simplifies Microservice Development?&lt;/li&gt;
&lt;li&gt;Understanding Dapr Building Blocks&lt;/li&gt;
&lt;li&gt;How Dapr Components Work&lt;/li&gt;
&lt;li&gt;How to set up Dapr with .NET Aspire&lt;/li&gt;
&lt;li&gt;How to use Dapr to Publish Events to a Message Queue with Pub/Sub&lt;/li&gt;
&lt;li&gt;How to use Dapr to Consume Events from a Message Queue&lt;/li&gt;
&lt;li&gt;How to use Dapr to Call REST Services with Service Invocation&lt;/li&gt;
&lt;li&gt;How to use Dapr to Manage State with State Management&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/getting-started-with-dapr-for-building-cloud-native-microservices-in-dotnet" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/getting-started-with-dapr-for-building-cloud-native-microservices-in-dotnet&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What is Dapr and How It Simplifies Microservice Development?
&lt;/h2&gt;

&lt;p&gt;When you build microservices, you quickly face a set of recurring challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do services discover and call each other reliably?&lt;/li&gt;
&lt;li&gt;How do you publish and consume events across services?&lt;/li&gt;
&lt;li&gt;How do you store state without coupling to a specific database?&lt;/li&gt;
&lt;li&gt;How do you manage secrets securely?&lt;/li&gt;
&lt;li&gt;How do you handle retries, timeouts, and circuit breakers?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without Dapr, you solve each problem independently.&lt;br&gt;
You install a RabbitMQ client library for messaging, a Redis library for caching, a Vault client for secrets, and write custom retry logic.&lt;br&gt;
Each library has its own API, configuration, and behavior.&lt;/p&gt;

&lt;p&gt;You can use messaging libraries such as MassTransit, Wolverine, Rebus or Brighter, but they solve only the async messaging flows.&lt;br&gt;
You still need to implement caching, secrets, synchronous communication, service discovery, and others.&lt;/p&gt;

&lt;p&gt;Dapr solves all of these by providing a unified set of APIs that abstract these concerns.&lt;br&gt;
Your application talks to Dapr, and Dapr talks to the infrastructure.&lt;/p&gt;

&lt;p&gt;If you need to switch from RabbitMQ to Kafka for messaging, you change a configuration file (in production).&lt;br&gt;
Your application code stays exactly the same.&lt;/p&gt;
&lt;h3&gt;
  
  
  How the Sidecar Architecture Works
&lt;/h3&gt;

&lt;p&gt;Dapr runs as a &lt;a href="https://antondevtips.com/blog/12-essential-distributed-system-design-patterns-every-architect-should-know#7-sidecar-pattern" rel="noopener noreferrer"&gt;sidecar&lt;/a&gt; alongside your application.&lt;br&gt;
This means Dapr is a separate process (or container) that runs next to your service.&lt;/p&gt;

&lt;p&gt;Your application communicates with the Dapr sidecar over HTTP or gRPC on localhost.&lt;br&gt;
The sidecar handles the actual interaction with external infrastructure: message brokers, databases, secret stores, and other services.&lt;/p&gt;

&lt;p&gt;Here is how it works:&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%2F5d9i5fac1091xrrd57sy.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%2F5d9i5fac1091xrrd57sy.png" alt="Screenshot_3" width="799" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a Kubernetes environment, Dapr runs as a companion container within the same pod as your application.&lt;br&gt;
In local development, it runs as a separate process on your machine.&lt;/p&gt;

&lt;p&gt;This sidecar architecture provides several benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Language agnostic:&lt;/strong&gt; Dapr works with any language that can make HTTP or gRPC calls. Whether you write in C#, Go, Python, or Java, you use the same Dapr APIs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No SDK lock-in:&lt;/strong&gt; While Dapr provides SDKs for popular languages (including .NET), you can always fall back to raw HTTP calls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Independent lifecycle:&lt;/strong&gt; Your application and the Dapr sidecar can be updated independently. Upgrading Dapr does not require recompiling your application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consistent behavior:&lt;/strong&gt; Retry policies, timeouts, and circuit breakers are configured at the sidecar level. Every service in your system automatically receives the same resilience guarantees.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each service in your system has its own Dapr sidecar.&lt;br&gt;
When Service A wants to call Service B, it tells its local Dapr sidecar.&lt;br&gt;
The sidecar handles service discovery, routes the request to Service B's sidecar, which then forwards it to Service B.&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%2F5tv8j4uymnniqwksdtlp.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%2F5tv8j4uymnniqwksdtlp.png" alt="Screenshot_4" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means your services never communicate directly with each other.&lt;br&gt;
All communication goes through the Dapr sidecars, which gives you observability, security, and resilience for free.&lt;/p&gt;

&lt;p&gt;What about an overhead when talking to the sidecar on each request?&lt;br&gt;
This overhead is minimal, as all requests are done on the localhost.&lt;/p&gt;

&lt;p&gt;And the benefits you get outpass this small disadvantage.&lt;/p&gt;
&lt;h2&gt;
  
  
  Understanding Dapr Building Blocks
&lt;/h2&gt;

&lt;p&gt;Dapr organizes its capabilities into &lt;a href="https://docs.dapr.io/concepts/building-blocks-concept/" rel="noopener noreferrer"&gt;building blocks&lt;/a&gt;.&lt;br&gt;
Each building block is an independent API that addresses a specific challenge in distributed systems.&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%2Fmwca2rdqak1gwacf8g1u.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%2Fmwca2rdqak1gwacf8g1u.png" alt="Screenshot_1" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can use any combination of building blocks.&lt;/p&gt;

&lt;p&gt;Here are the main building blocks:&lt;/p&gt;
&lt;h3&gt;
  
  
  Service Invocation
&lt;/h3&gt;

&lt;p&gt;Enables services to call each other reliably.&lt;br&gt;
Dapr handles service discovery, retries, and distributed tracing automatically.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding URLs or using a service registry, you reference services by their &lt;strong&gt;App ID&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="c1"&gt;// Call the "hotels-api" service via Dapr&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rooms&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;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvokeMethodAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RoomAvailabilityResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;HttpMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"hotels-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"hotels/1/rooms/available"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dapr resolves the address of &lt;code&gt;hotels-api&lt;/code&gt; service, routes the request through the sidecar network, and returns the response.&lt;br&gt;
If the target service is temporarily unavailable, Dapr can retry the request based on your configured policies.&lt;/p&gt;
&lt;h3&gt;
  
  
  Publish and Subscribe
&lt;/h3&gt;

&lt;p&gt;Allows services to communicate asynchronously through queues and topics.&lt;br&gt;
Publishers send events to a topic, and subscribers receive them.&lt;/p&gt;

&lt;p&gt;The pub/sub building block supports at-least-once delivery guarantees.&lt;br&gt;
It works with any supported message broker: RabbitMQ, Kafka, Azure Service Bus, Redis Streams, and more.&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;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishEventAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"booking-created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bookingEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subscribers receive events through HTTP endpoints that Dapr calls automatically:&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;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/dapr/booking-created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"booking-created"&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;BookingCreatedEvent&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Process the event&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This model is fundamentally different from traditional background service consumers.&lt;br&gt;
Dapper Sidecar subscribes to a message queue, receives your message and pushes it to your application via HTTP POST requests.&lt;/p&gt;
&lt;h3&gt;
  
  
  State Management
&lt;/h3&gt;

&lt;p&gt;Provides key/value state storage with pluggable backends.&lt;br&gt;
You can use Redis, PostgreSQL, MongoDB, Azure Cosmos DB, or any other supported state store.&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;// Save state&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveStateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"statestore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Get state&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetStateAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"statestore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user-123"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Delete state&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeleteStateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"statestore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user-123"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state management building block also supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency control&lt;/strong&gt; with ETags for optimistic locking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL (Time-to-Live)&lt;/strong&gt; for automatic expiration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactions&lt;/strong&gt; for atomic operations across multiple keys&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Secrets Management
&lt;/h3&gt;

&lt;p&gt;Retrieves secrets from various secret stores without hardcoding credentials in your application.&lt;br&gt;
Supported stores include Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, Kubernetes secrets, and local file-based stores.&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;secrets&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;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSecretAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"secretstore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"payment-gateway"&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;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"api-key"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bindings
&lt;/h3&gt;

&lt;p&gt;Creates connections to external systems.&lt;br&gt;
&lt;strong&gt;Input bindings&lt;/strong&gt; trigger your application when an external event occurs.&lt;br&gt;
&lt;strong&gt;Output bindings&lt;/strong&gt; let your application invoke external systems.&lt;/p&gt;

&lt;p&gt;For example, you can use an output binding to write files to local storage, send emails, or trigger a cloud function:&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;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeBindingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"email-output"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emailContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Distributed Lock
&lt;/h3&gt;

&lt;p&gt;Provides mutual exclusion for scenarios where multiple service instances might access the same resource concurrently:&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;lockResponse&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;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"statestore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"room-lock:101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"owner-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expiryInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lockResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Exclusive access to the resource&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"statestore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"room-lock:101"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"owner-id"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other Building Blocks
&lt;/h3&gt;

&lt;p&gt;Dapr also provides building blocks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflows:&lt;/strong&gt; Long-running, persistent processes that span multiple services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actors:&lt;/strong&gt; Virtual actor model for stateful computations with single-threaded execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration:&lt;/strong&gt; Dynamic application configuration retrieval and subscription&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cryptography:&lt;/strong&gt; Encryption and decryption operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jobs:&lt;/strong&gt; Scheduled and triggered batch processing&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/getting-started-with-dapr-for-building-cloud-native-microservices-in-dotnet" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/getting-started-with-dapr-for-building-cloud-native-microservices-in-dotnet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>microservices</category>
    </item>
    <item>
      <title>How to Build Production-Ready Projects With Claude Code</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 02 Oct 2026 08:17:01 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-build-production-ready-projects-with-claude-code-ih7</link>
      <guid>https://dev.to/antonmartyniuk/how-to-build-production-ready-projects-with-claude-code-ih7</guid>
      <description>&lt;p&gt;In modern software development, there are two common paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You don't use AI, and you spend weeks on architecture decisions and months writing boilerplate code.&lt;/li&gt;
&lt;li&gt;You use AI heavily, shortcut every possible architecture decision, vibe-code 98% of your project, and ship quickly with low quality.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither of these paths is great.&lt;/p&gt;

&lt;p&gt;I want to tell you something about AI-assisted development in 2026.&lt;/p&gt;

&lt;p&gt;If you don't understand your programming language deeply, your main web framework, HTTP, databases, concurrency fundamentals, Architecture Patterns, tradeoffs...&lt;br&gt;
You can't tell when the code generated by Claude or Copilot is subtly wrong - and it will be, often enough to matter in production.&lt;/p&gt;

&lt;p&gt;AI amplifies what you already know.&lt;/p&gt;

&lt;p&gt;The stronger your fundamentals, the better your judgment on what Architecture Decisions to make, what to keep, what to fix, and what to throw away entirely.&lt;br&gt;
And going forward, developers with deep knowledge who operate AI, will be the most valued.&lt;/p&gt;

&lt;p&gt;Over the past year, I built a 10-step workflow that I use to ship quality software much quicker with AI.&lt;br&gt;
This workflow is designed to create well-architected, production-ready software with solid architecture, proper validation, error handling, security, and clean code.&lt;/p&gt;

&lt;p&gt;You should treat AI as your personal assistant who helps you in each phase of design and development.&lt;br&gt;
You guide it through a structured process where each step builds on the output of the previous one.&lt;/p&gt;

&lt;p&gt;In this post, I will share the complete workflow with detailed prompts you can adapt for your own projects.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Why most AI-generated code fails in production&lt;/li&gt;
&lt;li&gt;The 10-step workflow I use to ship production-quality software with AI&lt;/li&gt;
&lt;li&gt;Prompt engineering principles for production code&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-build-production-ready-projects-with-claude-code" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-build-production-ready-projects-with-claude-code&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Most AI-Generated Code Fails in Production
&lt;/h2&gt;

&lt;p&gt;The most common way developers use AI for coding is what I call "vibe coding."&lt;br&gt;
You open a chat, type "build me a REST API for managing orders," and get back something that compiles.&lt;/p&gt;

&lt;p&gt;The generated code typically has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Poor code quality&lt;/li&gt;
&lt;li&gt;Poor input validation (or just inline if-else statements)&lt;/li&gt;
&lt;li&gt;No error handling strategy (or try-catch in API endpoints)&lt;/li&gt;
&lt;li&gt;No consistent project structure&lt;/li&gt;
&lt;li&gt;No separation of concerns&lt;/li&gt;
&lt;li&gt;Hard-coded configuration&lt;/li&gt;
&lt;li&gt;No logging or observability&lt;/li&gt;
&lt;li&gt;No security considerations&lt;/li&gt;
&lt;li&gt;No tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This happens because the AI has no context about your project.&lt;br&gt;
It does not know your architecture decisions, your team's coding conventions, your error handling patterns, or your deployment constraints.&lt;br&gt;
It gives you a generic answer to a generic question.&lt;/p&gt;

&lt;p&gt;And even with the best AI models, like Claude Opus, with such generic prompts, you cannot guarantee that the output will be production-ready.&lt;/p&gt;

&lt;p&gt;Production-ready code means something specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It follows a consistent architecture across the entire codebase&lt;/li&gt;
&lt;li&gt;It has high-quality code&lt;/li&gt;
&lt;li&gt;It handles errors gracefully with a unified error response format&lt;/li&gt;
&lt;li&gt;It validates all user inputs&lt;/li&gt;
&lt;li&gt;It uses proper logging and has observability&lt;/li&gt;
&lt;li&gt;It follows your team's naming conventions and file organization&lt;/li&gt;
&lt;li&gt;It is structured for testing and maintenance&lt;/li&gt;
&lt;li&gt;It has a comprehensive test suite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To achieve this, we need a better process.&lt;br&gt;
When you give AI structured requirements, architecture decisions, and code examples to follow, the output quality increases dramatically.&lt;/p&gt;

&lt;p&gt;The workflow I share in this post mirrors how experienced developers already think:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Requirements&lt;/li&gt;
&lt;li&gt;Architecture&lt;/li&gt;
&lt;li&gt;Specification&lt;/li&gt;
&lt;li&gt;Writing code&lt;/li&gt;
&lt;li&gt;Reviewing code&lt;/li&gt;
&lt;li&gt;Testing code and deploying&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI accelerates each step, but the human remains the architect and decision-maker.&lt;br&gt;
I always said that the creative and decision-making part should always be on us, humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10-Step Workflow to Ship Production-Quality Software with AI
&lt;/h2&gt;

&lt;p&gt;I have created and refined this workflow over time to ship production-quality software with AI.&lt;/p&gt;

&lt;p&gt;The workflow has 5 phases and 10 steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Requirements and Planning&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 1: Define Business Requirements&lt;/li&gt;
&lt;li&gt;Step 2: Define Non-Functional Requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Architecture and Specification&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 3: Design the Architecture&lt;/li&gt;
&lt;li&gt;Step 4: Generate the Backend Specification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Backend Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 5: Code the Backend&lt;/li&gt;
&lt;li&gt;Step 6: Review and Refine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Frontend Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 7: Generate the Frontend Specification&lt;/li&gt;
&lt;li&gt;Step 8: Code the Frontend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 5: Deployment&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 9: Automate Deployment with CI/CD&lt;/li&gt;
&lt;li&gt;Step 10: Human Review, Test, and Deploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each phase produces artifacts that feed into the next phase.&lt;/p&gt;

&lt;p&gt;Let's walk through each step in detail.&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-build-production-ready-projects-with-claude-code" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-build-production-ready-projects-with-claude-code&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Real Cost of Returning the Identity Value in EF Core</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Wed, 30 Sep 2026 12:39:09 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/the-real-cost-of-returning-the-identity-value-in-ef-core-5il</link>
      <guid>https://dev.to/antonmartyniuk/the-real-cost-of-returning-the-identity-value-in-ef-core-5il</guid>
      <description>&lt;p&gt;When you insert thousands of rows into SQL Server with Entity Framework Core, performance drops quickly.&lt;/p&gt;

&lt;p&gt;Most developers blame change tracking.&lt;br&gt;
But there is another hidden cost that slows down every &lt;code&gt;SaveChanges&lt;/code&gt; call: returning database-generated identity values back to your entities.&lt;/p&gt;

&lt;p&gt;If your table has an auto-increment primary key (like most SQL Server tables), EF Core must ask the database for each generated identity after the insert.&lt;br&gt;
This round-trip limits batch size, forces extra SQL logic, and is the reason &lt;code&gt;SaveChanges&lt;/code&gt; struggles at scale.&lt;/p&gt;

&lt;p&gt;In this post, you will learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why &lt;code&gt;SaveChanges&lt;/code&gt; is slow when inserting 10,000 rows into a table with an identity column&lt;/li&gt;
&lt;li&gt;How EF Core handles generated value properties under the hood&lt;/li&gt;
&lt;li&gt;How &lt;a href="https://entityframework-extensions.net/" rel="noopener noreferrer"&gt;Entity Framework Extensions&lt;/a&gt; solves this problem with &lt;code&gt;BulkInsert&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How to make &lt;code&gt;BulkInsert&lt;/code&gt; faster with the &lt;code&gt;AutoMapOutputDirection&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;Why &lt;code&gt;BulkInsertOptimized&lt;/code&gt; is the fastest bulk insert method and how it compares with the other approaches&lt;/li&gt;
&lt;li&gt;When you should (and shouldn't) return identity values after a bulk insert&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All examples in this post were tested against a SQL Server database running in Docker, with benchmarks written in BenchmarkDotNet on .NET 10.&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/the-real-cost-of-returning-the-identity-value-in-ef-core" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/the-real-cost-of-returning-the-identity-value-in-ef-core&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem: Inserting 10,000 Rows with EF Core SaveChanges
&lt;/h2&gt;

&lt;p&gt;I built a small benchmark project to measure how long EF Core takes to insert 10,000 &lt;code&gt;Product&lt;/code&gt; rows into SQL Server.&lt;/p&gt;

&lt;p&gt;Here is the &lt;code&gt;Product&lt;/code&gt; entity:&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;Product&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;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="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;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;Description&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;Sku&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;Barcode&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;Category&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;Brand&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;Manufacturer&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&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;StockQuantity&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;decimal&lt;/span&gt; &lt;span class="n"&gt;Weight&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;bool&lt;/span&gt; &lt;span class="n"&gt;IsActive&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;true&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="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the Fluent API mapping that configures the identity column:&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;ProductConfiguration&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;Product&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;Product&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ValueGeneratedOnAdd&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;250&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasColumnType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"decimal(18,2)"&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sku&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Barcode&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Brand&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Manufacturer&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StockQuantity&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Weight&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasColumnType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"decimal(10,3)"&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HasDefaultValue&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsRequired&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;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedAt&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sku&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="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 &lt;code&gt;ValueGeneratedOnAdd()&lt;/code&gt; call tells EF Core that the database will generate &lt;code&gt;Id&lt;/code&gt; on insert.&lt;br&gt;
On SQL Server, this maps to an &lt;code&gt;IDENTITY(1,1)&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;Here is the baseline benchmark that uses the standard &lt;code&gt;SaveChangesAsync&lt;/code&gt; approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Benchmark&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;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateProducts&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="n"&gt;_dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use &lt;a href="https://github.com/bchavez/Bogus" rel="noopener noreferrer"&gt;Bogus&lt;/a&gt; to generate fake products:&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="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GenerateProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Faker&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&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;ProductName&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lorem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Paragraph&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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;Price&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"SKU-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Barcode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&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="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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&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;Categories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Brand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompanyName&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Manufacturer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompanyName&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StockQuantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Int&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="m"&gt;10_000&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.01m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;50m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.9f&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Past&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToUniversalTime&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;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Recent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;OrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.3f&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;ToUniversalTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my SQL Server instance running in Docker, this benchmark takes around 7,000 ms for 10,000 rows.&lt;br&gt;
For production APIs with a strict p99 SLA, that is way too slow.&lt;/p&gt;

&lt;p&gt;But here is the interesting part: &lt;strong&gt;the slowdown is not only change tracking&lt;/strong&gt;.&lt;br&gt;
A large part of the cost comes from EF Core's need to return the database-generated &lt;code&gt;Id&lt;/code&gt; back to every entity.&lt;/p&gt;

&lt;p&gt;Let's see why.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why EF Core Is Slow When Returning Identity Values
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;Product.Id&lt;/code&gt; is marked as &lt;code&gt;ValueGeneratedOnAdd&lt;/code&gt;, EF Core must perform two jobs during &lt;code&gt;SaveChanges&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert the row into the database&lt;/li&gt;
&lt;li&gt;Retrieve the generated identity value and assign it back to the in-memory &lt;code&gt;Product.Id&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On SQL Server, EF Core uses a &lt;code&gt;MERGE&lt;/code&gt; statement with an &lt;code&gt;OUTPUT&lt;/code&gt; clause to batch multiple inserts into one command and still return each new identity.&lt;/p&gt;

&lt;p&gt;The generated SQL looks roughly like this:&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="n"&gt;MERGE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;OUTPUT&lt;/span&gt; &lt;span class="n"&gt;INSERTED&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is clever, but it has hard limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 2,100 parameter cap&lt;/strong&gt; - SQL Server allows a maximum of 2,100 parameters per command. With 14 mapped columns, that is roughly 100-200 rows per batch. For 10,000 &lt;code&gt;Product&lt;/code&gt; rows, EF Core needs many round-trips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameter binding cost&lt;/strong&gt; - Every parameter has to be created, bound, and sent over the network. For 10,000 rows with a few columns, that is tens of thousands of parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change tracker overhead&lt;/strong&gt; - After each batch, EF Core updates the change tracker and assigns the returned identity values to each tracked entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log writes and index updates&lt;/strong&gt; - The database writes to the transaction log and updates indexes for every batch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You might think: "What if I disable the identity column?"&lt;br&gt;
That removes the OUTPUT round-trip, but now you are responsible for generating unique IDs, and you lose the simplicity of auto-increment keys.&lt;/p&gt;

&lt;p&gt;For a deeper explanation of how EF Core handles generated values, see &lt;a href="https://www.learnentityframeworkcore.com/model/generated-value#the-truth-about-how-ef-core-handles-generated-value-properties" rel="noopener noreferrer"&gt;this article on LearnEntityFrameworkCore&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is a better solution.&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/the-real-cost-of-returning-the-identity-value-in-ef-core" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/the-real-cost-of-returning-the-identity-value-in-ef-core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
    </item>
    <item>
      <title>How to Model Relationships in NoSQL Databases</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 29 Sep 2026 11:12:58 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-model-relationships-in-nosql-databases-3971</link>
      <guid>https://dev.to/antonmartyniuk/how-to-model-relationships-in-nosql-databases-3971</guid>
      <description>&lt;p&gt;After you move from a SQL database to NoSQL, you usually forget about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Joins&lt;/li&gt;
&lt;li&gt;Foreign keys&lt;/li&gt;
&lt;li&gt;Normalization and Denormalization&lt;/li&gt;
&lt;li&gt;Database migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yet, most developers have no idea how to model relationships in NoSQL correctly.&lt;/p&gt;

&lt;p&gt;Most developers come to MongoDB (for example) after years of SQL.&lt;br&gt;
So they model the data the way SQL taught them.&lt;/p&gt;

&lt;p&gt;Customers, Orders and Order Items - all go in multiple collections by habit.&lt;br&gt;
Every link becomes an ID, just like a foreign key.&lt;/p&gt;

&lt;p&gt;Then they write the joins by hand in C#, because MongoDB has no &lt;code&gt;JOIN&lt;/code&gt; keyword.&lt;/p&gt;

&lt;p&gt;The result is a document database that behaves like a slow relational one.&lt;br&gt;
A single read needs four database round trips.&lt;/p&gt;

&lt;p&gt;I've run MongoDB in production for years.&lt;br&gt;
Almost every performance problem I've seen there came from the wrong data model.&lt;/p&gt;

&lt;p&gt;Today you will learn how to model relationships in a document database.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Why Relationships Work Differently in NoSQL&lt;/li&gt;
&lt;li&gt;Embed or Reference: The Core Decision&lt;/li&gt;
&lt;li&gt;One-to-One: Embed by Default&lt;/li&gt;
&lt;li&gt;One-to-Many: The Three Sizes

&lt;ul&gt;
&lt;li&gt;One-to-Few: Embed the Array&lt;/li&gt;
&lt;li&gt;One-to-Many: Reference from the Child&lt;/li&gt;
&lt;li&gt;One-to-Zillions: Never Grow an Array Forever&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Many-to-Many: Orders and Products&lt;/li&gt;
&lt;li&gt;Reading Related Data Without Joins&lt;/li&gt;
&lt;li&gt;Keeping Duplicated Data Correct&lt;/li&gt;
&lt;li&gt;4 Mistakes That Break NoSQL Data Models&lt;/li&gt;
&lt;li&gt;The Same Rules in DynamoDB, Cassandra and Cosmos DB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the code in this article I show using MongoDB and C#, but the rules apply to any NoSQL database.&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/how-to-model-relationships-in-nosql-databases" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-model-relationships-in-nosql-databases&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Relationships Work Differently in NoSQL
&lt;/h2&gt;

&lt;p&gt;In SQL databases, you have tables and rows.&lt;br&gt;
In NoSQL databases, you have collections and documents.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;document&lt;/strong&gt; is one record. It's a JSON-like object that can contain other objects and arrays.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;collection&lt;/strong&gt; is a group of documents. It's the closest thing MongoDB has to a table.&lt;/p&gt;

&lt;p&gt;MongoDB has no foreign key constraints. Nothing stops you from deleting a customer who still has orders.&lt;/p&gt;

&lt;p&gt;There are no cascade deletes either. Delete a parent, and the children stay behind as orphans.&lt;/p&gt;

&lt;p&gt;And a normal query can't join. MongoDB can join inside the aggregation pipeline, but that isn't how you read data most of the time.&lt;/p&gt;

&lt;p&gt;In SQL you design the tables first and write the queries later. You normalize the data, which means you store each fact once and link to it.&lt;br&gt;
The query planner works out the rest.&lt;/p&gt;

&lt;p&gt;In a document database, you do the opposite. &lt;strong&gt;You shape the documents around the queries you already know you need.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the same shipment in both worlds. In SQL it's three tables:&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="n"&gt;shipments&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;number&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;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;shipment_address&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment_id&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="n"&gt;city&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="n"&gt;shipment_items&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;shipment_id&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;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading one shipment means joining all three.&lt;/p&gt;

&lt;p&gt;In MongoDB, it's one document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8e3b1a6c-2f45-4d17-9b0a-51c9e2f7a410"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345678"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-9931"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dispatched"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"street"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Main St 15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warsaw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00-001"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"product"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Laptop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"product"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One read returns the whole shipment, without joins.&lt;/p&gt;

&lt;p&gt;Side by side, the two shapes look 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;SQL: 3 tables                                 MongoDB: 1 document
-------------------------------------------   ---------------------------------
shipments        (id, number, status)         shipment { number, status,
shipment_address (shipment_id, street,          address: { street, city, zip },
                  city, zip)                    items: [ { product, quantity } ]
shipment_items   (id, shipment_id,            }
                  product, quantity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Embed or Reference: The Core Decision
&lt;/h2&gt;

&lt;p&gt;In a NoSQL database, you have just two tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embed&lt;/strong&gt; means you put the related data inside the parent document. The address above is embedded in the shipment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference&lt;/strong&gt; means you store an id and keep the data in its own collection. That's the same idea as a foreign key, except the database doesn't enforce it.&lt;/p&gt;

&lt;p&gt;Every relationship in your model is one of those two. Picking the right one is most of the work.&lt;/p&gt;

&lt;p&gt;Here's the rule I use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embed when&lt;/th&gt;
&lt;th&gt;Reference when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You read both together almost every time&lt;/td&gt;
&lt;td&gt;The child is often read on its own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The list has a known upper limit&lt;/td&gt;
&lt;td&gt;The list can grow infinitely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The child changes when the parent changes&lt;/td&gt;
&lt;td&gt;The child is updated on its own schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The child belongs to one parent&lt;/td&gt;
&lt;td&gt;Many parents share the same child&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The data is small&lt;/td&gt;
&lt;td&gt;The data is large&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One hard limit settles a lot of arguments. A MongoDB document can't be bigger than &lt;strong&gt;16 MB&lt;/strong&gt;.&lt;br&gt;
If a list can outgrow that, it can't be embedded, and there's nothing left to discuss.&lt;/p&gt;

&lt;p&gt;Speed pushes the other way. Embedded data comes back in the same read, so it costs nothing extra to fetch.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: A good model embeds some things and references others. Don't pick one style and apply it everywhere.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now I will show you the three shapes a relationship can take.&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-model-relationships-in-nosql-databases" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-model-relationships-in-nosql-databases&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>mongodb</category>
    </item>
    <item>
      <title>How to Add JWT Authentication to SignalR Hubs in ASP.NET Core</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 25 Sep 2026 08:11:41 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-add-jwt-authentication-to-signalr-hubs-in-aspnet-core-5d03</link>
      <guid>https://dev.to/antonmartyniuk/how-to-add-jwt-authentication-to-signalr-hubs-in-aspnet-core-5d03</guid>
      <description>&lt;p&gt;SignalR makes real-time communication in .NET applications simple.&lt;br&gt;
You can send live data, notifications, and streaming updates to clients with minimal code.&lt;/p&gt;

&lt;p&gt;By default, SignalR hubs are accessible to all clients.&lt;br&gt;
Clients can connect, call hub methods, and receive messages without authentication.&lt;/p&gt;

&lt;p&gt;In production, you may need to know who is connecting to your hub, what they are allowed to do, and how to reject unauthorized access.&lt;/p&gt;

&lt;p&gt;JWT authentication is the standard for securing SignalR hubs in any environment, including browsers.&lt;br&gt;
It works with WebSockets, &lt;a href="https://antondevtips.com/blog/real-time-server-sent-events-in-asp-net-core" rel="noopener noreferrer"&gt;Server-Sent Events&lt;/a&gt;, and Long Polling transports.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Why SignalR Needs Authentication&lt;/li&gt;
&lt;li&gt;Setting Up JWT Authentication for SignalR&lt;/li&gt;
&lt;li&gt;How to Stream Events with SignalR for a Given User&lt;/li&gt;
&lt;li&gt;Role-Based Authorization on Hub Methods&lt;/li&gt;
&lt;li&gt;Connecting from a JavaScript Client&lt;/li&gt;
&lt;li&gt;Connecting from a .NET Client&lt;/li&gt;
&lt;li&gt;Security Best Practices for SignalR&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-add-jwt-authentication-to-signalr-hubs-in-aspnetcore" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-add-jwt-authentication-to-signalr-hubs-in-aspnetcore&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why SignalR Needs Authentication
&lt;/h2&gt;

&lt;p&gt;SignalR uses multiple transport protocols to maintain real-time connections: WebSockets, &lt;a href="https://antondevtips.com/blog/real-time-server-sent-events-in-asp-net-core" rel="noopener noreferrer"&gt;Server-Sent Events&lt;/a&gt;, and Long Polling.&lt;/p&gt;

&lt;p&gt;By default, any client can connect to a SignalR hub and call its methods.&lt;/p&gt;

&lt;p&gt;This creates several problems in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No user identity&lt;/strong&gt; - You can't tell who is connected or send messages to specific users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No access control&lt;/strong&gt; - Anyone can call any hub method, including admin operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No audit trail&lt;/strong&gt; - You can't log which users performed which actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security exposure&lt;/strong&gt; - Sensitive data could be streamed to unauthorized clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why JWT and not Cookies?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cookies work well for browser-based apps where users log in through a web form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But JWT tokens are the better choice when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your clients are mobile apps, desktop apps, or SPAs.&lt;/li&gt;
&lt;li&gt;You need cross-platform authentication.&lt;/li&gt;
&lt;li&gt;Your API and frontend run on different domains.&lt;/li&gt;
&lt;li&gt;You're building microservices that need stateless authentication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one important detail about how tokens are handled in SignalR connections.&lt;br&gt;
In standard REST APIs, the client sends the JWT token in the &lt;code&gt;Authorization&lt;/code&gt; HTTP header.&lt;br&gt;
But SignalR can't always do this.&lt;/p&gt;

&lt;p&gt;When using WebSockets or Server-Sent Events in a browser, the browser API does not allow setting custom headers.&lt;br&gt;
Instead, the token is sent as a query string parameter: &lt;code&gt;?access_token=&amp;lt;token&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This means you need extra configuration on the server to read the token from the query string.&lt;br&gt;
We will cover this setup in the next section.&lt;/p&gt;

&lt;p&gt;For a deeper dive into authentication and authorization in ASP.NET Core, read my article on &lt;a href="https://antondevtips.com/blog/authentication-and-authorization-best-practices-in-aspnetcore" rel="noopener noreferrer"&gt;Authentication and Authorization Best Practices&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting Up JWT Authentication for SignalR
&lt;/h2&gt;

&lt;p&gt;Let's build the backend for the stock price streaming application.&lt;/p&gt;

&lt;p&gt;In Modern .NET versions, you no longer need to install the SignalR NuGet package.&lt;br&gt;
SignalR is now included in the ASP.NET Core Web SDK.&lt;/p&gt;

&lt;p&gt;Here is how you can configure SignalR in your project:&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;AddSignalR&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="c1"&gt;// Map the SignalR hub&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;MapHub&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;StockPriceHub&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hubs/stocks"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to configure JWT bearer authentication and add the &lt;code&gt;OnMessageReceived&lt;/code&gt; event handler that extracts the token from the query string for SignalR connections:&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;AddAuthentication&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;DefaultAuthenticateScheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&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;DefaultChallengeScheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&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;AddJwtBearer&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&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;SymmetricSecurityKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authConfig&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="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TokenValidationParameters&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;TokenValidationParameters&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ValidateIssuer&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;ValidateAudience&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;ValidateLifetime&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;ValidateIssuerSigningKey&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;ValidIssuer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Issuer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ValidAudience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Audience&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IssuerSigningKey&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="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JwtBearerEvents&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;OnMessageReceived&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&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;accessToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"access_token"&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;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&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;Path&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWithSegments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hubs"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accessToken&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;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&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;When a browser connects to a SignalR hub via WebSockets or SSE, it can't send the &lt;code&gt;Authorization&lt;/code&gt; header.&lt;br&gt;
The JavaScript client sends the token as &lt;code&gt;?access_token=&amp;lt;token&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;OnMessageReceived&lt;/code&gt; event fires before the JWT middleware validates the token.&lt;/p&gt;

&lt;p&gt;Token extraction is limited to paths starting with &lt;code&gt;/hubs&lt;/code&gt;.&lt;br&gt;
This avoids reading tokens from query strings on other endpoints.&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-add-jwt-authentication-to-signalr-hubs-in-aspnetcore" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-add-jwt-authentication-to-signalr-hubs-in-aspnetcore&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>Getting Started With Event Sourcing in .NET With Marten and PostgreSQL</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 25 Sep 2026 04:42:41 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/getting-started-with-event-sourcing-in-net-with-marten-and-postgresql-2bpj</link>
      <guid>https://dev.to/antonmartyniuk/getting-started-with-event-sourcing-in-net-with-marten-and-postgresql-2bpj</guid>
      <description>&lt;p&gt;Most applications store data by saving the current state to a database.&lt;/p&gt;

&lt;p&gt;When a customer adds an item to a shopping cart, you update a row in the database.&lt;br&gt;
The previous state is lost.&lt;br&gt;
You only see the cart's current state, with all history lost.&lt;/p&gt;

&lt;p&gt;This works for many applications. But what happens when you need to know the full history of changes?&lt;br&gt;
What if you need to rebuild past states, debug complex business flows, or generate analytics from historical data?&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;Event Sourcing&lt;/strong&gt; pattern is useful.&lt;/p&gt;

&lt;p&gt;Event Sourcing stores every change as an immutable event.&lt;br&gt;
Instead of overwriting data, you append new events to a stream.&lt;br&gt;
The current state is derived by replaying these events.&lt;/p&gt;

&lt;p&gt;In this post, I will show you how to build an Event Sourcing application in .NET using the Marten library and PostgreSQL.&lt;/p&gt;

&lt;p&gt;We will build a Shopping Cart that demonstrates all the core Event Sourcing concepts: events, streams, aggregates, commands, and projections.&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;What is Event Sourcing&lt;/li&gt;
&lt;li&gt;Overview of the Shopping Cart Application&lt;/li&gt;
&lt;li&gt;Setting Up Marten with PostgreSQL&lt;/li&gt;
&lt;li&gt;Defining Domain Events&lt;/li&gt;
&lt;li&gt;Building the Aggregate&lt;/li&gt;
&lt;li&gt;Writing Commands: Appending Events to Streams&lt;/li&gt;
&lt;li&gt;Building Read Models with Projections&lt;/li&gt;
&lt;li&gt;The Query Side: Reading Projected Data&lt;/li&gt;
&lt;li&gt;When to Use Event Sourcing&lt;/li&gt;
&lt;li&gt;Summary&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/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Event Sourcing
&lt;/h2&gt;

&lt;p&gt;In traditional CRUD applications, you store the current state of an entity in a database row.&lt;/p&gt;

&lt;p&gt;When a customer adds a product to a shopping cart, you insert or update a row in the &lt;code&gt;CartItems&lt;/code&gt; table.&lt;br&gt;
When they remove an item, you delete that row.&lt;br&gt;
The database always shows the latest state.&lt;/p&gt;

&lt;p&gt;This approach is simple and works well for many use cases. But it has a fundamental limitation: you lose history.&lt;/p&gt;

&lt;p&gt;If a customer added 5 items, removed 2, changed quantities 3 times, and then checked out, all you see in the database is the final state.&lt;br&gt;
You cannot answer questions like "What did the cart look like 10 minutes ago?" or "Which items were removed before checkout?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event Sourcing takes a different approach.&lt;/strong&gt; Instead of storing the current state, you store the sequence of events that led to the current state.&lt;/p&gt;

&lt;p&gt;For a shopping cart, these events might look like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CartCreated&lt;/strong&gt; - Customer started a new cart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ItemAdded&lt;/strong&gt; - Wireless Mouse, quantity 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ItemAdded&lt;/strong&gt; - Mechanical Keyboard, quantity 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ItemRemoved&lt;/strong&gt; - Wireless Mouse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ItemAdded&lt;/strong&gt; - Gaming Mouse, quantity 2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CartCheckedOut&lt;/strong&gt; - Customer completed the purchase&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The current state of the cart is derived by replaying all these events from the beginning. This sequence of events is called an &lt;strong&gt;event stream&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Concepts
&lt;/h3&gt;

&lt;p&gt;Event Sourcing introduces several core concepts that work together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Events&lt;/strong&gt; are immutable facts that describe something that happened in the past.&lt;br&gt;
An event is never deleted or modified.&lt;br&gt;
Once &lt;code&gt;ItemAdded&lt;/code&gt; is recorded, it stays in the stream forever.&lt;br&gt;
Events use past tense names: &lt;code&gt;CartCreated&lt;/code&gt;, &lt;code&gt;ItemAdded&lt;/code&gt;, &lt;code&gt;OrderConfirmed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streams&lt;/strong&gt; are ordered sequences of events that belong to the same entity.&lt;br&gt;
A shopping cart has its own stream. Each stream has a unique identifier, typically a GUID.&lt;br&gt;
All events for cart &lt;code&gt;abc-123&lt;/code&gt; are stored together in correct order as they appear in time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aggregates&lt;/strong&gt; are domain objects that derive their current state by replaying events from a stream.&lt;br&gt;
The &lt;code&gt;ShoppingCart&lt;/code&gt; aggregate starts empty and applies each event one by one.&lt;br&gt;
After replaying all events, the aggregate reflects the current state.&lt;br&gt;
Aggregates also enforce business rules before new events are appended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Projections&lt;/strong&gt; are read models built from events.&lt;br&gt;
While aggregates give you the state of a single stream, projections can combine data from multiple streams to create optimized views for querying.&lt;br&gt;
For example, a "Product Popularity" projection might track how many times each product was added across all carts.&lt;/p&gt;

&lt;p&gt;Event Sourcing is not a replacement for CRUD.&lt;br&gt;
It is a different approach that shines in specific scenarios.&lt;br&gt;
We will explore when to use it at the end of this post.&lt;/p&gt;

&lt;p&gt;Now let's build a real application to see these concepts in action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of the Shopping Cart Application
&lt;/h2&gt;

&lt;p&gt;We will build a Shopping Cart API that demonstrates all the core Event Sourcing concepts.&lt;/p&gt;

&lt;p&gt;Here is what our application supports:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commands (write side):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new cart&lt;/li&gt;
&lt;li&gt;Add items to the cart&lt;/li&gt;
&lt;li&gt;Remove items from the cart&lt;/li&gt;
&lt;li&gt;Change item quantities&lt;/li&gt;
&lt;li&gt;Checkout the cart&lt;/li&gt;
&lt;li&gt;Confirm the order&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Queries (read side):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get cart details (from an inline projection)&lt;/li&gt;
&lt;li&gt;Get product popularity rankings (from an async cross-stream projection)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Domain events:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CartCreated&lt;/li&gt;
&lt;li&gt;ItemAdded&lt;/li&gt;
&lt;li&gt;ItemRemoved&lt;/li&gt;
&lt;li&gt;ItemQuantityChanged&lt;/li&gt;
&lt;li&gt;CartCheckedOut&lt;/li&gt;
&lt;li&gt;OrderConfirmed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;

&lt;p&gt;The application follows &lt;a href="https://antondevtips.com/blog/vertical-slice-architecture-the-best-ways-to-structure-your-project" rel="noopener noreferrer"&gt;Vertical Slice Architecture&lt;/a&gt; with three projects:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ShoppingCart.Domain&lt;/strong&gt; contains the pure domain model: event records and the aggregate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ShoppingCart.Features&lt;/strong&gt; contains the vertical slices: each feature has its own folder with an endpoint, handler, and optional validator. It also contains the projections. This project uses Marten, Carter, ErrorOr, and FluentValidation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ShoppingCart.WebApi&lt;/strong&gt; is the host project that wires everything together: Marten configuration, dependency injection, and the ASP.NET Core pipeline.&lt;/p&gt;

&lt;p&gt;This structure is similar to what I use in my &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; projects, but simplified for a single module.&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/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql&lt;/a&gt;&lt;/p&gt;

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