<?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>10 Rare SQL Features Every Developer Should Know</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 08 Sep 2026 11:17:35 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/10-rare-sql-features-every-developer-should-know-3ia7</link>
      <guid>https://dev.to/antonmartyniuk/10-rare-sql-features-every-developer-should-know-3ia7</guid>
      <description>&lt;p&gt;Most developers use maybe 20 percent of SQL's capabilities.&lt;/p&gt;

&lt;p&gt;They write &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;GROUP BY&lt;/code&gt;, and stop there.&lt;/p&gt;

&lt;p&gt;But SQL has a second layer - features that turn a page of application code or three separate queries into a single clean statement.&lt;/p&gt;

&lt;p&gt;Senior developers reach for these all the time. Many junior and mid-level developers have never seen them.&lt;/p&gt;

&lt;p&gt;None of them is new or obscure. They are sitting in the database you already use, waiting to be picked up.&lt;/p&gt;

&lt;p&gt;Today, I want to show you 10 rare SQL features every developer should know.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Common Table Expressions (CTEs)&lt;/li&gt;
&lt;li&gt;Window functions&lt;/li&gt;
&lt;li&gt;LATERAL joins&lt;/li&gt;
&lt;li&gt;GROUPING SETS, ROLLUP, and CUBE&lt;/li&gt;
&lt;li&gt;The FILTER clause in aggregates&lt;/li&gt;
&lt;li&gt;UPSERT with INSERT ... ON CONFLICT&lt;/li&gt;
&lt;li&gt;JSON support&lt;/li&gt;
&lt;li&gt;Computed / generated columns&lt;/li&gt;
&lt;li&gt;TABLESAMPLE&lt;/li&gt;
&lt;li&gt;Partial indexes&lt;/li&gt;
&lt;/ul&gt;

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

&lt;blockquote&gt;
&lt;p&gt;All queries in this post were tested on the PostgreSQL database. Most of these features exist in other databases too, though the exact syntax differs - I will note the main differences as we go.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Common Table Expressions (CTEs)
&lt;/h2&gt;

&lt;p&gt;A complex query packed into a single statement is hard to read and even harder to change.&lt;/p&gt;

&lt;p&gt;A Common Table Expression (CTE) lets you break that query into named, sequential steps using the &lt;code&gt;WITH&lt;/code&gt; keyword. Each step reads like a temporary, named result you can build on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;recent_shipments&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;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;created_at&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;shipments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shipments&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&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;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;shipment_details&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;rs&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;rs&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;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;si&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;si&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="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_quantity&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;recent_shipments&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;
    &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;shipments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shipment_items&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shipment_id&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;rs&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;rs&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;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;shipment_number&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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_quantity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;shipment_details&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_quantity&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query has two named parts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;recent_shipments&lt;/code&gt; selects shipments from the last 30 days. &lt;code&gt;shipment_details&lt;/code&gt; then builds on it, joining the items and aggregating counts and quantities. The final &lt;code&gt;SELECT&lt;/code&gt; reads from the second CTE as if it were a table.&lt;/p&gt;

&lt;p&gt;The result is a query you read top to bottom, like steps in a recipe, instead of using nested subqueries from the inside out.&lt;/p&gt;

&lt;p&gt;CTEs also support recursion with &lt;code&gt;WITH RECURSIVE&lt;/code&gt;, which is how you query hierarchical data like org charts and category trees.&lt;/p&gt;

&lt;p&gt;A Common Table Expression can be used within a SELECT, INSERT, UPDATE, or DELETE statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Window Functions
&lt;/h2&gt;

&lt;p&gt;Sometimes you need a calculation across related rows but still want every individual row in the result.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;GROUP BY&lt;/code&gt; collapses rows into one per group. A window function calculates across a set of rows - the window - while keeping each row intact.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;number&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;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&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;shipment_sequence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RANK&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&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;shipment_rank&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;shipments&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="k"&gt;SELECT&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;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LAG&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;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&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;previous_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;LEAD&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;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&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;next_carrier&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;shipments&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first query ranks each carrier's shipments by date. &lt;code&gt;ROW_NUMBER()&lt;/code&gt; gives a unique sequence within each carrier (the &lt;code&gt;PARTITION BY carrier&lt;/code&gt;), and &lt;code&gt;RANK()&lt;/code&gt; does the same, but ties share a rank.&lt;/p&gt;

&lt;p&gt;The second query uses &lt;code&gt;LAG&lt;/code&gt; and &lt;code&gt;LEAD&lt;/code&gt; to look at the previous and next row in order - here, the previous status and the next carrier - without a self-join.&lt;/p&gt;

&lt;p&gt;Window functions are how you build running totals, rankings, moving averages, and row-to-row comparisons.&lt;/p&gt;

&lt;p&gt;They are standard SQL and work in PostgreSQL, SQL Server, Oracle, and MySQL 8+.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>sql</category>
    </item>
    <item>
      <title>ASP.NET Core Output Cache: How to Speed Up Your API with In-Memory Cache and Redis</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Mon, 07 Sep 2026 11:00:48 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/aspnet-core-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis-50na</link>
      <guid>https://dev.to/antonmartyniuk/aspnet-core-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis-50na</guid>
      <description>&lt;p&gt;In ASP.NET Core, one of the most powerful and underused caching tools is the &lt;strong&gt;Output Cache middleware&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Though so many developers still don't know about it or how to use it effectively.&lt;/p&gt;

&lt;p&gt;Output Cache is not the same as storing objects in &lt;code&gt;IMemoryCache&lt;/code&gt; or &lt;code&gt;IDistributedCache&lt;/code&gt;.&lt;br&gt;
It operates at the HTTP response level, caching the full serialized response and serving it directly — without touching your handlers, your database, or your business logic.&lt;/p&gt;

&lt;p&gt;The result is dramatically lower latency and reduced load on your infrastructure.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;What Output Cache is and how it differs from IMemoryCache and IDistributedCache&lt;/li&gt;
&lt;li&gt;How to set up Output Cache in ASP.NET Core&lt;/li&gt;
&lt;li&gt;How to customize cache behavior with policies and options&lt;/li&gt;
&lt;li&gt;How to evict cached responses using tags, keys, and full cache clearing&lt;/li&gt;
&lt;li&gt;How to use Redis as the Output Cache store for distributed scenarios&lt;/li&gt;
&lt;li&gt;How to handle caching safely in authenticated APIs to avoid leaking data between users&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/aspnetcore-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/aspnetcore-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What Is Output Cache and How Does It Differ from Other Caches
&lt;/h2&gt;

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

&lt;p&gt;You use it to cache any .NET object — a list, a domain model, a computed value.&lt;br&gt;
You control what gets stored, how it is serialized, and when it expires.&lt;/p&gt;

&lt;p&gt;It is fast because there is no network hop.&lt;br&gt;
But it is local to a single instance of your app, so it does not work across multiple servers without extra coordination.&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;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMemoryCache&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrdersDbContext&lt;/span&gt; &lt;span class="n"&gt;db&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;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderSummary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOrdersAsync&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;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrCreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders:all"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AbsoluteExpirationRelativeToNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&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;o&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;OrderSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalAmount&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;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;IDistributedCache&lt;/code&gt; is an abstraction over an external cache store, usually Redis.&lt;br&gt;
It stores byte arrays, so you serialize and deserialize your objects manually (or with a wrapper).&lt;/p&gt;

&lt;p&gt;It works across multiple app instances because all instances share the same external store.&lt;br&gt;
The downside is the added latency of a network call to the cache server.&lt;/p&gt;

&lt;p&gt;Both &lt;code&gt;IMemoryCache&lt;/code&gt; and &lt;code&gt;IDistributedCache&lt;/code&gt; require you to write caching logic inside your service or handler.&lt;/p&gt;

&lt;p&gt;You have to call the cache before your database query, check for a hit, store the result after a miss, and handle expiration yourself.&lt;br&gt;
This adds boilerplate to every method you want to cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output Cache&lt;/strong&gt; works differently.&lt;/p&gt;

&lt;p&gt;Instead of caching objects inside your application code, Output Cache intercepts the HTTP response at the middleware level.&lt;br&gt;
It stores the full serialized response — the status code, headers, and body — and replays it on subsequent matching requests.&lt;/p&gt;

&lt;p&gt;Your endpoint handler, database query, and business logic are never called when a cached response is available.&lt;br&gt;
The middleware short-circuits the pipeline and writes the stored response directly.&lt;/p&gt;

&lt;p&gt;This means you can add caching to existing endpoints with almost no changes to your application code.&lt;br&gt;
You decorate an endpoint or controller with an attribute or a policy name, and the middleware handles the rest.&lt;/p&gt;

&lt;p&gt;The built-in cache lock feature in Output Cache is particularly useful.&lt;br&gt;
When multiple requests arrive for the same uncached resource at the same time, only one request is allowed through to execute the handler.&lt;/p&gt;

&lt;p&gt;The others wait for the first response and then receive the cached copy.&lt;br&gt;
This prevents the "thundering herd" problem, where a cache miss causes a spike of concurrent database queries.&lt;/p&gt;

&lt;p&gt;Output Cache was introduced in .NET 7 and has been improved in further .NET versions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting Up Output Cache in ASP.NET Core
&lt;/h2&gt;

&lt;p&gt;To get started with Output Cache, install the following NuGet package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.AspNetCore.OutputCaching
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register Output Cache services in &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Register OutputCache in DI&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOutputCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

&lt;span class="c1"&gt;// Add OutputCache Middleware&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseOutputCache&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

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

&lt;/div&gt;



&lt;p&gt;The middleware must be placed after &lt;code&gt;UseRouting&lt;/code&gt; (if you call it explicitly) and before &lt;code&gt;MapControllers&lt;/code&gt; or any Minimal API endpoints like &lt;code&gt;MapGet&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, let's define a simple Orders API example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/orders"&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;OrdersController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrdersDbContext&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OutputCache&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOrders&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;ct&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;orders&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&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;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&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;HttpGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{id:guid}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetOrder&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;ct&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;order&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&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;ct&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;order&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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;To cache the &lt;code&gt;GetOrders&lt;/code&gt; endpoint, we add the &lt;code&gt;[OutputCache]&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;With this one attribute, the first request to &lt;code&gt;GET /api/orders&lt;/code&gt; will execute the handler, call the database and store the response.&lt;br&gt;
Every subsequent request within the default expiration window (&lt;strong&gt;60 seconds&lt;/strong&gt;) will receive the cached response without hitting the database.&lt;/p&gt;

&lt;p&gt;You can also cache endpoints in Minimal APIs by calling &lt;code&gt;.CacheOutput()&lt;/code&gt; on the &lt;code&gt;RouteHandlerBuilder&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/orders"&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;OrdersDbContext&lt;/span&gt; &lt;span class="n"&gt;db&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;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&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;ct&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;orders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;CacheOutput&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/aspnetcore-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/aspnetcore-output-cache-how-to-speed-up-your-api-with-in-memory-cache-and-redis&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>caching</category>
    </item>
    <item>
      <title>Entity Framework Extensions Options Explained: Everything You Can Customize</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 04 Sep 2026 14:53:00 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/entity-framework-extensions-options-explained-everything-you-can-customize-54o0</link>
      <guid>https://dev.to/antonmartyniuk/entity-framework-extensions-options-explained-everything-you-can-customize-54o0</guid>
      <description>&lt;p&gt;When working with Entity Framework Core, you will eventually need to perform bulk operations on large datasets.&lt;br&gt;
Standard EF Core methods work well for small operations, but they become slow and inefficient when dealing with thousands of records.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://entityframework-extensions.net?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026" rel="noopener noreferrer"&gt;Entity Framework Extensions&lt;/a&gt; is famous for its fastest bulk operations in the market.&lt;br&gt;
It supports various database providers and provides methods for bulk insert, update, delete, merge, and synchronize operations.&lt;/p&gt;

&lt;p&gt;But the real power of this library is not just speed.&lt;br&gt;
Entity Framework Extensions is famous for its hundreds of available options.&lt;br&gt;
These options will save you hours, or even days, of tedious coding that is prone to bugs.&lt;/p&gt;

&lt;p&gt;In this post, we will explore the most important customizable options available in Entity Framework Extensions.&lt;br&gt;
You will learn how to fine-tune bulk operations for various real-world scenarios.&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/entity-framework-extensions-options-explained" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/entity-framework-extensions-options-explained&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Performing Bulk Insert with Entity Framework Extensions
&lt;/h2&gt;

&lt;p&gt;I have been working on an interesting project that manages IoT devices and their telemetry data in the SQL Server database.&lt;/p&gt;

&lt;p&gt;The database has three main tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Devices&lt;/strong&gt; - Represent IoT devices with properties like name, serial number, device type, manufacturer, firmware version, hardware version, status, and configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Components&lt;/strong&gt; - Represent components of these devices, such as sensors, and other hardware parts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry&lt;/strong&gt; - Store telemetry data collected from these devices, including temperature readings, humidity levels, and other sensor values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our ASP.NET Core application, we have three main entities: &lt;code&gt;Device&lt;/code&gt;, &lt;code&gt;Component&lt;/code&gt;, and &lt;code&gt;Telemetry&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Imagine a scenario where your system needs to insert 10,000-50,000 telemetry records into the database every few minutes.&lt;/p&gt;

&lt;p&gt;You can wait for minutes for this insert to happen with EF Core, but you don't have to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://entityframework-extensions.net?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026" rel="noopener noreferrer"&gt;Entity Framework Extensions&lt;/a&gt; solves this problem and provides lightning-fast bulk insert methods.&lt;/p&gt;

&lt;p&gt;To get started with the Entity Framework Extensions library, you need to install the following NuGet package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Z.EntityFramework.Extensions.EFCore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's how to bulk insert IoT devices into the database.&lt;/p&gt;

&lt;p&gt;The Entity Framework Extensions library provides various extension methods for the &lt;code&gt;DbContext&lt;/code&gt; class, such as &lt;a href="https://entityframework-extensions.net/bulk-insert?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026" rel="noopener noreferrer"&gt;BulkInsert&lt;/a&gt;.&lt;br&gt;
Both async and sync versions of this method are available.&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateDevices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="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;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Entity Framework Extensions provides extensive configuration options to customize the behavior of bulk operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entity Framework Extensions - Bulk Insert Options
&lt;/h2&gt;

&lt;p&gt;The bulk insert method provides several options for customizing behavior. The method accepts an options delegate as a second parameter, allowing you to configure various settings.&lt;/p&gt;

&lt;p&gt;Let's explore a few of the most important options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entity Framework Extensions - InsertIfNotExists Option
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://entityframework-extensions.net/bulk-insert?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026#common-options-in-entity-framework-extensions" rel="noopener noreferrer"&gt;InsertIfNotExists&lt;/a&gt; option allows you to insert only the entities that don't already exist in the database.&lt;/p&gt;

&lt;p&gt;To demonstrate this behavior, first insert 10 IoT devices, then attempt to insert the same devices again with &lt;code&gt;InsertIfNotExists&lt;/code&gt; enabled:&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateDevices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="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;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Insert the same 10 devices again with the InsertIfNotExists = true option&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;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&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;InsertIfNotExists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second insert operation prevents duplicate entries, ensuring that the 10 devices are not inserted again.&lt;/p&gt;

&lt;p&gt;By default, the Entity Framework Extensions library matches the entities by their primary key.&lt;br&gt;
In our case, in the Device entity, it will be the &lt;code&gt;DeviceId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But you can customize this behavior.&lt;/p&gt;
&lt;h3&gt;
  
  
  Entity Framework Extensions - Customizing Primary Key with ColumnPrimaryKeyExpression
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://entityframework-extensions.net/primary-key?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026" rel="noopener noreferrer"&gt;ColumnPrimaryKeyExpression&lt;/a&gt; option accepts a delegate that defines which property (or properties) to use for matching entities.&lt;/p&gt;

&lt;p&gt;This option supports matching by any property or combination of properties. For example, IoT devices can be matched by serial number instead of the default primary key:&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&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;InsertIfNotExists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ColumnPrimaryKeyExpression&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&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;SerialNumber&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;
  
  
  Entity Framework Extensions - InsertKeepIdentity Option
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://entityframework-extensions.net/insert-keep-identity?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026" rel="noopener noreferrer"&gt;InsertKeepIdentity&lt;/a&gt; option allows you to insert custom identity values instead of letting the database generate them automatically. This is particularly useful when the Device entity uses a long type for the primary key in EF Core and a database identity in SQL Server.&lt;/p&gt;

&lt;p&gt;Let's try to insert 2 devices with custom IDs:&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateDevices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;DeviceId&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;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;DeviceId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1001&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;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&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;InsertKeepIdentity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bulk insert operation preserves the custom identity values (1000 and 1001) instead of using database-generated values.&lt;/p&gt;

&lt;p&gt;This option is particularly useful when you synchronize data from other services or external providers, and you want to keep your identity values in sync with those systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entity Framework Extensions - AutoMapOutputDirection Option
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://entityframework-extensions.net/bulk-extensions?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026#bulk-insert-for-ef-core-with-entity-framework-extensions" rel="noopener noreferrer"&gt;AutoMapOutputDirection&lt;/a&gt; option controls whether database-generated values are mapped back to the entity objects after insertion. By default, this option is set to &lt;code&gt;true&lt;/code&gt;, which means primary keys and other database-generated columns are automatically populated:&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateDevices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="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;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After insertion, devices now have their DeviceId populated&lt;/span&gt;
&lt;span class="k"&gt;foreach&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;device&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;)&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;$"Device ID: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeviceId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The identity values for primary keys and any database-generated columns are automatically returned and mapped to the entity objects.&lt;/p&gt;

&lt;p&gt;When database-generated values are not needed, setting &lt;code&gt;AutoMapOutputDirection&lt;/code&gt; to false improves performance:&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&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;AutoMapOutputDirection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&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;With this option disabled, the &lt;code&gt;DeviceId&lt;/code&gt; property remains unpopulated after insertion, reducing overhead and improving performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entity Framework Extensions - BulkInsertOptimized Method
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://entityframework-extensions.net/bulk-insert-optimized?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026" rel="noopener noreferrer"&gt;BulkInsertOptimized&lt;/a&gt; method provides an alternative to &lt;code&gt;BulkInsert&lt;/code&gt; with built-in performance analysis capabilities.&lt;/p&gt;

&lt;p&gt;While &lt;code&gt;BulkInsertOptimized&lt;/code&gt; behaves similarly to &lt;code&gt;BulkInsert&lt;/code&gt; with &lt;code&gt;AutoMapOutputDirection = false&lt;/code&gt;, it offers a key advantage: it returns a &lt;a href="https://entityframework-extensions.net/bulk-insert-optimized?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026#bulkinsertoptimized-recommendations-and-performance-hints" rel="noopener noreferrer"&gt;BulkOptimizedAnalysis&lt;/a&gt; object containing performance hints and optimization recommendations.&lt;/p&gt;

&lt;p&gt;Here is how to insert 10,000 devices with &lt;code&gt;BulkInsertOptimizedAsync&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="c1"&gt;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateDevices&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="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;BulkInsertOptimizedAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this &lt;a href="https://antondevtips.com/blog/how-i-have-increased-the-production-payment-system-performance-by-15x-with-efcore-extensions#how-i-increased-the-performance-by-15x-with-1-line-of-code-with-entity-framework-extensions" rel="noopener noreferrer"&gt;article&lt;/a&gt;, I explained in depth the performance difference between &lt;code&gt;BulkInsertAsync&lt;/code&gt; and &lt;code&gt;BulkInsertOptimizedAsync&lt;/code&gt; methods.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entity Framework Extensions - IncludeGraph Option for Related Entities
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://entityframework-extensions.net/include-graph?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026" rel="noopener noreferrer"&gt;IncludeGraph&lt;/a&gt; option enables automatic insertion of related entities within an object graph. This feature is particularly useful when working with parent-child relationships, such as devices with their associated components.&lt;/p&gt;

&lt;p&gt;Consider a scenario with 10 devices, each containing three components:&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateDevices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&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;device&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Components&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateComponents&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;IncludeGraph&lt;/code&gt; option handles the insertion of the entire object graph, including all related entities:&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&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;IncludeGraph&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This option automatically inserts related entities from the graph of objects and preserves their relationships.&lt;/p&gt;

&lt;p&gt;This is a very useful option, typically achieved with a single line of code in EF Core Extensions.&lt;br&gt;
Imagine how much code you will write when using SQL Bulk Copy.&lt;/p&gt;
&lt;h3&gt;
  
  
  Entity Framework Extensions - AutoTruncate Option for String Length Management
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://entityframework-extensions.net/bulk-insert?utm_source=antondevtips&amp;amp;utm_medium=newsletter&amp;amp;utm_campaign=february-2026#common-options-in-entity-framework-extensions" rel="noopener noreferrer"&gt;AutoTruncate&lt;/a&gt; option automatically truncates string values to match the maximum length defined in the Entity Framework mapping. When enabled, strings exceeding the database column length are trimmed before insertion, preventing length constraint violations.&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;// @nuget: Z.EntityFramework.Extensions.EFCore&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Z.EntityFramework.Extensions&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;devices&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateDevices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&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;device&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HardwareVersion&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;" some long text that needs to be truncated by EF Core Extensions library"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BulkInsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;devices&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;AutoTruncate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/entity-framework-extensions-options-explained" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/entity-framework-extensions-options-explained&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
    </item>
    <item>
      <title>How Architects (Senior Devs) Choose a Database in 2026</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 01 Sep 2026 11:13:25 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-architects-senior-devs-choose-a-database-in-2026-5f3c</link>
      <guid>https://dev.to/antonmartyniuk/how-architects-senior-devs-choose-a-database-in-2026-5f3c</guid>
      <description>&lt;p&gt;Choosing a database is one of the most impactful decisions you make in a project.&lt;/p&gt;

&lt;p&gt;It affects your application's performance, your monthly bill, and how often your team gets called at night.&lt;/p&gt;

&lt;p&gt;When developers discuss databases, the conversation usually starts with product names: PostgreSQL, MongoDB, Redis.&lt;/p&gt;

&lt;p&gt;Architects start differently. They first ask questions about the data, the team, and the budget. The product name is the last thing they decide.&lt;/p&gt;

&lt;p&gt;Over the past years, I've built and scaled .NET systems using most of the databases on this list.&lt;br&gt;
In this post, I'll break down every major database family: what each one is good at, what it really costs, and how to choose the right one for your project.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;What an architect is actually choosing&lt;/li&gt;
&lt;li&gt;Relational databases: PostgreSQL, MySQL and MariaDB&lt;/li&gt;
&lt;li&gt;Commercial relational: SQL Server and Oracle&lt;/li&gt;
&lt;li&gt;Embedded databases: SQLite&lt;/li&gt;
&lt;li&gt;Document databases: MongoDB and RavenDB&lt;/li&gt;
&lt;li&gt;Key-value and in-memory: Redis&lt;/li&gt;
&lt;li&gt;Wide-column databases: Cassandra&lt;/li&gt;
&lt;li&gt;Managed cloud databases&lt;/li&gt;
&lt;li&gt;Cloud-native databases: DynamoDB and Cosmos DB&lt;/li&gt;
&lt;li&gt;The specialized database families you should know exist&lt;/li&gt;
&lt;li&gt;How to evaluate a database before you commit&lt;/li&gt;
&lt;li&gt;Choosing a database in 2026&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-architects-choose-a-database-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-architects-choose-a-database-in-2026&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What an Architect Is Actually Choosing
&lt;/h2&gt;

&lt;p&gt;When you choose a database, you're not just choosing a product.&lt;/p&gt;

&lt;p&gt;You're choosing how your system behaves when something fails, who fixes it at night, and how much you pay every month.&lt;/p&gt;

&lt;p&gt;Here are 6 questions that decide the choice before any product is named:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. What shape is the data, and how do you read it?&lt;/strong&gt; Rows with relationships, documents that you read as a whole, or values fetched by a known key? This question eliminates most of the list, and it's about the shape of your data, not the scale. I covered this split in &lt;a href="https://antondevtips.com/blog/getting-started-with-system-design" rel="noopener noreferrer"&gt;Getting Started with System Design&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What does a wrong answer cost?&lt;/strong&gt; A stale like count costs nothing. A stale account balance costs money and trust. This answer sets your consistency requirement, and consistency is the most expensive property to guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What is your p99 latency under real load?&lt;/strong&gt; The p99 is the response time that 99% of requests stay under. The average hides problems, so measure the p99 with your real query mix — that's what your slowest users actually experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Who supports the database in production?&lt;/strong&gt; A managed service moves patching, failover and backups to the provider. Self-hosting moves them to your team. Both options are valid; you just need to know which one you're signing up for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. What does it cost at twice today's traffic?&lt;/strong&gt; Consumption-based pricing looks cheap at the start, but it grows with every request. Per-core licensing is predictable, but it gets expensive when you scale out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. What does it cost to leave?&lt;/strong&gt; When you lock in to a specific database, moving to another one can mean a complete application rewrite.&lt;/p&gt;

&lt;p&gt;Notice that only the first question is about the database itself.&lt;br&gt;
The other five are about your team and your organization.&lt;/p&gt;

&lt;p&gt;There is one more question that most teams skip: &lt;strong&gt;do you need more than one database engine at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each engine can be better at its own job. Still, each one also adds operational work: backups and restores, a high-availability setup, upgrades, monitoring dashboards, driver maintenance, and people who understand it during an incident.&lt;br&gt;
That adds up to weeks of platform work per year, per engine.&lt;/p&gt;

&lt;p&gt;Two rules help you keep this under control.&lt;/p&gt;

&lt;p&gt;First, nominate &lt;strong&gt;one system of record per fact&lt;/strong&gt;.&lt;br&gt;
Every piece of data has exactly one authoritative home, and every other copy is a projection you can throw away and rebuild.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;never dual-write&lt;/strong&gt;.&lt;br&gt;
If your application writes to PostgreSQL and to a search index in the same request, one of those writes will eventually fail, and the two stores will silently diverge.&lt;br&gt;
Write to the main system of record first, and propagate the change asynchronously through an outbox or change data capture.&lt;/p&gt;

&lt;p&gt;Finally, add a new engine only when it's roughly &lt;strong&gt;few times better&lt;/strong&gt; for your workload, not 20% better.&lt;/p&gt;

&lt;p&gt;Now let's go through each database family, starting with the most popular one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relational Databases: PostgreSQL, MySQL and MariaDB
&lt;/h2&gt;

&lt;p&gt;PostgreSQL is the default choice for most systems, and it's what I recommend to most teams.&lt;/p&gt;

&lt;p&gt;It's a relational database that also stores JSON documents, runs full-text search, and handles geospatial queries.&lt;br&gt;
That range is the reason most projects never need a second engine.&lt;/p&gt;

&lt;p&gt;MySQL and its fork MariaDB solve a narrower problem well: simple, high-throughput reads on a well-indexed schema, with the shortest learning curve of the three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One engine covers relational, document, search and geospatial workloads&lt;/li&gt;
&lt;li&gt;Mature query tooling — &lt;code&gt;pg_stat_statements&lt;/code&gt; shows you which query is slow in seconds&lt;/li&gt;
&lt;li&gt;It's easy to hire developers who already know it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One write primary. Read replicas scale reads, but scaling writes means sharding or distributed SQL&lt;/li&gt;
&lt;li&gt;Self-managed failover needs extra tooling; it's not built in&lt;/li&gt;
&lt;li&gt;MySQL lags behind PostgreSQL on window functions, CTEs and richer types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Licensing:&lt;/strong&gt; PostgreSQL uses the permissive &lt;a href="https://www.postgresql.org/about/licence/" rel="noopener noreferrer"&gt;PostgreSQL License&lt;/a&gt; and is free for any purpose.&lt;br&gt;
MySQL is &lt;a href="https://www.mysql.com/about/legal/licensing/oem/" rel="noopener noreferrer"&gt;dual-licensed&lt;/a&gt; under GPLv2 or a commercial license, which you need if you bundle it inside closed-source software you distribute.&lt;br&gt;
MariaDB Server is GPLv2, though its MaxScale proxy moved to a fully proprietary license in 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose it when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building a line-of-business application and have no measured reason to do otherwise&lt;/li&gt;
&lt;li&gt;Your data has relationships and your reports aren't known in advance&lt;/li&gt;
&lt;li&gt;You want the safest hiring and tooling story available&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/how-architects-choose-a-database-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-architects-choose-a-database-in-2026&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>database</category>
    </item>
    <item>
      <title>Querying and Performing Transactions Across Multiple Database Schemas in a Modular Monolith</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Sat, 29 Aug 2026 12:42:19 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/querying-and-performing-transactions-across-multiple-database-schemas-in-a-modular-monolith-15l2</link>
      <guid>https://dev.to/antonmartyniuk/querying-and-performing-transactions-across-multiple-database-schemas-in-a-modular-monolith-15l2</guid>
      <description>&lt;p&gt;Building 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; gives you clear boundaries between modules, but it also introduces a challenge: how do you query data that lives in different schemas?&lt;br&gt;
And how do you maintain data consistency when a business operation spans multiple modules?&lt;/p&gt;

&lt;p&gt;In a traditional monolith, you could join tables across the database.&lt;/p&gt;

&lt;p&gt;But in a Modular Monolith, each module owns its schema.&lt;br&gt;
Direct database access between modules breaks the boundaries you worked hard to establish.&lt;/p&gt;

&lt;p&gt;I have been working with Modular Monoliths for years, and I have seen teams struggle with this exact problem.&lt;br&gt;
They start with good boundaries, create separate schemas for each module, but then they need to show a report that combines data from three different modules. What do they do?&lt;/p&gt;

&lt;p&gt;Some teams give up and start querying across schemas directly. Others try to solve it with complex event chains that are hard to debug.&lt;br&gt;
And some just avoid the problem altogether by keeping everything in one module.&lt;/p&gt;

&lt;p&gt;But there is a better way.&lt;/p&gt;

&lt;p&gt;Today, I want to show you proven approaches for querying data across schemas and strategies for managing transactions in a Modular Monolith.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Why You Can't Join Tables Across Multiple Schemas&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recommended Approaches for Cross-Schema Queries Inter-module API Calls Domain Events with Eventual Consistency Database Views Composite View Pattern (BFF with YARP) Reporting/Analysis Module&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performing Transactions Across Multiple Schemas Domain Events for Eventual Consistency Transaction across EF Core DbContexts for Strong Consistency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choosing Between Eventual and Strong Consistency&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in!&lt;/p&gt;
&lt;h2&gt;
  
  
  Why You Can't Join Tables Across Multiple Schemas
&lt;/h2&gt;

&lt;p&gt;In 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;, each module has its own database schema and DbContext in EF Core.&lt;br&gt;
This separation is intentional. It enforces boundaries and makes it possible to extract a module into a microservice later.&lt;/p&gt;

&lt;p&gt;But this separation creates two main challenges:&lt;/p&gt;

&lt;p&gt;First, you cannot simply join tables across schemas in your queries. If you want to show shipment details along with carrier information and stock levels, you cannot write a single SQL or EF Core query that joins all three schemas.&lt;/p&gt;

&lt;p&gt;Second, you cannot use a single database transaction that spans multiple modules.&lt;br&gt;
If creating a shipment requires updating stock levels and registering with a carrier, you need a strategy to keep all three operations consistent.&lt;/p&gt;

&lt;p&gt;Let me show you a concrete example.&lt;/p&gt;

&lt;p&gt;Imagine you need to build a dashboard that shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;All shipments created today&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The carrier assigned to each shipment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Current stock levels for products in those shipments&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a traditional Monolith, you might write something like this:&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;dashboard&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;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;Stocks&lt;/span&gt;&lt;span class="p"&gt;)&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;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;Carriers&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;CreatedAt&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="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="n"&gt;Date&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;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;ShipmentNumber&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;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CarrierName&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;Carrier&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;StockLevel&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;Stocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;First&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But in a Modular Monolith, this code will not work. The &lt;code&gt;ShipmentsDbContext&lt;/code&gt; does not know about &lt;code&gt;Carriers&lt;/code&gt; or &lt;code&gt;Stocks&lt;/code&gt; entities. They live in different schemas with different DbContexts.&lt;/p&gt;

&lt;p&gt;So how do you solve this?&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended Approaches for Cross-Schema Queries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Inter-module API Calls
&lt;/h3&gt;

&lt;p&gt;The most straightforward approach is to call other modules through their public APIs. Each module exposes an interface that other modules can use to query data.&lt;/p&gt;

&lt;p&gt;This is the approach is the simplest and the cheapest.&lt;br&gt;
It respects module boundaries and makes dependencies explicit.&lt;/p&gt;

&lt;p&gt;Let's say you need to display shipment details with carrier information. Here is how you would implement it:&lt;/p&gt;

&lt;p&gt;First, the &lt;code&gt;Carriers&lt;/code&gt; module exposes a public API:&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;ICarrierModuleApi&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;CarrierDetailsResponse&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCarrierByNameAsync&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;carrierName&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now in the &lt;code&gt;Shipments&lt;/code&gt; module, you can create a &lt;a href="https://antondevtips.com/blog/refactoring-a-modular-monolith-without-mediatr-in-dotnet" rel="noopener noreferrer"&gt;handler&lt;/a&gt; that combines data from multiple modules:&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;GetShipmentDetailsHandler&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;ICarrierModuleApi&lt;/span&gt; &lt;span class="n"&gt;carrierApi&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="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;ErrorOr&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentDetailsResponse&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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Get shipment from the local database&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;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;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;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;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&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;"ShipmentNotFound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"Shipment &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shipmentNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not found"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Get carrier details from the Carriers module&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;carrierDetails&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;carrierApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCarrierByNameAsync&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;Carrier&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="c1"&gt;// 3. Get stock levels for each product from the Stocks module&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stockLevels&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;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&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="k"&gt;foreach&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;item&lt;/span&gt; &lt;span class="k"&gt;in&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="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;GetStockLevelAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;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;stockLevels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&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="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;Quantity&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="c1"&gt;// 4. Combine all data into the response&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;ShipmentDetailsResponse&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ShipmentNumber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shipment&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;OrderId&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;OrderId&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&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="nf"&gt;ToString&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;shipment&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;Carrier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;carrierDetails&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;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;Select&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="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ShipmentItemDetails&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;i&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;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="n"&gt;CurrentStockLevel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stockLevels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetValueOrDefault&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;Product&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;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;&lt;strong&gt;This approach has several advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Clear boundaries: each module controls what data it exposes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Type safety: you work with strongly typed interfaces&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy to test: you can mock the module APIs in unit tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flexible: each module can change its internal implementation without affecting others&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But it also has some drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multiple database queries: you make separate calls to each module&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;N+1 query problem: if you need to enrich a list of shipments with carrier details, you will make one query per shipment&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most scenarios, this approach works well. If you have a few items, the performance overhead is usually acceptable, especially when you add caching or support for bulk operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain Events with Eventual Consistency
&lt;/h3&gt;

&lt;p&gt;Sometimes you need to query data that does not change frequently.&lt;br&gt;
In these cases, you can duplicate the data across modules using integration events.&lt;/p&gt;

&lt;p&gt;This approach works well when you need to denormalize data for read performance.&lt;/p&gt;

&lt;p&gt;Let's say you want to display the carrier name on the shipment list without having to call the Carriers module each time.&lt;br&gt;
You can store the carrier name directly in the Shipments schema.&lt;/p&gt;

&lt;p&gt;When a carrier is updated in the Carriers module, it publishes an event:&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;CarrierUpdatedEvent&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;CarrierId&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;CarrierName&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;ContactEmail&lt;/span&gt;&lt;span class="p"&gt;,&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="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEvent&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;UpdateCarrierHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CarriersDbContext&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;IEventPublisher&lt;/span&gt; &lt;span class="n"&gt;eventPublisher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUpdateCarrierHandler&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;ErrorOr&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CarrierResponse&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;UpdateCarrierRequest&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;carrier&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;Carriers&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;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CarrierId&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;carrier&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&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;"CarrierNotFound"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Carrier not found"&lt;/span&gt;&lt;span class="p"&gt;);&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;ContactEmail&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;ContactEmail&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;PhoneNumber&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;PhoneNumber&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;IsActive&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;IsActive&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Publish event for other modules&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;carrierUpdatedEvent&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;CarrierUpdatedEvent&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;Id&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;Name&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;ContactEmail&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;IsActive&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;carrierUpdatedEvent&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="k"&gt;return&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;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 Shipments module subscribes to this event and updates its local copy:&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;UpdateShipmentCarrierDetailsEventHandler&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;IEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CarrierUpdatedEvent&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;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;CarrierUpdatedEvent&lt;/span&gt; &lt;span class="n"&gt;@event&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;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;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;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;CarrierName&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CarrierName&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;span class="k"&gt;foreach&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="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;shipments&lt;/span&gt;&lt;span class="p"&gt;)&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;CarrierContactEmail&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContactEmail&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;UpdatedAt&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="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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can query shipments with carrier details in the Shipments module without having to call the Carriers module.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fast queries: all data is in one schema, no joins needed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No runtime dependencies: modules do not need to call each other during queries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Better performance: single database query instead of multiple calls&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But it comes with trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Eventual consistency: data might be temporarily out of sync&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data duplication: you store the same data in multiple places&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More complex: you need to handle events and keep data synchronized&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use this approach when read performance is critical and you can accept eventual consistency.&lt;br&gt;
This approach also allows you to change and scale each module independently.&lt;/p&gt;

&lt;p&gt;When using events - I highly recommend using &lt;a href="https://antondevtips.com/blog/getting-started-with-open-telemetry-in-dotnet-with-jaeger-and-seq" rel="noopener noreferrer"&gt;Open Telemetry&lt;/a&gt; to monitor your application.&lt;/p&gt;
&lt;h3&gt;
  
  
  Database Views
&lt;/h3&gt;

&lt;p&gt;Database views provide a way to query data across multiple schemas at the database level. You create a view that joins tables from different schemas, and then map it to a read-only entity in EF Core.&lt;/p&gt;

&lt;p&gt;This approach works well for reporting and analytics scenarios where you need to combine data from multiple modules.&lt;/p&gt;

&lt;p&gt;Let's create a view that combines shipments with carrier information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;shipments_report_view&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;OrderId&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="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;CarrierName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContactEmail&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;CarrierContactEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PhoneNumber&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;CarrierPhoneNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;CarrierIsActive&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Shipments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Carriers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Carriers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can map this view to a read-only entity in EF Core. Create a separate DbContext for read models:&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;ShipmentWithCarrier&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;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="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CarrierName&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;CarrierContactEmail&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;CarrierPhoneNumber&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;CarrierIsActive&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;ReadModelsDbContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContextOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReadModelsDbContext&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;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;DbContext&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DbSet&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentWithCarrier&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ShipmentsWithCarriers&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;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelBuilder&lt;/span&gt; &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentWithCarrier&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;HasNoKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipments_report_view"&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;Now you can query the view from your handler:&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;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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShipmentsWithCarriers&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;Take&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;PageSize&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;span class="k"&gt;return&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;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="n"&gt;ShipmentWithCarrierResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ShipmentNumber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;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;OrderId&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;OrderId&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;Status&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;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="n"&gt;CarrierName&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;CarrierName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CarrierContactEmail&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;CarrierContactEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CarrierPhoneNumber&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;CarrierPhoneNumber&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;&lt;strong&gt;This approach has the following advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Single query: the database handles the join, so you get all data in one query&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Good performance: database views are optimized by the query planner&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simple code: you query the view like a regular table&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No application-level joins: the database does the heavy lifting&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But it has some limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Breaks module boundaries: the view directly accesses multiple schemas&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database coupling: modules are coupled at the database level&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Harder to extract to microservices: you need to remove the view first&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Schema changes require view updates: if you change a table structure, you need to update the view&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use database views for reporting and analytics when performance is critical, and you do not plan to extract modules into microservices soon.&lt;/p&gt;

&lt;p&gt;I recommend using a separate database user for the database views with restricted permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composite View Pattern (BFF with YARP)
&lt;/h3&gt;

&lt;p&gt;The Composite View Pattern, also known as Backend for Frontend (BFF), involves creating a separate service that aggregates data from multiple modules.&lt;br&gt;
This service sits between your frontend and your Modular Monolith.&lt;/p&gt;

&lt;p&gt;This approach is particularly useful when you have complex UI requirements that need data from many modules.&lt;/p&gt;

&lt;p&gt;In our case, we can create a BFF service that queries multiple modules and combines the results. To access this BFF service, we use YARP as an API Gateway.&lt;/p&gt;

&lt;p&gt;YARP (Yet Another Reverse Proxy) is a reverse proxy toolkit from Microsoft that you can use to route requests to different services.&lt;br&gt;
I have written a detailed guide on how to set up YARP as an API Gateway. You can read it here: &lt;a href="https://antondevtips.com/blog/yarp-as-api-gateway-in-dotnet" rel="noopener noreferrer"&gt;YARP as API Gateway in .NET&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is how the architecture looks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Frontend calls YARP Gateway&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;YARP routes requests to either the main Modular Monolith or the BFF service&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;BFF service queries the Modular Monolith modules and aggregates the data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;BFF returns the combined response to the frontend&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's create a BFF service that provides a dashboard view:&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;ShipmentDashboardService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&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;ShipmentDashboardResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetDashboardAsync&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;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ModularMonolith"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Query shipments from the Shipments module&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shipmentsResponse&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"/api/shipments?pageSize=10"&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;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;shipmentsResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFromJsonAsync&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;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Query carriers from the Carriers module&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;carriersResponse&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"/api/carriers"&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;carriers&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;carriersResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFromJsonAsync&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;CarrierResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Query stock levels from the Stocks module&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stocksResponse&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"/api/stocks/summary"&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;stockSummary&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;stocksResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFromJsonAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;StockSummaryResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Combine all data into a dashboard view&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;ShipmentDashboardResponse&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The BFF service exposes its own API endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DashboardEndpoint&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICarterModule&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;AddRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEndpointRouteBuilder&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;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/bff/dashboard"&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="n"&gt;ShipmentDashboardService&lt;/span&gt; &lt;span class="n"&gt;dashboardService&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;dashboard&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;dashboardService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDashboardAsync&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;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;dashboard&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;This approach has the following advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Separation of concerns: the BFF handles UI-specific data aggregation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reduced frontend complexity: the frontend makes one call instead of many&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optimized for UI: you can shape the response exactly as the UI needs it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Independent scaling: you can scale the BFF separately from the main application&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Better performance for frontend: fewer HTTP calls from the browser&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But it has some drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Additional service: you need to deploy and maintain another application&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network overhead: the BFF makes HTTP calls to the main application&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Duplication: you might duplicate some logic between the BFF and the main application&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the BFF pattern when you have complex UI requirements that need data from many modules, or when you want to optimize the API for specific frontend needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S.:&lt;/strong&gt; For BFF, I can highly recommend using GraphQL. It simplifies a lot of things.&lt;br&gt;
If you are interested in GraphQL, you can read my article on &lt;a href="https://antondevtips.com/blog/getting-started-with-hot-chocolate-graphql" rel="noopener noreferrer"&gt;HotChocolate GraphQL in .NET&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reporting/Analysis Module
&lt;/h3&gt;

&lt;p&gt;The final approach is to create a dedicated Reporting or Analysis module that has permission to query multiple schemas.&lt;br&gt;
This module is the only one allowed to break the boundary rules.&lt;/p&gt;

&lt;p&gt;This approach works well when you need complex reporting or analytics that require data from multiple modules.&lt;/p&gt;

&lt;p&gt;The key is to enforce this rule with architecture tests.&lt;br&gt;
You can use tools like NetArchTest to ensure that only the Reporting module can access multiple schemas.&lt;/p&gt;

&lt;p&gt;I have written a detailed guide on architecture tests.&lt;br&gt;
You can read it here: &lt;a href="https://antondevtips.com/blog/why-do-you-need-to-write-architecture-tests-in-dotnet" rel="noopener noreferrer"&gt;Why Do You Need to Write Architecture Tests in .NET&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With this approach you can create complex reports that join data from multiple modules.&lt;/p&gt;

&lt;p&gt;To ensure that only the Reporting module can access multiple schemas, use separate database users for each module.&lt;br&gt;
Configure connection strings with specific permissions:&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;"ConnectionStrings"&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;"Shipments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=localhost;Database=ModularMonolith;User Id=shipments_user;Password=***;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Carriers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=localhost;Database=ModularMonolith;User Id=carriers_user;Password=***;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stocks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=localhost;Database=ModularMonolith;User Id=stocks_user;Password=***;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Reporting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=localhost;Database=ModularMonolith;User Id=reporting_user;Password=***;"&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;Grant the reporting user read access to all schemas, while other users only have access to their own schemas.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Powerful queries: you can write complex SQL joins across all schemas&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Good performance: single database query with proper indexes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Centralized reporting: all reporting logic is in one place&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But it has some limitations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Breaks module boundaries: the Reporting module knows about other modules' schemas&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database coupling: modules are coupled at the database level&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Requires discipline: without architecture tests you can break the module boundaries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Schema changes impact reporting: if you change a table structure, you need to update reports&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use this approach when you need complex reporting and analytics, and you can accept that one module has special privileges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performing Transactions Across Multiple Schemas
&lt;/h2&gt;

&lt;p&gt;Querying data across schemas is one challenge, but maintaining data consistency when you modify data in multiple modules is another.&lt;/p&gt;

&lt;p&gt;When you create a shipment, you need to update stock levels and register with a carrier.&lt;br&gt;
All three operations should succeed or fail together.&lt;br&gt;
But each module has its own DbContext and schema.&lt;/p&gt;

&lt;p&gt;How do you ensure consistency?&lt;/p&gt;

&lt;p&gt;You have two main strategies: eventual consistency with domain events, or strong consistency with a shared transaction.&lt;/p&gt;
&lt;h3&gt;
  
  
  Domain Events for Eventual Consistency
&lt;/h3&gt;

&lt;p&gt;The first strategy is to use domain events.&lt;br&gt;
One module completes its local transaction and publishes an event.&lt;br&gt;
Other modules subscribe to this event and perform their own local transactions in response.&lt;/p&gt;

&lt;p&gt;This results in eventual consistency.&lt;br&gt;
All parts of the system will eventually be consistent, but they might be temporarily out of sync.&lt;/p&gt;

&lt;p&gt;Let's see how this works with the CreateShipment use case:&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="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICreateShipmentHandler&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;ErrorOr&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="c1"&gt;// 1. Check if the shipment already exists&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. Check stock levels (read-only operation)&lt;/span&gt;
        &lt;span class="c1"&gt;// 3. Save the shipment in the local database&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Shipment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="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="c1"&gt;// 4. Publish an event for other modules to react&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="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;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="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;Now the Carriers and Stocks module subscribes to the &lt;code&gt;ShipmentCreatedEvent&lt;/code&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Loose coupling: modules do not depend on each other directly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resilience: if one module fails, others can continue&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability: you can process events asynchronously&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy to add new handlers: you can add new modules that react to the same event&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But it has some challenges:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Eventual consistency: data might be temporarily out of sync&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Error handling: if an event handler fails, you need a retry mechanism&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Debugging: it is harder to trace the flow of execution&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Complexity: you need to handle partial failures and compensating transactions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For better reliability, you should implement the &lt;a href="https://antondevtips.com/blog/use-masstransit-to-implement-outbox-pattern-with-ef-core-and-mongodb" rel="noopener noreferrer"&gt;Outbox pattern&lt;/a&gt;.&lt;br&gt;
This pattern stores events in the same database transaction as your business data, and then publishes them in a separate process. This ensures that events are never lost.&lt;/p&gt;

&lt;p&gt;In a production system, you might use a robust event messaging bus such as &lt;a href="https://antondevtips.com/blog/masstransit-rabbitmq-and-azure-service-bus-is-it-worth-a-commercial-license" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  TransactionManager for Strong Consistency
&lt;/h3&gt;

&lt;p&gt;The second strategy is to use a shared transaction across multiple DbContexts.&lt;br&gt;
This gives you strong consistency: either all operations succeed, or all fail together.&lt;/p&gt;

&lt;p&gt;To implement this, you need a TransactionManager that coordinates the transaction across multiple modules.&lt;/p&gt;

&lt;p&gt;Here is the TransactionManager implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ITransactionManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IDbContextTransaction&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;CurrentTransaction&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="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;SetTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDbContextTransaction&lt;/span&gt; &lt;span class="n"&gt;transaction&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;CommitTransactionAsync&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="k"&gt;default&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;RollbackTransactionAsync&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="k"&gt;default&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;TransactionManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ITransactionManager&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IDbContextTransaction&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_currentTransaction&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;IDbContextTransaction&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;CurrentTransaction&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_currentTransaction&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;SetTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IDbContextTransaction&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_currentTransaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CommitTransactionAsync&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&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;_currentTransaction&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No transaction in progress"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_currentTransaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&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;finally&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_currentTransaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;_currentTransaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="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;RollbackTransactionAsync&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&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;_currentTransaction&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_currentTransaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RollbackAsync&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;finally&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_currentTransaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;_currentTransaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="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;Register the TransactionManager as a scoped service:&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;AddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ITransactionManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TransactionManager&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now update your DbContexts to use the shared transaction:&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;ShipmentsDbContext&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ITransactionManager&lt;/span&gt; &lt;span class="n"&gt;_transactionManager&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;ShipmentsDbContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DbContextOptions&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;,&lt;/span&gt;
        &lt;span class="n"&gt;ITransactionManager&lt;/span&gt; &lt;span class="n"&gt;transactionManager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_transactionManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transactionManager&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;DbSet&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;Shipments&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;DbSet&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;ShipmentItems&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="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// If there is a shared transaction, use it&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;_transactionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentTransaction&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentTransaction&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseTransactionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;_transactionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentTransaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDbTransaction&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;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&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;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelBuilder&lt;/span&gt; &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasDefaultSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbConsts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShipmentsSchemaName&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;Do the same for &lt;code&gt;CarriersDbContext&lt;/code&gt; and &lt;code&gt;StocksDbContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now you can use the TransactionManager in your handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ErrorOr&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="c1"&gt;// Start a shared transaction&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transaction&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;shipmentsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&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;transactionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stocksContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;shipmentsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;carriersContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transactionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitTransactionAsync&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Strong consistency: all operations succeed or fail together&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ACID guarantees: you get the full benefits of database transactions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simpler error handling: if anything fails, everything rolls back&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easier to reason about: the flow is linear and predictable&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;But it has some drawbacks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Tight coupling: the handler needs to know about all DbContexts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Breaks module boundaries: you are directly accessing other modules' DbContexts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Harder to extract to microservices: you need to remove the coupled transaction&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use this approach when you absolutely need strong consistency and cannot accept any temporary inconsistency.&lt;/p&gt;

&lt;p&gt;I recommend using a separate database user for this kind of TransactionManager that is not available to other modules.&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/querying-and-performing-transactions-across-multiple-database-schemas-in-a-modular-monolith" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/querying-and-performing-transactions-across-multiple-database-schemas-in-a-modular-monolith&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>YARP as an API Gateway in .NET: 7 Real-World Scenarios You Should Know</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 28 Aug 2026 19:12:14 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/yarp-as-an-api-gateway-in-net-7-real-world-scenarios-you-should-know-544o</link>
      <guid>https://dev.to/antonmartyniuk/yarp-as-an-api-gateway-in-net-7-real-world-scenarios-you-should-know-544o</guid>
      <description>&lt;p&gt;Building modern distributed applications requires a robust API Gateway to manage traffic, security, and routing.&lt;/p&gt;

&lt;p&gt;YARP (Yet Another Reverse Proxy) is Microsoft's high-performance reverse proxy built on ASP.NET Core.&lt;br&gt;
You can use it as a flexible and extensible foundation for building API Gateways in .NET.&lt;/p&gt;

&lt;p&gt;It features high performance and integrates seamlessly with ASP.NET Core.&lt;br&gt;
And it's pretty simple to set up.&lt;/p&gt;

&lt;p&gt;If you are using Ocelot, consider that YARP provides improved performance and more efficient integration with ASP.NET Core, making it a stronger choice for .NET environments.&lt;/p&gt;

&lt;p&gt;If you use complex API Gateways such as Traefik or Envoy, YARP can serve as a simpler solution if advanced features are unnecessary.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Getting Started with YARP&lt;/li&gt;
&lt;li&gt;Using YARP as an API Gateway for Microservices&lt;/li&gt;
&lt;li&gt;Load Balancing Across Service Instances&lt;/li&gt;
&lt;li&gt;Centralized Authentication and Authorization&lt;/li&gt;
&lt;li&gt;Request Routing and Path Rewriting&lt;/li&gt;
&lt;li&gt;YARP as Backend-For-Frontend (BFF) Gateway&lt;/li&gt;
&lt;li&gt;Traffic Shaping and Rate Limiting&lt;/li&gt;
&lt;li&gt;Observability and Centralized Logging&lt;/li&gt;
&lt;li&gt;Additional Scenarios You Might Consider&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting Started with YARP
&lt;/h2&gt;

&lt;p&gt;YARP is a reverse proxy toolkit developed by Microsoft for building fast and customizable proxy servers using ASP.NET Core infrastructure.&lt;/p&gt;

&lt;p&gt;Unlike traditional API Gateway solutions with fixed features, YARP offers building blocks you can compose for your needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why choose YARP:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built on ASP.NET Core, using the same high-performance infrastructure&lt;/li&gt;
&lt;li&gt;Fully extensible through middleware and custom transformations&lt;/li&gt;
&lt;li&gt;Configuration can be loaded from appsettings.json, C# code, or external sources&lt;/li&gt;
&lt;li&gt;Supports dynamic configuration updates without restarting the application&lt;/li&gt;
&lt;li&gt;Integrates seamlessly with existing ASP.NET Core features like authentication, logging, and health checks&lt;/li&gt;
&lt;li&gt;Open source and actively maintained by Microsoft&lt;/li&gt;
&lt;li&gt;Available as a &lt;a href="https://hub.docker.com/r/microsoft/dotnet-nightly-yarp" rel="noopener noreferrer"&gt;Docker container&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting started with YARP is easy.&lt;/p&gt;

&lt;p&gt;First, create a new ASP.NET Core Web API project and install the YARP NuGet package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Yarp.ReverseProxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is how to configure YARP in your &lt;code&gt;Program.cs&lt;/code&gt; 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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Add YARP services&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddReverseProxy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadFromConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ReverseProxy"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

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

&lt;span class="c1"&gt;// Map YARP routes&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;MapReverseProxy&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;This minimal setup lets YARP read the reverse proxy configuration from appsettings.json and sets up all the necessary routing.&lt;/p&gt;

&lt;p&gt;Let's create a simple reverse proxy that forwards requests to a backend service.&lt;/p&gt;

&lt;p&gt;Add this configuration to your &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&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;"LogLevel"&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;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Microsoft.AspNetCore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&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;"ReverseProxy"&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;"Routes"&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;"products-route"&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;"ClusterId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipments-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Match"&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;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/products/{**catch-all}"&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;span class="nl"&gt;"Clusters"&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;"shipments-cluster"&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;"Destinations"&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;"destination1"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5001/"&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;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;In this configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Routes&lt;/strong&gt; define how incoming requests are matched and which cluster handles them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match.Path&lt;/strong&gt; uses pattern matching - &lt;code&gt;{**catch-all}&lt;/code&gt; captures everything after &lt;code&gt;/products/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clusters&lt;/strong&gt; define groups of backend destinations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destinations&lt;/strong&gt; specify the actual backend service addresses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a client sends a request to &lt;code&gt;https://127.0.0.1:5000/products/123&lt;/code&gt; (assuming you run your YARP application on port 5000), YARP forwards it to &lt;code&gt;https://localhost:5001/products/123&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can also configure YARP entirely in code if you prefer:&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;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddReverseProxy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadFromMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;routes&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RouteConfig&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;RouteId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"products-route"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ClusterId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"shipments-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Match&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;RouteMatch&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="s"&gt;"/products/{**catch-all}"&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;clusters&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="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ClusterConfig&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ClusterId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"shipments-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Destinations&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;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&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;DestinationConfig&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="s"&gt;"destination1"&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;DestinationConfig&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="s"&gt;"https://localhost:5001/"&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="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;MapReverseProxy&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;This approach provides identical functionality with full code control, useful for dynamic configurations.&lt;/p&gt;

&lt;p&gt;YARP supports loading the proxy configuration from multiple sources. &lt;code&gt;LoadFromConfig&lt;/code&gt; may be called multiple times, referencing different IConfiguration sections from different config files, or it may be combined with a different config source, such as InMemory.&lt;/p&gt;

&lt;p&gt;In YARP, code-based configuration loaded via &lt;code&gt;IProxyConfigProvider&lt;/code&gt; generally takes priority or can override appsettings.json.&lt;/p&gt;

&lt;p&gt;Now that you have YARP set up, let's explore real-world scenarios where it's helpful.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Gateway for Microservices
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://antondevtips.com/blog/migrating-modular-monolith-to-microservices-in-dotnet" rel="noopener noreferrer"&gt;microservices architecture&lt;/a&gt;, you have multiple services, each handling a specific business boundary.&lt;br&gt;
An API Gateway acts as a single entry point for all client requests, routing them to the appropriate backend service.&lt;/p&gt;

&lt;p&gt;Without an API Gateway, clients need to know the address of every microservice. This creates tight coupling and makes it difficult to change service locations or add new services.&lt;/p&gt;

&lt;p&gt;YARP solves this by providing a unified endpoint that routes requests based on path patterns.&lt;/p&gt;

&lt;p&gt;Let's build an API Gateway for three microservices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shipment Service&lt;/strong&gt; - manages shipments and their states (runs on port 5001)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stock Service&lt;/strong&gt; - handles stock updates (runs on port 5002)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Service&lt;/strong&gt; - manages users (runs on port 5003)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the complete &lt;code&gt;appsettings.json&lt;/code&gt; configuration:&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;"Logging"&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;"LogLevel"&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;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Microsoft.AspNetCore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Yarp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&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;"ReverseProxy"&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;"Routes"&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;"products-route"&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;"ClusterId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipments-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Match"&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;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/shipments/{**catch-all}"&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;"orders-route"&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;"ClusterId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stocks-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Match"&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;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/stocks/{**catch-all}"&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;"customers-route"&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;"ClusterId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"users-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Match"&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;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/users/{**catch-all}"&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;span class="nl"&gt;"Clusters"&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;"shipments-cluster"&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;"Destinations"&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;"products-service"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5001/"&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;span class="nl"&gt;"stocks-cluster"&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;"Destinations"&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;"orders-service"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5002/"&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;span class="nl"&gt;"users-cluster"&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;"Destinations"&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;"customers-service"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5003/"&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;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;With this configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests to &lt;code&gt;https://gateway:5000/api/shipments/123&lt;/code&gt; route to the Shipments Service&lt;/li&gt;
&lt;li&gt;Requests to &lt;code&gt;https://gateway:5000/api/stocks/456&lt;/code&gt; route to the Stocks Service&lt;/li&gt;
&lt;li&gt;Requests to &lt;code&gt;https://gateway:5000/api/users/789&lt;/code&gt; route to the Users Service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;{**catch-all}&lt;/code&gt; pattern captures the entire path after the prefix, including any additional segments and query strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load Balancing Across Service Instances
&lt;/h2&gt;

&lt;p&gt;When your application scales, you may need to run multiple instances of the same service to handle increased traffic.&lt;br&gt;
Load balancing spreads incoming requests across these service instances to ensure no single instance becomes overwhelmed.&lt;/p&gt;

&lt;p&gt;YARP provides built-in load balancing strategies that automatically distribute traffic across multiple destinations within a cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load Balancing Strategies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;YARP supports several load balancing policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PowerOfTwoChoices&lt;/strong&gt; (default) - picks two random destinations and chooses the one with fewer active requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RoundRobin&lt;/strong&gt; - spreads requests evenly across all destinations in sequence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random&lt;/strong&gt; - randomly selects a destination for each request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FirstAlphabetical&lt;/strong&gt; - always picks the first destination alphabetically (useful for testing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeastRequests&lt;/strong&gt; - routes to the destination with the fewest active requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is how you can configure load balancing for the Shipment Service running on three instances:&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;"ReverseProxy"&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;"Routes"&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;"products-route"&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;"ClusterId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipments-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Match"&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;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/shipments/{**catch-all}"&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;span class="nl"&gt;"Clusters"&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;"shipments-cluster"&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;"LoadBalancingPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RoundRobin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Destinations"&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;"shipment-service-instance-1"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5001/"&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;"shipment-service-instance-2"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5011/"&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;"shipment-service-instance-3"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5012/"&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;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;With this configuration, YARP distributes requests evenly across all three instances using the Round-Robin strategy.&lt;/p&gt;

&lt;p&gt;Health checks let YARP route traffic only to healthy instances.&lt;br&gt;
If an instance fails its health check, YARP automatically stops sending requests to it until it recovers.&lt;/p&gt;

&lt;p&gt;First, install the health checks package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure health checks in &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ReverseProxy"&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;"Routes"&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;"products-route"&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;"ClusterId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shipments-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Match"&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;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/shipments/{**catch-all}"&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;span class="nl"&gt;"Clusters"&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;"shipments-cluster"&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;"LoadBalancingPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RoundRobin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"HealthCheck"&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;"Active"&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;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Interval"&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:00:10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Timeout"&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:00:05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ConsecutiveFailures"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/health"&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;"Passive"&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;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TransportFailureRate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"ReactivationPeriod"&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:01:00"&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;"Destinations"&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;"shipment-service-instance-1"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5001/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Health"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5001/health"&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;"shipment-service-instance-2"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5011/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Health"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5011/health"&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;"shipment-service-instance-3"&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;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5012/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Health"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5012/health"&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;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;&lt;strong&gt;Active Health Checks&lt;/strong&gt; periodically send requests to the &lt;code&gt;/health&lt;/code&gt; endpoint of each destination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interval&lt;/strong&gt; - how often to check (every 10 seconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout&lt;/strong&gt; - how long to wait for a response (5 seconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy&lt;/strong&gt; - when to mark a destination as unhealthy (after consecutive failures)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path&lt;/strong&gt; - the health check endpoint on the backend service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Passive Health Checks&lt;/strong&gt; monitor actual traffic and mark destinations as unhealthy based on real request failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Policy&lt;/strong&gt; - what triggers marking as unhealthy (transport failures like connection errors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ReactivationPeriod&lt;/strong&gt; - how long to wait before trying an unhealthy destination again (1 minute)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this to work, your backend services need to expose a health check endpoint. See &lt;a href="https://antondevtips.com/blog/how-to-set-up-production-ready-monitoring-with-aspnetcore-health-checks" rel="noopener noreferrer"&gt;this article&lt;/a&gt; for how to add Health Checks in ASP.NET Core.&lt;/p&gt;

&lt;p&gt;Sometimes you need to route requests from the same client to the same backend instance.&lt;br&gt;
This is called session affinity or sticky sessions.&lt;/p&gt;

&lt;p&gt;For more information, see the following &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/session-affinity" rel="noopener noreferrer"&gt;article&lt;/a&gt; from official Microsoft documentation.&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/yarp-as-api-gateway-in-dotnet" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/yarp-as-api-gateway-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 Validate Configuration in ASP.NET Core</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 25 Aug 2026 11:17:44 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-validate-configuration-in-aspnet-core-27mp</link>
      <guid>https://dev.to/antonmartyniuk/how-to-validate-configuration-in-aspnet-core-27mp</guid>
      <description>&lt;p&gt;Your app starts fine.&lt;br&gt;
The first few requests work.&lt;br&gt;
Then, hours later, one feature reaches for a setting that was never configured - and throws, deep in a request, far from the real cause.&lt;/p&gt;

&lt;p&gt;I've been through this multiple times.&lt;/p&gt;

&lt;p&gt;Configuration is just JSON or strings until you validate it.&lt;/p&gt;

&lt;p&gt;ASP.NET Core can check your configuration the moment the app starts, so a typo in &lt;code&gt;appsettings.json&lt;/code&gt; stops the app right away instead of returning a 500 Internal Error later.&lt;/p&gt;

&lt;p&gt;There are two ways to do this: Data Annotations and FluentValidation.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Why configuration validation matters&lt;/li&gt;
&lt;li&gt;Validate with Data Annotations&lt;/li&gt;
&lt;li&gt;Fail fast at startup with ValidateOnStart&lt;/li&gt;
&lt;li&gt;Validate with FluentValidation&lt;/li&gt;
&lt;li&gt;Cross-field rules and named options&lt;/li&gt;
&lt;li&gt;Data Annotations vs FluentValidation: when to use each&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Configuration Validation Matters
&lt;/h2&gt;

&lt;p&gt;Most apps bind configuration to a strongly typed class with the Options pattern.&lt;/p&gt;

&lt;p&gt;Here is a settings class for talking to the GitHub API:&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;class&lt;/span&gt; &lt;span class="nc"&gt;GitHubSettings&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;Token&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;BaseUrl&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;RetryCount&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;RetryDelaySeconds&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;It is bound from &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"GitHubSettings"&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;"Token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ghp_xxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"BaseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.github.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"RetryCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"RetryDelaySeconds"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GitHubSettings&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;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&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;GitHubSettings&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This binds the section, but it does not check anything.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;Token&lt;/code&gt; is missing or &lt;code&gt;RetryCount&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, binding still succeeds - you get an empty string and a zero.&lt;br&gt;
The problem only shows up later, when some code actually uses those values.&lt;/p&gt;

&lt;p&gt;An empty &lt;code&gt;Token&lt;/code&gt; sails through binding and only fails when you make your first GitHub call - as a 401, or a &lt;code&gt;NullReferenceException&lt;/code&gt; three layers deep, with nothing pointing back to the real cause: a missing config value.&lt;/p&gt;

&lt;p&gt;Validation closes that gap. It turns a silent, half-configured object into an immediate failure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the Options pattern is new to you, start with &lt;a href="https://antondevtips.com/blog/master-configuration-in-asp-net-core-with-the-options-pattern" rel="noopener noreferrer"&gt;Master Configuration in ASP.NET Core with the Options Pattern&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;👉 &lt;strong&gt;Read the full article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-validate-configuration-in-aspnet-core" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-validate-configuration-in-aspnet-core&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>How to Avoid Code Duplication in Vertical Slice Architecture in .NET</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 21 Aug 2026 08:24:27 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-avoid-code-duplication-in-vertical-slice-architecture-in-net-35fg</link>
      <guid>https://dev.to/antonmartyniuk/how-to-avoid-code-duplication-in-vertical-slice-architecture-in-net-35fg</guid>
      <description>&lt;p&gt;&lt;a href="https://antondevtips.com/blog/n-layered-vs-clean-vs-vertical-slice-architecture#vertical-slice-architecture" rel="noopener noreferrer"&gt;Vertical Slice Architecture&lt;/a&gt; gives you incredible speed and flexibility by organizing code around features instead of technical layers.&lt;/p&gt;

&lt;p&gt;Each slice encapsulates all aspects of a specific feature, including the API, business logic, and data access.&lt;br&gt;
When you implement each feature, the necessary code stays together in the same folder or file.&lt;/p&gt;

&lt;p&gt;Many developers were adopting Vertical Slice Architecture because of the following benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changes are isolated to specific features, reducing the risk of unintended side effects.&lt;/li&gt;
&lt;li&gt;Other developers and teams to work on different features independently.&lt;/li&gt;
&lt;li&gt;You can use different technologies or approaches within each slice as needed (aka CQRS on steroids).&lt;/li&gt;
&lt;li&gt;It's easier to navigate in the solution, as all the code for a specific feature stays together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have built many projects using N-Layered and Clean Architecture, and one of the biggest challenges I faced was too much abstraction.&lt;br&gt;
These types of Architectures often result in you creating a lot of premature abstractions that tend to solve some future problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if a database might change in the future&lt;/li&gt;
&lt;li&gt;What if we need to replace a messaging or logging library&lt;/li&gt;
&lt;li&gt;What if this, what if that...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why I liked the Vertical Slice Architecture approach from the start.&lt;br&gt;
I finally have the freedom to get rid of many premature abstractions and have the actual implementation sit in my slices.&lt;/p&gt;

&lt;p&gt;But this freedom comes with a challenge: you can end up with a lot of code duplication:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two slices need similar validation logic.&lt;/li&gt;
&lt;li&gt;Three features query the same database table.&lt;/li&gt;
&lt;li&gt;Five handlers format dates the same way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why some devs critique Vertical Slice Architecture, saying it violates the DRY principle and encourages the WET Principle (write everything twice).&lt;/p&gt;

&lt;p&gt;I have built a few projects with Vertical Slice Architecture in the last few years, and I want to show you my pragmatic approach to managing duplication.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Why Vertical Slice Architecture Leads to Code Duplication&lt;/li&gt;
&lt;li&gt;Detecting Code Duplication&lt;/li&gt;
&lt;li&gt;How to Avoid Duplication in Database Concerns&lt;/li&gt;
&lt;li&gt;How to Avoid Duplication in Infrastructure Concerns&lt;/li&gt;
&lt;li&gt;How to Avoid Duplication in Business Concerns&lt;/li&gt;
&lt;li&gt;How to Avoid Duplication in Application Concerns&lt;/li&gt;
&lt;li&gt;Decision Framework When Addressing Code Duplication&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  Why Vertical Slice Architecture Leads to Code Duplication
&lt;/h2&gt;

&lt;p&gt;In traditional layered architecture, you organize code by technical concerns: Controllers, Services, Repositories, and Models.&lt;/p&gt;

&lt;p&gt;When you need to add a new feature, you touch multiple layers. You add a controller method, a service method, a repository method, and maybe a DTO.&lt;/p&gt;

&lt;p&gt;This structure naturally pushes you toward reuse.&lt;br&gt;
If two features need similar logic, you put it in a shared service.&lt;br&gt;
If three features query the same table, you add a method to the repository.&lt;/p&gt;

&lt;p&gt;Vertical Slice Architecture flips this approach.&lt;/p&gt;

&lt;p&gt;Each feature is a vertical slice that contains everything it needs: the endpoint, the handler, the validation, the data access, and the response model.&lt;/p&gt;

&lt;p&gt;Here is what a typical VSA project structure looks like:&lt;/p&gt;

&lt;p&gt;Each slice is independent. &lt;code&gt;CreateShipment&lt;/code&gt; feature does not call &lt;code&gt;GetShipmentByNumber&lt;/code&gt; and doesn't share code with other features.&lt;br&gt;
This independence is the superpower of VSA. You can change one feature without worrying about breaking another.&lt;/p&gt;

&lt;p&gt;But this independence also makes duplication visible.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;CreateShipment&lt;/code&gt; and &lt;code&gt;ProcessShipment&lt;/code&gt; both need to validate that a product exists, you will write that validation twice.&lt;/p&gt;

&lt;p&gt;But before addressing every code duplication case, you need to stop and think for a while.&lt;/p&gt;

&lt;p&gt;Two pieces of code can look identical today and evolve in completely different directions tomorrow.&lt;/p&gt;

&lt;p&gt;If you share them too early, you create coupling.&lt;br&gt;
When one feature needs to change, you have to modify shared code that affects the other feature.&lt;/p&gt;

&lt;p&gt;This is the cost of premature abstraction: you trade the flexibility to change one feature independently for the illusion of DRY code.&lt;/p&gt;

&lt;p&gt;And the most important part is that most developers misunderstand DRY.&lt;br&gt;
They think it's about removing duplicate code. It's not.&lt;/p&gt;

&lt;p&gt;DRY is about duplicated knowledge, not the code.&lt;/p&gt;

&lt;p&gt;Duplicate code is only a symptom. The real problem is duplicated business rules hiding across the system.&lt;/p&gt;

&lt;p&gt;Here is where most teams get it wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ They extract shared helpers too early.&lt;/li&gt;
&lt;li&gt;❌ They create generic utilities that are hard to maintain&lt;/li&gt;
&lt;li&gt;❌ They couple unrelated features just to reduce duplication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This actually makes the code worse. Sometimes duplication is fine.&lt;/p&gt;

&lt;p&gt;If two pieces of code change for different reasons, keep them separate.&lt;br&gt;
This follows Single Responsibility better than forced reuse.&lt;/p&gt;

&lt;p&gt;DRY is violated only when the same reason to change exists in multiple places.&lt;/p&gt;

&lt;p&gt;Okay, enough theory.&lt;br&gt;
Now, let's explore how we can avoid duplication and when and where we should extract shared code.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Read the full article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-avoid-code-duplication-in-vertical-slice-architecture-in-dotnet" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-avoid-code-duplication-in-vertical-slice-architecture-in-dotnet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Getting Started with System Design: 7 Key Decisions</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 18 Aug 2026 11:49:59 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/getting-started-with-system-design-7-key-decisions-3aig</link>
      <guid>https://dev.to/antonmartyniuk/getting-started-with-system-design-7-key-decisions-3aig</guid>
      <description>&lt;p&gt;Most developers learn system design in the wrong way, especially with the help of AI.&lt;/p&gt;

&lt;p&gt;They usually study concepts such as load balancing, the CAP theorem, Kafka and Saga.&lt;br&gt;
Something AI can explain.&lt;/p&gt;

&lt;p&gt;But knowing the components is not the same as designing a system.&lt;/p&gt;

&lt;p&gt;Every real system is the result of a handful of decisions and trade-offs, and each one costs you something.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pick strong consistency, and you pay in latency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pick a queue, and you pay in debugging across multiple services.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pick microservices, and you pay in premium operations.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over the past years, I have built and scaled .NET systems that made every one of these choices, sometimes badly.&lt;br&gt;
This is the short version of what I wish someone had handed me at the start.&lt;/p&gt;

&lt;p&gt;In this post, we will explore the 7 decisions every system has to make:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How fresh does this data have to be?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Where should this data live?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do you buy a bigger machine or more machines?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What do you cache, and where?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Should this be a call or an event?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What happens when more work arrives than you can handle?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How many things do you deploy?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  How Fresh Does This Data Have To Be?
&lt;/h2&gt;

&lt;p&gt;Most teams answer this once, for the whole system, and then live with the answer for years.&lt;br&gt;
That is the mistake. Freshness is a per-data-type decision.&lt;/p&gt;

&lt;p&gt;A payment balance and a product review count do not need the same guarantee.&lt;br&gt;
Treating them the same means you either overpay for the reviews or underprotect the money.&lt;/p&gt;

&lt;p&gt;These are the models you are actually choosing between:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What you are promised&lt;/th&gt;
&lt;th&gt;Typical use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strong (linearizable)&lt;/td&gt;
&lt;td&gt;Every read sees the latest committed write, from any node.&lt;/td&gt;
&lt;td&gt;Bank balances, stock counts, leader election.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read-after-write&lt;/td&gt;
&lt;td&gt;A user always sees their own most recent write.&lt;/td&gt;
&lt;td&gt;Your own post appearing in your own feed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monotonic reads&lt;/td&gt;
&lt;td&gt;Once you see a value, you never see an older one again.&lt;/td&gt;
&lt;td&gt;News feeds, where going backward looks broken.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Causal&lt;/td&gt;
&lt;td&gt;If A caused B, every reader sees A before B.&lt;/td&gt;
&lt;td&gt;Comment threads, chat messages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eventual&lt;/td&gt;
&lt;td&gt;All replicas converge given enough time and no new writes.&lt;/td&gt;
&lt;td&gt;Analytics counters, review totals.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now the part most articles get wrong.&lt;br&gt;
The CAP theorem says a distributed system can only guarantee two of Consistency, Availability, and Partition tolerance.&lt;/p&gt;

&lt;p&gt;That "pick two of three" framing is the part Eric Brewer himself walked back in 2012.&lt;br&gt;
Partition tolerance is not optional because networks fail, and a system that does not tolerate them just falls over.&lt;/p&gt;

&lt;p&gt;So the real choice is binary, and only active during a partition.&lt;br&gt;
Either you keep accepting writes and risk divergence (AP), or you refuse writes to protect correctness (CP).&lt;br&gt;
CA databases do not exist in any meaningful sense.&lt;/p&gt;

&lt;p&gt;Network partition happens&lt;/p&gt;

&lt;p&gt;Can the system&lt;br&gt;
safely guess?&lt;/p&gt;

&lt;p&gt;Pick CP&lt;br&gt;
Refuse the request&lt;/p&gt;

&lt;p&gt;Pick AP&lt;br&gt;
Answer anyway&lt;/p&gt;

&lt;p&gt;Bank ledger&lt;br&gt;
Booking seats&lt;br&gt;
Leader election&lt;br&gt;
Stock counts&lt;/p&gt;

&lt;p&gt;Social feed&lt;br&gt;
Shopping cart&lt;br&gt;
DNS&lt;br&gt;
CDN cache&lt;/p&gt;

&lt;p&gt;A more honest everyday framing is &lt;strong&gt;PACELC&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;P&lt;/strong&gt;artition: choose &lt;strong&gt;A&lt;/strong&gt;vailability or &lt;strong&gt;C&lt;/strong&gt;onsistency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;E&lt;/strong&gt;lse: choose &lt;strong&gt;L&lt;/strong&gt;atency or &lt;strong&gt;C&lt;/strong&gt;onsistency.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the network is healthy, and it usually is, you trade consistency against latency on every write.&lt;br&gt;
Synchronous quorum costs roughly 1-5 ms per write inside a region and 50-200 ms across regions, and a US East to Europe round trip is 80-100 ms before your code does anything.&lt;/p&gt;

&lt;p&gt;CAP only shows up when something is on fire. PACELC is the tax you pay every day.&lt;/p&gt;

&lt;p&gt;When you do have to pick a side for a specific piece of data, answer these four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is the cost of stale data?&lt;/strong&gt; An old balance leads to a bad decision. An old like count leads to nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What is the cost of downtime?&lt;/strong&gt; If 30 seconds of unavailability costs thousands in lost sales, lean toward AP.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can you reconcile later?&lt;/strong&gt; Merging two shopping carts is easy. Merging two conflicting bank transfers is not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How long do partitions last?&lt;/strong&gt; Short ones favour CP. Long multi-region ones favor AP.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most production systems use both CP for payments, orders, and auth tokens and AP for the catalog, recommendations, and activity tracking.&lt;/p&gt;

&lt;p&gt;Once you know how fresh each piece of data must be, the list of databases that can hold it gets much shorter.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Read the full article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/getting-started-with-system-design" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/getting-started-with-system-design&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Set Up Production-Ready Monitoring With ASP.NET Core Health Checks</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 14 Aug 2026 08:13:52 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-set-up-production-ready-monitoring-with-aspnet-core-health-checks-14a9</link>
      <guid>https://dev.to/antonmartyniuk/how-to-set-up-production-ready-monitoring-with-aspnet-core-health-checks-14a9</guid>
      <description>&lt;p&gt;Deploying an application to production is only the beginning. You need to know whether your application is healthy and whether your database, cache, and message queue are reachable.&lt;/p&gt;

&lt;p&gt;Without proper monitoring, you will only find out about issues when your users start complaining.&lt;/p&gt;

&lt;p&gt;I have spent years building and maintaining high-load .NET systems, and health checks have always been my first line of defense. Today, I want to show you how to set up a production-ready monitoring system that gives you complete visibility into your application's health.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Why health checks matter for production applications&lt;/li&gt;
&lt;li&gt;Adding health checks for Postgres, Redis, MongoDB, and RabbitMQ&lt;/li&gt;
&lt;li&gt;Building custom health checkers&lt;/li&gt;
&lt;li&gt;Building a custom JSON response formatter&lt;/li&gt;
&lt;li&gt;Implementing a custom health check publisher&lt;/li&gt;
&lt;li&gt;Setting up a professional Health Checks UI&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  Why Health Checks Matter
&lt;/h2&gt;

&lt;p&gt;When you run an application in a modern environment like Kubernetes or Azure, the platform needs to know if your app is ready to handle traffic. This is where health checks come in.&lt;/p&gt;

&lt;p&gt;A health check is a simple endpoint that returns the status of your application. If the database or message queue is down, the health check should report that the service is "Unhealthy".&lt;/p&gt;

&lt;p&gt;Without health checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Load balancers might send traffic to a broken instance.&lt;/li&gt;
&lt;li&gt;You won't know about database connection issues until users report errors.&lt;/li&gt;
&lt;li&gt;Background tasks might stop running without anyone noticing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ASP.NET Core provides a built-in framework for health checks. It allows you to check everything from database connections to external APIs and system resources.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Read the full article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-set-up-production-ready-monitoring-with-aspnetcore-health-checks" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-set-up-production-ready-monitoring-with-aspnetcore-health-checks&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>REST API Security Best Practices in ASP.NET Core</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 11 Aug 2026 11:20:09 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/rest-api-security-best-practices-in-aspnet-core-3jph</link>
      <guid>https://dev.to/antonmartyniuk/rest-api-security-best-practices-in-aspnet-core-3jph</guid>
      <description>&lt;p&gt;It does not matter how clean your architecture is or how fast your queries run - if an attacker can read another user's orders or forge a token, none of that matters.&lt;/p&gt;

&lt;p&gt;Most API security problems come from skipping the basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not updated NuGet package with security vulnerability&lt;/li&gt;
&lt;li&gt;Missing validation check&lt;/li&gt;
&lt;li&gt;Weak authentication setup&lt;/li&gt;
&lt;li&gt;An over-permissive CORS policy&lt;/li&gt;
&lt;li&gt;A secret committed to source control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The good news is that ASP .NET Core gives you almost everything you need built in.&lt;/p&gt;

&lt;p&gt;Over the years, I have shipped and reviewed many .NET APIs, and the same set of practices keeps them safe.&lt;/p&gt;

&lt;p&gt;Here are 18 of them, each with the code to apply it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Enforce HTTPS everywhere&lt;/li&gt;
&lt;li&gt;Authenticate with tokens, not sessions&lt;/li&gt;
&lt;li&gt;Validate the JWT signature, issuer, audience, and lifetime&lt;/li&gt;
&lt;li&gt;Authorize with policies, not just [Authorize]&lt;/li&gt;
&lt;li&gt;Apply the principle of least privilege&lt;/li&gt;
&lt;li&gt;Validate and sanitize all input&lt;/li&gt;
&lt;li&gt;Protect against over-posting / mass assignment&lt;/li&gt;
&lt;li&gt;Validate content types and limit request size&lt;/li&gt;
&lt;li&gt;Use parameterized queries and EF Core&lt;/li&gt;
&lt;li&gt;Implement rate limiting and throttling&lt;/li&gt;
&lt;li&gt;Configure CORS restrictively&lt;/li&gt;
&lt;li&gt;Return minimal error detail&lt;/li&gt;
&lt;li&gt;Set security headers&lt;/li&gt;
&lt;li&gt;Store secrets securely&lt;/li&gt;
&lt;li&gt;Enforce CSRF protection where relevant&lt;/li&gt;
&lt;li&gt;Version your API and deprecate insecure endpoints&lt;/li&gt;
&lt;li&gt;Log and audit security events&lt;/li&gt;
&lt;li&gt;Keep dependencies patched&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  1. Enforce HTTPS Everywhere
&lt;/h2&gt;

&lt;p&gt;Every request to your API should travel over an encrypted connection.&lt;/p&gt;

&lt;p&gt;Without HTTPS, tokens, passwords, and personal data move in plain text, where anyone on the network path can read them.&lt;br&gt;
Plain HTTP also opens the door to downgrade attacks, in which an attacker forces a client to use an insecure connection.&lt;/p&gt;

&lt;p&gt;ASP .NET Core gives you two tools. &lt;code&gt;UseHttpsRedirection&lt;/code&gt; redirects HTTP requests to HTTPS, and HSTS (HTTP Strict Transport Security) tells browsers to only ever connect over HTTPS:&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;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHsts&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;MaxAge&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;365&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;IncludeSubDomains&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Preload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="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="k"&gt;if&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="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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHsts&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;UseHttpsRedirection&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;UseHttpsRedirection&lt;/code&gt; upgrades any HTTP request to HTTPS.&lt;br&gt;
&lt;code&gt;UseHsts&lt;/code&gt; adds the &lt;code&gt;Strict-Transport-Security&lt;/code&gt; header, so compliant browsers refuse to talk to your API over plain HTTP at all - which blocks downgrade attacks.&lt;/p&gt;

&lt;p&gt;HSTS is skipped during development because you often use &lt;code&gt;http://localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the full setup, see &lt;a href="https://antondevtips.com/blog/configuring-https-redirection-and-hsts-in-aspnet-core" rel="noopener noreferrer"&gt;Configuring HTTPS Redirection and HSTS in ASP .NET Core&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Read the full article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/rest-api-security-best-practices-in-aspnetcore" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/rest-api-security-best-practices-in-aspnetcore&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Start a New .NET Project in 2026</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:16:35 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/how-to-start-a-new-net-project-in-2026-5bbb</link>
      <guid>https://dev.to/antonmartyniuk/how-to-start-a-new-net-project-in-2026-5bbb</guid>
      <description>&lt;p&gt;Starting a new .NET project is exciting, but it can also be overwhelming. There are so many decisions to make, and the choices you make in the first few days will affect your project for months or even years.&lt;/p&gt;

&lt;p&gt;I have created dozens of .NET projects over the years, and today I want to share my experience with you.&lt;/p&gt;

&lt;p&gt;In this post, we will explore the following essential steps to start a new .NET project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set Project-Wide Standards&lt;/li&gt;
&lt;li&gt;Add Static Code Analysis Packages&lt;/li&gt;
&lt;li&gt;Enforce Coding Standards&lt;/li&gt;
&lt;li&gt;Centralize Package Management&lt;/li&gt;
&lt;li&gt;Simplify Local Development with Aspire&lt;/li&gt;
&lt;li&gt;Setup OpenTelemetry&lt;/li&gt;
&lt;li&gt;Automate Build and Testing with GitHub Actions&lt;/li&gt;
&lt;li&gt;Project Template to Start With&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  1. Directory.Build.props - Set Project-Wide Standards
&lt;/h2&gt;

&lt;p&gt;Every .NET solution should start with a Directory.Build.props 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 .csproj files. When you want to change a setting, you have to update every project file manually. This leads to inconsistencies and wasted time.&lt;/p&gt;

&lt;p&gt;Directory.Build.props solves this problem by centralizing configuration in one place. You create this file in the same directory as your .sln file, and MSBuild automatically 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;If needed, you can also put Directory.Build.props on any directory level in your project hierarchy and override settings there.&lt;/p&gt;

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

&lt;p&gt;Nullable: 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;ImplicitUsings: Automatically includes common namespace imports in every file. You do not need to write using System; or using System.Linq; anymore.&lt;/p&gt;

&lt;p&gt;AnalysisLevel: 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;AnalysisMode: Enables all code analysis rules. This gives you the most comprehensive feedback on code quality.&lt;/p&gt;

&lt;p&gt;TreatWarningsAsErrors: Prevents compilation if there are any warnings. This forces you to fix issues immediately rather than letting them accumulate.&lt;/p&gt;

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

&lt;p&gt;EnforceCodeStyleInBuild: 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;These settings create a strong foundation for code quality. They catch issues early and enforce consistency across your entire solution.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read the full article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-start-a-new-dotnet-project-in-2026" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-start-a-new-dotnet-project-in-2026&lt;/a&gt;&lt;/p&gt;

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