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

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

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

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

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

&lt;ul&gt;
&lt;li&gt;Why SignalR Needs Authentication&lt;/li&gt;
&lt;li&gt;Setting Up JWT Authentication for SignalR&lt;/li&gt;
&lt;li&gt;How to Stream Events with SignalR for a Given User&lt;/li&gt;
&lt;li&gt;Role-Based Authorization on Hub Methods&lt;/li&gt;
&lt;li&gt;Connecting from a JavaScript Client&lt;/li&gt;
&lt;li&gt;Connecting from a .NET Client&lt;/li&gt;
&lt;li&gt;Security Best Practices for SignalR&lt;/li&gt;
&lt;/ul&gt;

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



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-add-jwt-authentication-to-signalr-hubs-in-aspnetcore" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-add-jwt-authentication-to-signalr-hubs-in-aspnetcore&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why SignalR Needs Authentication
&lt;/h2&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Here is how you can configure SignalR in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSignalR&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

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

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

&lt;span class="c1"&gt;// Map the SignalR hub&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MapHub&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;StockPriceHub&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hubs/stocks"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to configure JWT bearer authentication and add the &lt;code&gt;OnMessageReceived&lt;/code&gt; event handler that extracts the token from the query string for SignalR connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAuthentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultAuthenticateScheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultChallengeScheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JwtBearerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticationScheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddJwtBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SymmetricSecurityKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TokenValidationParameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TokenValidationParameters&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ValidateIssuer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ValidateAudience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ValidateLifetime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ValidateIssuerSigningKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ValidIssuer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Issuer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ValidAudience&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Audience&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IssuerSigningKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JwtBearerEvents&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;OnMessageReceived&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartsWithSegments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hubs"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a browser connects to a SignalR hub via WebSockets or SSE, it can't send the &lt;code&gt;Authorization&lt;/code&gt; header.&lt;br&gt;
The JavaScript client sends the token as &lt;code&gt;?access_token=&amp;lt;token&amp;gt;&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Token extraction is limited to paths starting with &lt;code&gt;/hubs&lt;/code&gt;.&lt;br&gt;
This avoids reading tokens from query strings on other endpoints.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/how-to-add-jwt-authentication-to-signalr-hubs-in-aspnetcore" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/how-to-add-jwt-authentication-to-signalr-hubs-in-aspnetcore&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

&lt;p&gt;We will build a Shopping Cart that demonstrates all the core Event Sourcing concepts: events, streams, aggregates, commands, and projections.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;What is Event Sourcing&lt;/li&gt;
&lt;li&gt;Overview of the Shopping Cart Application&lt;/li&gt;
&lt;li&gt;Setting Up Marten with PostgreSQL&lt;/li&gt;
&lt;li&gt;Defining Domain Events&lt;/li&gt;
&lt;li&gt;Building the Aggregate&lt;/li&gt;
&lt;li&gt;Writing Commands: Appending Events to Streams&lt;/li&gt;
&lt;li&gt;Building Read Models with Projections&lt;/li&gt;
&lt;li&gt;The Query Side: Reading Projected Data&lt;/li&gt;
&lt;li&gt;When to Use Event Sourcing&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

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




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;This structure is similar to what I use in my &lt;a href="https://antondevtips.com/blog/building-a-modular-monolith-with-vertical-slice-architecture-in-dotnet" rel="noopener noreferrer"&gt;Modular Monolith&lt;/a&gt; projects, but simplified for a single module.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/getting-started-with-event-sourcing-in-dotnet-with-marten-and-postgresql&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Humanizer in .NET: Turn Strings, Dates, and Numbers Into Human-Friendly Text</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 22 Sep 2026 11:11:31 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/humanizer-in-net-turn-strings-dates-and-numbers-into-human-friendly-text-10ck</link>
      <guid>https://dev.to/antonmartyniuk/humanizer-in-net-turn-strings-dates-and-numbers-into-human-friendly-text-10ck</guid>
      <description>&lt;p&gt;Software stores data the way machines like it. Users want to read it the way humans like it.&lt;/p&gt;

&lt;p&gt;So you write this boilerplate code every day.&lt;/p&gt;

&lt;p&gt;You manually split a PascalCase enum into words. You turn 2 into "2nd" with a few if statements.&lt;/p&gt;

&lt;p&gt;You format a timestamp as "3 hours ago". You pluralize "item" only when the count is not 1.&lt;/p&gt;

&lt;p&gt;It is a few lines here, a helper method there - and it piles up across a codebase.&lt;/p&gt;

&lt;p&gt;Humanizer is a small, free .NET library that does all of it for you.&lt;/p&gt;

&lt;p&gt;It turns developer-shaped data into human-friendly text with a single method call.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;How to Humanize and transform strings&lt;/li&gt;
&lt;li&gt;How to Humanize enums&lt;/li&gt;
&lt;li&gt;How to Humanize dates and times&lt;/li&gt;
&lt;li&gt;How to Turn numbers into words&lt;/li&gt;
&lt;li&gt;How to Work with culture&lt;/li&gt;
&lt;li&gt;How to Pluralize and quantify&lt;/li&gt;
&lt;li&gt;How to Format byte sizes and large numbers&lt;/li&gt;
&lt;li&gt;Bonus: Roman numerals, collections, and fluent dates&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/humanizer-in-dotnet-turn-strings-dates-and-numbers-into-human-friendly-text" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/humanizer-in-dotnet-turn-strings-dates-and-numbers-into-human-friendly-text&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Humanize and Transform Strings
&lt;/h2&gt;

&lt;p&gt;First, add Humanizer to your project by installing 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 Humanizer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core of Humanizer is Humanize(), which turns a developer string into a readable sentence.&lt;/p&gt;

&lt;p&gt;It splits strings in PascalCase, camelCase, snake_case, kebab-case, all in lower or UPPER case, into words and applies sentence casing:&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="s"&gt;"PascalCaseInputStringIsTurnedIntoSentence"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Humanize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; "Pascal case input string is turned into sentence"&lt;/span&gt;

&lt;span class="s"&gt;"Underscored_input_string_is_turned_into_sentence"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Humanize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; "Underscored input string is turned into sentence"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transform changes the casing without splitting words. It takes one or more strategies:&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="s"&gt;"Sentence casing"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TitleCase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// =&amp;gt; "Sentence Casing"&lt;/span&gt;
&lt;span class="s"&gt;"Sentence casing"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LowerCase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// =&amp;gt; "sentence casing"&lt;/span&gt;
&lt;span class="s"&gt;"Sentence casing"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SentenceCase&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// =&amp;gt; "Sentence casing"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dehumanize is the reverse of Humanize - it turns a sentence back into a PascalCase identifier:&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="s"&gt;"Pascal case input string is turned into sentence"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dehumanize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; "PascalCaseInputStringIsTurnedIntoSentence"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Truncate shortens text to a length, adding an ellipsis:&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="s"&gt;"Long text to truncate"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Truncate&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="c1"&gt;// =&amp;gt; "Long text…"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These four cover most of the string formatting you write by hand: generating labels, display names, and identifiers from code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Humanize Enums
&lt;/h2&gt;

&lt;p&gt;Enum names are in PascalCase, and your UI needs readable labels.&lt;/p&gt;

&lt;p&gt;Normally, you map each enum member to a label by hand:&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;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutForDelivery&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Out for delivery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InTransit&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"In transit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... a line for every member&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling Humanize() on the enum value does the same thing for free. It splits the name into words, so the switch disappears:&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;enum&lt;/span&gt; &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;InTransit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OutForDelivery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Delivered&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutForDelivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Humanize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// =&amp;gt; "Out for delivery"&lt;/span&gt;
&lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InTransit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Humanize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// =&amp;gt; "In transit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you need a label that does not match the member name, add a [Description] attribute and Humanizer uses it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Awaiting pickup"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Humanize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// =&amp;gt; "Awaiting pickup"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also go the other way, turning a display string back into the enum value with DehumanizeTo:&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="s"&gt;"Out for delivery"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DehumanizeTo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// =&amp;gt; ShipmentStatus.OutForDelivery&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone removes a surprising amount of mapping code from the boundary between your domain and your UI.&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/humanizer-in-dotnet-turn-strings-dates-and-numbers-into-human-friendly-text" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/humanizer-in-dotnet-turn-strings-dates-and-numbers-into-human-friendly-text&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Synchronous vs Asynchronous Communication in Microservices: How to Choose the Right Approach</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 18 Sep 2026 08:20:21 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/synchronous-vs-asynchronous-communication-in-microservices-how-to-choose-the-right-approach-3p9k</link>
      <guid>https://dev.to/antonmartyniuk/synchronous-vs-asynchronous-communication-in-microservices-how-to-choose-the-right-approach-3p9k</guid>
      <description>&lt;p&gt;Choosing the right communication style between &lt;a href="https://antondevtips.com/blog/building-event-driven-microservices-with-azure-service-bus-in-dotnet" rel="noopener noreferrer"&gt;microservices&lt;/a&gt; is one of the most important architectural decisions you will make.&lt;/p&gt;

&lt;p&gt;Get it wrong, and you end up with a fragile system where one failing service brings everything down.&lt;br&gt;
Get it right, and your services stay independent, scalable, and resilient.&lt;/p&gt;

&lt;p&gt;In distributed systems, there are two fundamental ways services talk to each other: synchronous and asynchronous communication.&lt;br&gt;
Each comes with trade-offs in latency, coupling, reliability, and complexity.&lt;/p&gt;

&lt;p&gt;Throughout my career, I have designed and built distributed systems using both approaches.&lt;br&gt;
In this post, I will break down how each communication style works, when to use each one, and how real systems often combine both to achieve the best results.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;How Microservices Communicate&lt;/li&gt;
&lt;li&gt;Synchronous Communication: HTTP and gRPC&lt;/li&gt;
&lt;li&gt;Asynchronous Communication: Message Brokers and Event Streaming&lt;/li&gt;
&lt;li&gt;Synchronous vs Asynchronous: Deep Comparison&lt;/li&gt;
&lt;li&gt;Hybrid Communication: Mixing Sync and Async in One Flow&lt;/li&gt;
&lt;li&gt;How to Choose: Decision Checklist&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

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



&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/synchronous-vs-asynchronous-communication-in-microservices" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/synchronous-vs-asynchronous-communication-in-microservices&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How Microservices Communicate
&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; application, components call each other through in-process method calls.&lt;br&gt;
It's fast, simple, and reliable.&lt;/p&gt;

&lt;p&gt;But when you &lt;a href="https://antondevtips.com/blog/migrating-modular-monolith-to-microservices-in-dotnet" rel="noopener noreferrer"&gt;break a Modular Monolith&lt;/a&gt; into microservices, those calls become network calls.&lt;br&gt;
And network calls introduce latency, failures, and a whole new set of challenges.&lt;/p&gt;

&lt;p&gt;There are two fundamental approaches for microservices communication:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synchronous communication:&lt;/strong&gt; the caller sends a request and waits for a response before continuing.&lt;br&gt;
The caller is blocked until the response arrives or a timeout occurs.&lt;/p&gt;

&lt;p&gt;Think of it like a phone call - you dial, the other person picks up, you talk, and you wait for an answer before continuing the conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asynchronous communication:&lt;/strong&gt; the caller sends a message and moves on without waiting for an immediate response.&lt;br&gt;
The message is processed later by the receiving service at its own pace.&lt;/p&gt;

&lt;p&gt;Think of it like sending an email - you send it and continue with your work, without waiting for the recipient to read and reply.&lt;/p&gt;

&lt;p&gt;These two styles lead to fundamentally different system behaviors in terms of coupling, resilience, and scalability.&lt;/p&gt;

&lt;p&gt;Now let's explore each communication style in detail.&lt;/p&gt;
&lt;h2&gt;
  
  
  Synchronous Communication: HTTP and gRPC
&lt;/h2&gt;

&lt;p&gt;Synchronous communication is the most intuitive way for services to talk.&lt;br&gt;
Service A calls Service B, waits for the result, and then continues its work.&lt;/p&gt;

&lt;p&gt;The two most popular protocols for synchronous communication in .NET are HTTP (REST) and gRPC.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can also use &lt;a href="https://chillicream.com/docs/strawberryshake/v15" rel="noopener noreferrer"&gt;HotChocolate GraphQL Strawberry Shake&lt;/a&gt; for HTTP/REST communication. But this is a pretty niche approach. If you want to learn more about HotChocolate GraphQL, see this &lt;a href="https://antondevtips.com/blog/getting-started-with-hot-chocolate-graphql" rel="noopener noreferrer"&gt;article&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  HTTP/REST
&lt;/h3&gt;

&lt;p&gt;HTTP-based REST APIs are the most common way microservices communicate synchronously.&lt;br&gt;
They are simple to build, easy to debug, and supported by virtually every language and framework.&lt;/p&gt;

&lt;p&gt;Here is a typical example. The &lt;strong&gt;Booking Service&lt;/strong&gt; needs to check room availability from the &lt;strong&gt;Property Service&lt;/strong&gt; before confirming a reservation:&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;PropertyServiceClient&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;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_httpClient&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;PropertyServiceClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpClient&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;RoomAvailability&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CheckAvailabilityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;propertyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateOnly&lt;/span&gt; &lt;span class="n"&gt;checkIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateOnly&lt;/span&gt; &lt;span class="n"&gt;checkOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_httpClient&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/properties/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;propertyId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/availability?checkIn=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;checkIn&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;checkOut=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;checkOut&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&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;response&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;RoomAvailability&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;HTTP/REST works well when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need a simple request-response interaction&lt;/li&gt;
&lt;li&gt;The client needs data immediately to continue&lt;/li&gt;
&lt;li&gt;You are building public APIs&lt;/li&gt;
&lt;li&gt;You want broad compatibility across languages and platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main downside is &lt;strong&gt;temporal coupling&lt;/strong&gt; - the caller must wait for the receiver to respond.&lt;br&gt;
If the Property Service is slow or down, the Booking Service is stuck.&lt;/p&gt;
&lt;h3&gt;
  
  
  gRPC
&lt;/h3&gt;

&lt;p&gt;gRPC is a high-performance RPC (Remote Procedure Call) framework built on HTTP/2.&lt;br&gt;
It uses Protocol Buffers (Protobuf) for serialization, which is significantly faster and smaller than JSON.&lt;/p&gt;

&lt;p&gt;gRPC shines in service-to-service communication where performance matters.&lt;/p&gt;

&lt;p&gt;Let's explore an example: how to check the room availability with gRPC.&lt;/p&gt;

&lt;p&gt;First, define the service contract in a &lt;code&gt;.proto&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="na"&gt;syntax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"proto3"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;option&lt;/span&gt; &lt;span class="na"&gt;csharp_namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"BookingPlatform.Protos"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;service&lt;/span&gt; &lt;span class="n"&gt;PropertyService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;CheckAvailability&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AvailabilityRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AvailabilityResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;AvailabilityRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="na"&gt;property_id&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;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;check_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;check_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;AvailabilityResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="na"&gt;is_available&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;span class="kt"&gt;double&lt;/span&gt; &lt;span class="na"&gt;price_per_night&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&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;Then call it from the Booking 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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PropertyGrpcClient&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;PropertyService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropertyServiceClient&lt;/span&gt; &lt;span class="n"&gt;_client&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;PropertyGrpcClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PropertyService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropertyServiceClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&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;client&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;AvailabilityResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CheckAvailabilityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;propertyId&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;checkIn&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;checkOut&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;request&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;AvailabilityRequest&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;PropertyId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;propertyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;CheckIn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checkIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;CheckOut&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checkOut&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;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckAvailabilityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;gRPC works well when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need low-latency, high-throughput communication between internal services&lt;/li&gt;
&lt;li&gt;You have strict performance requirements&lt;/li&gt;
&lt;li&gt;You want strongly typed contracts between services&lt;/li&gt;
&lt;li&gt;You need streaming capabilities (server streaming, client streaming, or bidirectional)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;gRPC is not ideal for browser-to-service communication (though gRPC-Web exists as a workaround) or when you need human-readable payloads for debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Failures in Synchronous Communication
&lt;/h3&gt;

&lt;p&gt;The biggest risk with synchronous communication is cascading failures.&lt;br&gt;
When Service A calls Service B, and Service B calls Service C - if Service C is down, all three services are affected.&lt;/p&gt;

&lt;p&gt;To mitigate this, you should implement &lt;a href="https://antondevtips.com/blog/how-to-implement-retries-and-resilience-patterns-with-polly-and-microsoft-resilience" rel="noopener noreferrer"&gt;resilience patterns&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries with backoff&lt;/strong&gt; - retry failed requests with increasing delays between attempts:&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;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PropertyServiceClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;client&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&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;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://property-service: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="nf"&gt;AddStandardResilienceHandler&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;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRetryAttempts&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="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BackoffType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DelayBackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&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;Retry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;500&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;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SamplingDuration&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;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FailureRatio&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.9&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;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MinimumThroughput&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AttemptTimeout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&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;FromSeconds&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://antondevtips.com/blog/how-to-implement-retries-and-resilience-patterns-with-polly-and-microsoft-resilience#standard-resilience-pipeline" rel="noopener noreferrer"&gt;AddStandardResilienceHandler&lt;/a&gt; from &lt;code&gt;Microsoft.Extensions.Http.Resilience&lt;/code&gt; gives you retries, circuit breaker, and timeouts in a single configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circuit Breaker&lt;/strong&gt; - stops calling a failing service to give it time to recover.&lt;/p&gt;

&lt;p&gt;When a service fails repeatedly, the circuit "opens," and all subsequent requests fail immediately instead of waiting for a timeout.&lt;br&gt;
After a cooldown period, the circuit "half-opens" to test if the service has recovered.&lt;/p&gt;

&lt;p&gt;These patterns don't eliminate the problem of synchronous coupling.&lt;br&gt;
They reduce the impact.&lt;br&gt;
If you need true independence between services, you need asynchronous communication.&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/synchronous-vs-asynchronous-communication-in-microservices" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/synchronous-vs-asynchronous-communication-in-microservices&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>microservices</category>
    </item>
    <item>
      <title>.NET Aspire Integration Testing Best Practices for Distributed Applications</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Thu, 17 Sep 2026 17:55:13 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/net-aspire-integration-testing-best-practices-for-distributed-applications-imp</link>
      <guid>https://dev.to/antonmartyniuk/net-aspire-integration-testing-best-practices-for-distributed-applications-imp</guid>
      <description>&lt;p&gt;A while ago, I published an article about ASP.NET Core Integration Testing Best Practices.&lt;br&gt;
WebApplicationFactory, TestContainers, and Respawn simplifies integration testing in .NET Core applications.&lt;/p&gt;

&lt;p&gt;But .NET Aspire simplifies the process further for distributed applications.&lt;/p&gt;

&lt;p&gt;.NET Aspire replaces both WebApplicationFactory and TestContainers with a single tool: DistributedApplicationTestingBuilder.&lt;br&gt;
You no longer need to set up Docker containers manually or override environment variables.&lt;br&gt;
Aspire handles service discovery, container orchestration, and connection strings for you.&lt;/p&gt;

&lt;p&gt;In this post, I will share the best practices I discovered while writing integration tests for a distributed Aspire application with multiple APIs, PostgreSQL, and Redis.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;The System We Will Be Testing&lt;/li&gt;
&lt;li&gt;Best Practice 1: Use DistributedApplicationTestingBuilder Instead of WebApplicationFactory&lt;/li&gt;
&lt;li&gt;Best Practice 2: Share the App Instance Across Tests with ICollectionFixture&lt;/li&gt;
&lt;li&gt;Best Practice 3: Wait for Resources to Be Healthy&lt;/li&gt;
&lt;li&gt;Best Practice 4: Wait for Services to Start before Running Tests&lt;/li&gt;
&lt;li&gt;Best Practice 5: Cleanup Database Between Tests&lt;/li&gt;
&lt;li&gt;Best Practice 6: Test your API Contracts&lt;/li&gt;
&lt;li&gt;Best Practice 7: Test Error Responses and Cross-Service Communication&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/dotnet-aspire-integration-testing-best-practices-for-distributed-applications" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/dotnet-aspire-integration-testing-best-practices-for-distributed-applications&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The System We Will Be Testing
&lt;/h2&gt;

&lt;p&gt;I have built a distributed system with two APIs orchestrated by .NET Aspire:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Products API: manages products, supports CRUD operations and purchasing. Uses PostgreSQL and Redis for caching. Calls the Stocks API to check and update stock levels during purchases.&lt;/li&gt;
&lt;li&gt;Stocks API: manages stock inventory for products. Uses PostgreSQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both APIs share the same PostgreSQL server but use separate database schemas.&lt;/p&gt;

&lt;p&gt;Here is the Aspire AppHost that defines the architecture:&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;DistributedApplication&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;postgres&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;AddPostgres&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDataVolume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isReadOnly&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRedis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cache"&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;stocksApi&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;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stocks_Api&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"stocks-api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithExternalHttpEndpoints&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;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products_Api&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"products-api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stocksApi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stocksApi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithExternalHttpEndpoints&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="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 Products API depends on the Stocks API for inter-service communication.&lt;br&gt;
When a user purchases a product, the Products API calls the Stocks API via HTTP to verify stock availability and update the count.&lt;/p&gt;

&lt;p&gt;Both APIs use EF Core with PostgreSQL and run database migrations on startup.&lt;/p&gt;

&lt;p&gt;Now let's explore how to write integration tests for this system.&lt;/p&gt;
&lt;h2&gt;
  
  
  Best Practice 1: Use DistributedApplicationTestingBuilder Instead of WebApplicationFactory
&lt;/h2&gt;

&lt;p&gt;In my previous article, I used WebApplicationFactory together with TestContainers to spin up Docker containers for PostgreSQL and RabbitMQ.&lt;/p&gt;

&lt;p&gt;With .NET Aspire, you don't need either of these.&lt;br&gt;
Aspire provides DistributedApplicationTestingBuilder, which replaces both tools with a single, unified approach.&lt;/p&gt;

&lt;p&gt;With the traditional approach, your test project references the API project directly:&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;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\Products.Api\Products.Api.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Aspire, your test project references the Aspire AppHost project instead:&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;ProjectReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"..\AspireNetConf.AppHost\AspireNetConf.AppHost.csproj"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you test the entire distributed application, not just a single API in isolation.&lt;/p&gt;

&lt;p&gt;To create a test project, you can use the ready Aspire Test Project template:&lt;/p&gt;

&lt;p&gt;You can install the Aspire project templates by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new &lt;span class="nb"&gt;install &lt;/span&gt;Aspire.ProjectTemplates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will also need to install the Aspire CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; aspire.cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The testing project installs the Aspire.Hosting.Testing NuGet package.&lt;/p&gt;

&lt;p&gt;This one package replaces multiple packages from the traditional approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft.AspNetCore.Mvc.Testing (WebApplicationFactory)&lt;/li&gt;
&lt;li&gt;Testcontainers.PostgreSql (TestContainers for PostgreSQL)&lt;/li&gt;
&lt;li&gt;Testcontainers.RabbitMq (or other container packages)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To create the distributed application in your tests, use DistributedApplicationTestingBuilder:&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;appHost&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;DistributedApplicationTestingBuilder&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AspireTests_AppHost&lt;/span&gt;&lt;span class="p"&gt;&amp;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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;appHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BuildAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts the entire Aspire application, including all containers and services.&lt;/p&gt;

&lt;p&gt;To create HTTP clients for your APIs, use CreateHttpClient with the resource name from your AppHost:&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;productsClient&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;CreateHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"products-api"&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;stocksClient&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;CreateHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stocks-api"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No need to configure base URLs or connection strings.&lt;br&gt;
Aspire handles service discovery automatically.&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/dotnet-aspire-integration-testing-best-practices-for-distributed-applications" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/dotnet-aspire-integration-testing-best-practices-for-distributed-applications&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspire</category>
    </item>
    <item>
      <title>Optimistic vs Pessimistic Concurrency in .NET</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Tue, 15 Sep 2026 11:13:46 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/optimistic-vs-pessimistic-concurrency-in-net-4jf3</link>
      <guid>https://dev.to/antonmartyniuk/optimistic-vs-pessimistic-concurrency-in-net-4jf3</guid>
      <description>&lt;p&gt;Almost everyone had this situation in production: a customer changed the delivery address on a shipment, saved it, saw the confirmation, and an hour later the old address is back.&lt;/p&gt;

&lt;p&gt;You start investigating, and you don't see any errors or exceptions in the logs.&lt;br&gt;
The request returned 200 OK, but the data is just wrong.&lt;/p&gt;

&lt;p&gt;This is a lost update, and it's one of the most common bugs in production .NET systems.&lt;br&gt;
It happens whenever two requests read the same row, change it in memory, and write it back.&lt;/p&gt;

&lt;p&gt;I've debugged this exact bug in payment systems, warehouse software, and booking flows.&lt;br&gt;
It never shows up in a unit or integration test, because the tests don't run two requests in the same millisecond (your integration test can actually test concurrent requests).&lt;/p&gt;

&lt;p&gt;There are two classic ways to solve it: optimistic concurrency and pessimistic concurrency.&lt;br&gt;
They solve the same problem with different assumptions, and picking the wrong one either kills your throughput or leaves the bug in place.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;The Lost Update Problem&lt;/li&gt;
&lt;li&gt;Optimistic Concurrency: Detect the Conflict&lt;/li&gt;
&lt;li&gt;Handling DbUpdateConcurrencyException&lt;/li&gt;
&lt;li&gt;Retrying Conflicts with a Resilience Pipeline&lt;/li&gt;
&lt;li&gt;Pessimistic Concurrency: Lock the Row First&lt;/li&gt;
&lt;li&gt;A Third Option: One Atomic Statement&lt;/li&gt;
&lt;li&gt;Optimistic vs Pessimistic: Deep Comparison&lt;/li&gt;
&lt;li&gt;How to Choose: A Decision Checklist&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/optimistic-vs-pessimistic-concurrency-in-dot-net" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/optimistic-vs-pessimistic-concurrency-in-dot-net&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Lost Update Problem
&lt;/h2&gt;

&lt;p&gt;Here is the domain we'll use throughout the post: a shipment that a warehouse operator can edit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Shipment&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Carrier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ShipmentStatus&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ShipmentItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the update handler almost everyone writes first:&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;Result&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;UpdateShipmentRequest&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;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;shipment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shipments&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;request&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;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;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;Result&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;$"Shipment '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&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="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&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="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;request&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&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 code reads a row, changes it in memory, and writes it back.&lt;/p&gt;

&lt;p&gt;Between the read and the write there is a gap. It's small, usually a few milliseconds, but it's real. Anything that happens inside that gap is invisible to this handler.&lt;/p&gt;

&lt;p&gt;Now put two operators in that gap at the same time. One changes the address, the other changes the carrier:&lt;/p&gt;

&lt;p&gt;Both requests got a success response, and both updated exactly one row. Neither of them did anything wrong on its own.&lt;/p&gt;

&lt;p&gt;But the row ends up with the address "Amsterdam" and the carrier "UPS". Request A's address change is gone, and no one was notified.&lt;/p&gt;

&lt;p&gt;This is what a &lt;strong&gt;lost update&lt;/strong&gt; means: one writer silently overwrites another writer's change because it never saw it.&lt;/p&gt;

&lt;p&gt;The first instinct is to wrap the handler in a transaction, and that doesn't fix it.&lt;/p&gt;

&lt;p&gt;At the Read Committed isolation level, which is the default in PostgreSQL and SQL Server, both transactions read a valid committed row, and both writes succeed.&lt;br&gt;
The database is doing exactly what you asked. You just never told it that the second write depended on the first read.&lt;/p&gt;

&lt;p&gt;Serializable isolation does catch this, at the cost of serialization failures that you have to retry anyway.&lt;br&gt;
If you want the full picture of what each level protects against, I covered it in &lt;a href="https://antondevtips.com/blog/complete-guide-to-transaction-isolation-levels-in-sql" rel="noopener noreferrer"&gt;Complete Guide to Transaction Isolation Levels in SQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The two techniques below fix the problem directly, and you can apply either one per use case.&lt;/p&gt;
&lt;h2&gt;
  
  
  Optimistic Concurrency: Detect the Conflict
&lt;/h2&gt;

&lt;p&gt;Optimistic concurrency starts with the assumption that conflicts are rare.&lt;/p&gt;

&lt;p&gt;So it doesn't lock anything. It lets both requests run at full speed and, at the moment of writing, makes the database check whether anyone changed the row in the meantime.&lt;/p&gt;

&lt;p&gt;The mechanism is a &lt;strong&gt;concurrency token&lt;/strong&gt;: a column whose value changes on every update. You read it together with the row, and your &lt;code&gt;UPDATE&lt;/code&gt; statement carries it in the &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;If the token in the database no longer matches the one you read, your &lt;code&gt;UPDATE&lt;/code&gt; matches zero rows, and you know somebody got there first.&lt;/p&gt;

&lt;p&gt;EF Core supports this out of the box, and you have three ways to configure it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL, using the built-in &lt;code&gt;xmin&lt;/code&gt; system column:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This is the cheapest option on PostgreSQL because &lt;code&gt;xmin&lt;/code&gt; already exists on every row. You get concurrency checks without adding a column or writing a migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL Server, using a &lt;code&gt;rowversion&lt;/code&gt; column:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;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;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsRowVersion&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQL Server maintains the value itself on every update, so you never assign it in code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Any provider, using your own token:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Shipment&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&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;Version&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;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;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&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;Version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsConcurrencyToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A manual token is the one I reach for most often, and not because of provider portability. It's a plain &lt;code&gt;Guid&lt;/code&gt; column, so it survives a round trip to a browser or a mobile client, which is exactly what a stateless web API needs.&lt;/p&gt;

&lt;p&gt;You do have to change the value yourself. The cleanest place is a &lt;code&gt;SaveChangesAsync&lt;/code&gt; override on the DbContext:&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;override&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;ct&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChangeTracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Added&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Modified&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;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&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;Entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&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;ct&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;Whichever option you pick, EF Core now generates a different &lt;code&gt;UPDATE&lt;/code&gt;:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;shipments&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;carrier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p3&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;version = @p4&lt;/code&gt; predicate is the whole trick. EF Core sends the value it read, counts how many rows the statement actually changed, and throws a &lt;code&gt;DbUpdateConcurrencyException&lt;/code&gt; when the answer is zero.&lt;/p&gt;

&lt;p&gt;Here is the same race as before, with the token in place:&lt;/p&gt;

&lt;p&gt;Request A still wins the race, exactly as it did before. The difference is that Request B now finds out rather than quietly replacing the value.&lt;/p&gt;

&lt;p&gt;There's one part that's easy to miss in a web API. Your two requests don't share a DbContext, and they don't even overlap in time. The operator opens an edit form, thinks for two minutes, and submits.&lt;/p&gt;

&lt;p&gt;For the check to mean anything, the version has to travel to the client and come back:&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;ShipmentResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Carrier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;UpdateShipmentRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Carrier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you tell EF Core to use the client's version instead of the one you just read from the database:&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;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&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;Version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OriginalValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&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;Address&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;Address&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;request&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;OriginalValue&lt;/code&gt; is what puts the client's version into the &lt;code&gt;WHERE&lt;/code&gt; clause. Without this line, you're comparing the row against a value you read milliseconds ago, and the two minutes when the operator was typing go completely unchecked.&lt;/p&gt;

&lt;p&gt;Now the write fails when it should. The next question is what to do about it.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/optimistic-vs-pessimistic-concurrency-in-dot-net" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/optimistic-vs-pessimistic-concurrency-in-dot-net&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>Building Event-Driven Microservices with Azure Service Bus in .NET</title>
      <dc:creator>Anton Martyniuk</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:18:44 +0000</pubDate>
      <link>https://dev.to/antonmartyniuk/building-event-driven-microservices-with-azure-service-bus-in-net-4c2</link>
      <guid>https://dev.to/antonmartyniuk/building-event-driven-microservices-with-azure-service-bus-in-net-4c2</guid>
      <description>&lt;p&gt;In Microservices Architecture, services need to communicate with each other.&lt;br&gt;
The simplest approach is synchronous HTTP calls, but it creates tight coupling between services.&lt;br&gt;
If one service is slow or unavailable, it affects all dependent services.&lt;/p&gt;

&lt;p&gt;Message brokers solve this problem by enabling asynchronous, message-based communication between your services.&lt;br&gt;
Services publish events to the broker, and other services consume them independently.&lt;/p&gt;

&lt;p&gt;When you are working in Azure, you can use Azure Service Bus to implement a message broker.&lt;br&gt;
It supports both point-to-point and publish-subscribe messaging patterns.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Azure Service Bus: Core Concepts&lt;/li&gt;
&lt;li&gt;How To Create Azure Service Bus in Azure Portal&lt;/li&gt;
&lt;li&gt;Setting Up Azure Service Bus with .NET Aspire&lt;/li&gt;
&lt;li&gt;How to Publish and Consume Messages with Azure Service Bus&lt;/li&gt;
&lt;li&gt;The Event-Driven Pipeline: Payments, Fraud Detection, and Notifications&lt;/li&gt;
&lt;li&gt;Running the Application Locally with Azure Service Bus Emulator&lt;/li&gt;
&lt;li&gt;Deploying Microservices to Azure with Aspire&lt;/li&gt;
&lt;li&gt;Azure Service Bus vs Other Azure Messaging Services&lt;/li&gt;
&lt;/ul&gt;

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




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/building-event-driven-microservices-with-azure-service-bus-in-dotnet" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/building-event-driven-microservices-with-azure-service-bus-in-dotnet&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure Service Bus: Core Concepts
&lt;/h2&gt;

&lt;p&gt;Azure Service Bus is a fully managed enterprise message broker in the Azure Cloud.&lt;br&gt;
It supports two messaging patterns: &lt;strong&gt;Queues&lt;/strong&gt; and &lt;strong&gt;Topics with Subscriptions&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queues
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Queues&lt;/strong&gt; implement point-to-point communication.&lt;br&gt;
One sender sends a message, and one receiver processes it.&lt;br&gt;
Each message is consumed by exactly one consumer.&lt;/p&gt;

&lt;p&gt;Use queues when a message should be processed by a single service.&lt;br&gt;
For example, sending a stock update command to the Stocks service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Topics with Subscriptions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Topics with Subscriptions&lt;/strong&gt; implement publish-subscribe communication.&lt;br&gt;
One sender publishes a message to a topic, and multiple subscribers can each receive a copy of that message.&lt;br&gt;
Each subscription acts as its own virtual queue.&lt;/p&gt;

&lt;p&gt;Use topics when multiple services need to react to the same event.&lt;br&gt;
For example, when a payment is processed, both the Fraud Detection service and the Notifications service may need to be notified.&lt;/p&gt;

&lt;p&gt;Our solution has five microservices that communicate through Azure Service Bus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product-Service&lt;/strong&gt; - manages products and handles purchases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stock-Service&lt;/strong&gt; - manages product stock levels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment-Service&lt;/strong&gt; - processes payments and tracks their status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FraudDetection-Service&lt;/strong&gt; - analyzes payments for fraud risk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification-Service&lt;/strong&gt; - sends email notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the message flow:&lt;/p&gt;

&lt;p&gt;When a user purchases a product:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Product-Service&lt;/strong&gt; publishes &lt;code&gt;UpdateStockEvent&lt;/code&gt; to the &lt;code&gt;update-stock&lt;/code&gt; queue and &lt;code&gt;PurchaseCompletedEvent&lt;/code&gt; to the &lt;code&gt;payment-created&lt;/code&gt; queue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stock-Service&lt;/strong&gt; consumes from &lt;code&gt;update-stock&lt;/code&gt; and updates the stock count&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment-Service&lt;/strong&gt; consumes from &lt;code&gt;payment-created&lt;/code&gt;, creates a payment record, and publishes &lt;code&gt;PaymentRegisteredEvent&lt;/code&gt; to the &lt;code&gt;payment-registered&lt;/code&gt; topic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FraudDetection-Service&lt;/strong&gt; subscribes to the &lt;code&gt;payment-registered&lt;/code&gt; topic, analyzes the payment, and publishes &lt;code&gt;FraudDecisionEvent&lt;/code&gt; to the &lt;code&gt;fraud-decision&lt;/code&gt; queue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment-Service&lt;/strong&gt; consumes from &lt;code&gt;fraud-decision&lt;/code&gt;, updates the payment status, and publishes &lt;code&gt;PaymentProcessedEvent&lt;/code&gt; to the &lt;code&gt;payment-processed&lt;/code&gt; topic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification-Service&lt;/strong&gt; subscribes to the &lt;code&gt;payment-processed&lt;/code&gt; topic and sends a notification&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In our project, we use both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queues&lt;/strong&gt; for &lt;code&gt;update-stock&lt;/code&gt;, &lt;code&gt;payment-created&lt;/code&gt;, and &lt;code&gt;fraud-decision&lt;/code&gt; (one sender, one receiver)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topics&lt;/strong&gt; for &lt;code&gt;payment-registered&lt;/code&gt; and &lt;code&gt;payment-processed&lt;/code&gt; (one sender, multiple receivers)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Messages Flow Through Service Bus
&lt;/h3&gt;

&lt;p&gt;When a consumer receives a message, Service Bus does not delete it immediately.&lt;br&gt;
It &lt;strong&gt;locks&lt;/strong&gt; the message and hides it from other consumers for a configurable lock duration.&lt;/p&gt;

&lt;p&gt;The consumer has that time to process the message and complete the call.&lt;br&gt;
If it completes successfully, Service Bus deletes the message.&lt;br&gt;
If the handler throws or the lock expires, Service Bus releases the message and makes it available for retry.&lt;/p&gt;

&lt;p&gt;Service Bus tracks how many times a message has been delivered using the &lt;strong&gt;delivery count&lt;/strong&gt;.&lt;br&gt;
When the delivery count exceeds &lt;code&gt;MaxDeliveryCount&lt;/code&gt; (default: 10), Service Bus automatically moves the message to the dead-letter queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At-least-once delivery&lt;/strong&gt; is the core guarantee Service Bus provides.&lt;br&gt;
A message will be delivered at least once, but in rare cases it may be delivered more than once — for example, if a network failure prevents the completion acknowledgement from reaching the Service Bus.&lt;br&gt;
Your consumers must be &lt;strong&gt;idempotent&lt;/strong&gt;: processing the same message twice must produce the same result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dead-Letter Queue
&lt;/h3&gt;

&lt;p&gt;Every queue and every topic subscription has a built-in dead-letter sub-queue (DLQ).&lt;br&gt;
Messages end up in the DLQ when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The delivery count exceeds &lt;code&gt;MaxDeliveryCount&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Your code explicitly calls &lt;code&gt;DeadLetterMessageAsync&lt;/code&gt; — for example, when a message is malformed, and retrying is pointless&lt;/li&gt;
&lt;li&gt;The message TTL (time-to-live) expires before it is consumed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The DLQ is permanent — messages do not expire there.&lt;br&gt;
You can inspect, replay, or archive them using the Azure Portal.&lt;/p&gt;

&lt;p&gt;In production, monitor the DLQ message count and set up alerts.&lt;br&gt;
A growing DLQ means something in your pipeline is consistently failing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate Detection
&lt;/h3&gt;

&lt;p&gt;Service Bus can automatically discard duplicate messages within a configurable time window.&lt;br&gt;
Enable it on a queue or topic at creation time.&lt;/p&gt;

&lt;p&gt;Service Bus tracks messages by their &lt;code&gt;MessageId&lt;/code&gt;.&lt;br&gt;
If a message with the same &lt;code&gt;MessageId&lt;/code&gt; arrives within the duplicate detection window (default: 10 minutes), Service Bus silently drops it.&lt;/p&gt;

&lt;p&gt;Use duplicate detection when your producer may retry sending — for example, after a timeout or a transient network failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Message Sessions
&lt;/h3&gt;

&lt;p&gt;Sessions enable &lt;strong&gt;ordered, grouped processing&lt;/strong&gt; of related messages.&lt;br&gt;
Set a &lt;code&gt;SessionId&lt;/code&gt; on each message to group them together.&lt;/p&gt;

&lt;p&gt;Service Bus guarantees that all messages with the same &lt;code&gt;SessionId&lt;/code&gt; are processed &lt;strong&gt;in order&lt;/strong&gt; by a single consumer at a time.&lt;br&gt;
No two consumers can process messages from the same session simultaneously.&lt;/p&gt;

&lt;p&gt;Use sessions when order matters — for example, processing all state transitions for the same payment in sequence.&lt;br&gt;
Sessions require &lt;code&gt;RequiresSession = true&lt;/code&gt; on the queue or subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  How To Create Azure Service Bus in Azure Portal
&lt;/h2&gt;

&lt;p&gt;First, create a new Azure Service Bus namespace in the Azure Portal.&lt;/p&gt;

&lt;p&gt;Go to the Azure Portal and search for "Service Bus":&lt;/p&gt;

&lt;p&gt;Create a new Service Bus namespace with the following settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource group:&lt;/strong&gt; Choose your resource group or create a new one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespace name:&lt;/strong&gt; Choose a globally unique name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; Select the region closest to your services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing tier:&lt;/strong&gt; Select &lt;strong&gt;Standard&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pricing tier is important.&lt;br&gt;
The &lt;strong&gt;Basic&lt;/strong&gt; tier only supports queues.&lt;br&gt;
If you need &lt;strong&gt;topics and subscriptions&lt;/strong&gt; (which we do), you must use at least the &lt;strong&gt;Standard&lt;/strong&gt; tier.&lt;/p&gt;

&lt;p&gt;For most development and production workloads, &lt;strong&gt;Standard&lt;/strong&gt; is sufficient.&lt;br&gt;
Choose &lt;strong&gt;Premium&lt;/strong&gt; only if you need dedicated resources, virtual network integration, or messages larger than 256 KB.&lt;/p&gt;




&lt;p&gt;👉 &lt;strong&gt;Read original article on my newsletter:&lt;/strong&gt; &lt;a href="https://antondevtips.com/blog/building-event-driven-microservices-with-azure-service-bus-in-dotnet" rel="noopener noreferrer"&gt;https://antondevtips.com/blog/building-event-driven-microservices-with-azure-service-bus-in-dotnet&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>azure</category>
    </item>
    <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>
  </channel>
</rss>
