<?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: Alex</title>
    <description>The latest articles on DEV Community by Alex (@alexleeeeeeeeee).</description>
    <link>https://dev.to/alexleeeeeeeeee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2749953%2F527402db-322a-4aec-aa3d-6463ccfaab88.png</url>
      <title>DEV Community: Alex</title>
      <link>https://dev.to/alexleeeeeeeeee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexleeeeeeeeee"/>
    <language>en</language>
    <item>
      <title>.NET Learning Notes:Deploying a Microservices Application to VPS with Docker, Nginx, and CD</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 19 Mar 2026 04:09:22 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notesdeploying-a-microservices-application-to-vps-with-docker-nginx-and-cd-3i0n</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notesdeploying-a-microservices-application-to-vps-with-docker-nginx-and-cd-3i0n</guid>
      <description>&lt;h2&gt;
  
  
  1.Environment Setup(VPS Preparation)
&lt;/h2&gt;

&lt;p&gt;Before deploying the application, I prepared a clean Ubuntu-based VPS environment.&lt;br&gt;
&lt;strong&gt;Connect to VPS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;ssh&lt;/span&gt; root@your_vps_ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;System Update&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the upgrade process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kept the existing SSH configuration&lt;/li&gt;
&lt;li&gt;Restarted services using default options
This ensures system packages are up to date without breaking remote access.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Install Docker&lt;/strong&gt;&lt;br&gt;
To containerize the microservices, Docker and Docker Compose were installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; ca-certificates curl gnupg

&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 &lt;span class="nt"&gt;-d&lt;/span&gt; /etc/apt/keyrings

curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://download.docker.com/linux/ubuntu/gpg | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /etc/apt/keyrings/docker.gpg

&lt;span class="nb"&gt;chmod &lt;/span&gt;a+r /etc/apt/keyrings/docker.gpg

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"deb [arch=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
  &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /etc/os-release &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$VERSION_CODENAME&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; stable"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/docker.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null

apt update

apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify Installation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run hello-world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms Docker is correctly installed and functioning.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;2. Application Deployment &amp;amp; Infrastructure Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;After preparing the VPS environment, I deployed the application using Docker and configured domain routing, reverse proxy, and HTTPS.&lt;br&gt;
&lt;strong&gt;Deploy Application with Docker Compose&lt;/strong&gt;&lt;br&gt;
The project is structured as a microservices system and deployed via Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &amp;lt;your-repo&amp;gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &amp;lt;repo&amp;gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts all services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handling Database Initialization Issue&lt;/strong&gt;&lt;br&gt;
During the first deployment, a common issue occurred:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The application attempted to run database migrations before MySQL was fully ready.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This caused startup failures. To address this, I enabled retry logic in the database configuration, allowing the service to wait and retry until the database becomes available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Proxy &amp;amp; SSL Configuration&lt;/strong&gt;&lt;br&gt;
A custom domain was purchased and configured via Cloudflare.&lt;br&gt;
DNS records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chenlis.com → frontend
admin.chenlis.com → admin panel
api.chenlis.com → API gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudflare proxy (orange cloud) was enabled to introduce an additional layer between users and the origin server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → Cloudflare → VPS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides several important benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CDN acceleration&lt;/strong&gt;: Static assets can be cached at Cloudflare edge nodes, reducing latency and improving load times for users in different regions. It also reduces direct traffic to the VPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic DDoS protection&lt;/strong&gt;: Incoming traffic is filtered by Cloudflare before reaching the server. This helps mitigate simple flooding or abnormal request patterns that could otherwise overwhelm a small VPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hiding the origin server IP&lt;/strong&gt;:The public IP of the VPS is no longer exposed. Requests are routed through Cloudflare, making it harder for attackers to directly target the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS optimization&lt;/strong&gt;: Cloudflare handles TLS negotiation at the edge, improving connection performance and reducing cryptographic overhead on the origin server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SSL Mode: Full (Strict)&lt;/strong&gt;&lt;br&gt;
The SSL/TLS mode was configured to Full (Strict).&lt;br&gt;
This ensures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → HTTPS → Cloudflare → HTTPS → Origin Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with certificate validation on both sides.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In Full mode, Cloudflare encrypts traffic to the server but does not validate the certificate.&lt;/li&gt;
&lt;li&gt;In Full (Strict) mode, Cloudflare requires a valid certificate (e.g., Let’s Encrypt), ensuring both encryption and trust.
Since a valid certificate was already issued via Certbot, switching to Full (Strict) guarantees end-to-end secure communication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reverse Proxy with Nginx&lt;/strong&gt;&lt;br&gt;
Nginx was installed and configured as a reverse proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;chenlis.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&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;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;admin.chenlis.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&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;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.chenlis.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:5193&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&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;Enable the configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/fintrack /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /etc/nginx/sites-enabled/default
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Enable HTTPS with Certbot&lt;/strong&gt;&lt;br&gt;
To secure all endpoints, HTTPS was configured using Let’s Encrypt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot python3-certbot-nginx &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; chenlis.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; admin.chenlis.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; api.chenlis.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Issues SSL certificates&lt;/li&gt;
&lt;li&gt;Configures Nginx&lt;/li&gt;
&lt;li&gt;Enables auto-renewal&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;3. CI/CD Pipeline with GitHub Actions&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To automate deployment, I implemented a simple CI/CD pipeline using GitHub Actions.&lt;br&gt;
This allows the application to be deployed automatically to the VPS whenever changes are merged into the main branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment Strategy&lt;/strong&gt;&lt;br&gt;
The workflow follows a straightforward approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code is developed in feature branches&lt;/li&gt;
&lt;li&gt;Changes are merged into main via pull requests&lt;/li&gt;
&lt;li&gt;Deployment is triggered only on push to main
This avoids deploying unverified code and keeps the production environment stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions Workflow&lt;/strong&gt;&lt;br&gt;
The deployment is handled via SSH using a GitHub Actions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to VPS&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy via SSH&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1.0.3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_SSH_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.VPS_PORT }}&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;set -e&lt;/span&gt;
            &lt;span class="s"&gt;cd ~/FinTrack-Microservices&lt;/span&gt;
            &lt;span class="s"&gt;git pull&lt;/span&gt;
            &lt;span class="s"&gt;docker compose up -d --build&lt;/span&gt;
            &lt;span class="s"&gt;docker image prune -f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why SSH-Based Deployment&lt;/strong&gt;&lt;br&gt;
Instead of using a full CI/CD platform, I chose a lightweight SSH-based approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple to set up&lt;/li&gt;
&lt;li&gt;No additional infrastructure required&lt;/li&gt;
&lt;li&gt;Full control over deployment commands
This is sufficient for small-to-medium scale systems and personal projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deployment Flow&lt;/strong&gt;&lt;br&gt;
 Once configured, the deployment flow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; git push → GitHub Actions → SSH to VPS → git pull → docker rebuild → services updated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This eliminates the need for manual deployment and ensures consistency between local and production environments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>dotnet</category>
      <category>microservices</category>
    </item>
    <item>
      <title>.NET Learning Notes: Configure docker for project</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Tue, 10 Mar 2026 02:45:19 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-configure-docker-for-project-cok</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-configure-docker-for-project-cok</guid>
      <description>&lt;h2&gt;
  
  
  Project Background
&lt;/h2&gt;

&lt;p&gt;The project discussed in this article is available here: &lt;a href="https://github.com/liananddandan/FinTrack-Microservices" rel="noopener noreferrer"&gt;FinTrack&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FinTrack is a microservices-based backend system. The services are separated into multiple independent components and communicate through APIs and message queues.&lt;/p&gt;

&lt;p&gt;During early development, everything was started manually. Each service needed to run in its own terminal, and several external services were also required, such as databases and message brokers.&lt;/p&gt;

&lt;p&gt;This quickly became inconvenient.&lt;/p&gt;

&lt;p&gt;For example, starting the system locally often required opening multiple terminals and running different commands for each service. In addition, the project depended on several infrastructure components. Managing and starting all of them manually was tedious and error-prone.&lt;/p&gt;

&lt;p&gt;Another problem appeared when demonstrating the project. Launching each service one by one was not an ideal workflow.&lt;/p&gt;

&lt;p&gt;This is where Docker becomes useful.&lt;/p&gt;

&lt;p&gt;By containerizing the services and their dependencies, the entire system can be started in a consistent way. With tools such as Docker Compose, multiple services can be launched together with a single command.&lt;/p&gt;

&lt;p&gt;For a microservices project, this greatly simplifies both local development and project demonstration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Docker Compose Commands
&lt;/h2&gt;

&lt;p&gt;In daily development, only a few Docker Compose commands are used frequently. Most of the time, managing containers revolves around starting, stopping, and rebuilding services.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;docker compose up&lt;/strong&gt;
The most common command is:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts the services defined in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;br&gt;
Two options are commonly used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-d&lt;/code&gt; runs the containers in detached mode (in the background).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--build&lt;/code&gt; forces Docker to rebuild images before starting the containers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is useful when the source code or Dockerfile has changed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;docker compose down&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;docker compose down&lt;/p&gt;

&lt;p&gt;This stops and removes the containers, networks, and default resources created by the Compose project.&lt;/p&gt;

&lt;p&gt;Sometimes volumes are also removed:&lt;/p&gt;

&lt;p&gt;docker compose down -v&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-v&lt;/code&gt; option removes the volumes associated with the containers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;docker compose start&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;docker compose start&lt;/p&gt;

&lt;p&gt;This command starts containers that already exist but are currently stopped.&lt;/p&gt;

&lt;p&gt;It does not rebuild images or recreate containers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;docker compose stop&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;docker compose stop&lt;/p&gt;

&lt;p&gt;This stops the running containers but does not remove them. The containers can later be restarted using &lt;code&gt;docker compose start&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;docker compose -f&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes multiple compose files are used. In that case, the &lt;code&gt;-f&lt;/code&gt; option allows specifying the compose file manually.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;docker compose -f docker-compose.dev.yml up -d&lt;/p&gt;

&lt;p&gt;This tells Docker Compose to use a specific configuration file instead of the default &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfile Example
&lt;/h2&gt;

&lt;p&gt;Below is the Dockerfile used to build the IdentityService.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ---------- build stage ----------&lt;/span&gt;
&lt;span class="c"&gt;# Use the .NET SDK image as the build environment&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;mcr.microsoft.com/dotnet/sdk:9.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;

&lt;span class="c"&gt;# Set the working directory inside the container to /src&lt;/span&gt;
&lt;span class="c"&gt;# After this line, all following commands (COPY, RUN, etc.)&lt;/span&gt;
&lt;span class="c"&gt;# will use /src as their base directory&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;

&lt;span class="c"&gt;# Copy the solution file into the container build environment&lt;/span&gt;
&lt;span class="c"&gt;# The destination "./" means the current WORKDIR, which is /src&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; FinTrack.sln ./&lt;/span&gt;

&lt;span class="c"&gt;# Copy project files (csproj) into the container&lt;/span&gt;
&lt;span class="c"&gt;# These paths are relative to the current WORKDIR (/src)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src/Services/IdentityService/IdentityService.csproj src/Services/IdentityService/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src/Shared/SharedKernel/SharedKernel.csproj src/Shared/SharedKernel/&lt;/span&gt;

&lt;span class="c"&gt;# Restore NuGet dependencies for the project&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet restore src/Services/IdentityService/IdentityService.csproj

&lt;span class="c"&gt;# Copy the full source code into the container&lt;/span&gt;
&lt;span class="c"&gt;# "." means the build context root on the host&lt;/span&gt;
&lt;span class="c"&gt;# "." as the destination means the current WORKDIR (/src)&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Publish the application&lt;/span&gt;
&lt;span class="c"&gt;# The compiled output will be placed in /app/publish inside the container&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dotnet publish src/Services/IdentityService/IdentityService.csproj &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;-o&lt;/span&gt; /app/publish &lt;span class="se"&gt;\
&lt;/span&gt;    /p:UseAppHost&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;


&lt;span class="c"&gt;# ---------- runtime stage ----------&lt;/span&gt;
&lt;span class="c"&gt;# Use a lighter ASP.NET runtime image for running the application&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/dotnet/aspnet:9.0&lt;/span&gt;

&lt;span class="c"&gt;# Set the runtime working directory inside the container&lt;/span&gt;
&lt;span class="c"&gt;# The application will run from /app&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy the published output from the build stage&lt;/span&gt;
&lt;span class="c"&gt;# "/app/publish" refers to the path inside the build stage container&lt;/span&gt;
&lt;span class="c"&gt;# "." means the current WORKDIR (/app) in this runtime stage&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/publish .&lt;/span&gt;

&lt;span class="c"&gt;# Configure ASP.NET Core to listen on port 8080 inside the container&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; ASPNETCORE_URLS=http://+:8080&lt;/span&gt;

&lt;span class="c"&gt;# Document that the container exposes port 8080&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;

&lt;span class="c"&gt;# When the container starts, run the service&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["dotnet", "IdentityService.dll"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Docker Compose File example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.9'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# ========================&lt;/span&gt;
  &lt;span class="c1"&gt;# Infrastructure services&lt;/span&gt;
  &lt;span class="c1"&gt;# These are shared components used by backend services&lt;/span&gt;
  &lt;span class="c1"&gt;# ========================&lt;/span&gt;

  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fintrack-mysql&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;123456&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fintrack_identity&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3307:3306"&lt;/span&gt; &lt;span class="c1"&gt;# host:container&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql_data:/var/lib/mysql&lt;/span&gt; &lt;span class="c1"&gt;# persist database data outside container&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# wait until MySQL is ready before dependent services start&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysqladmin"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-h"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-p123456"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;


  &lt;span class="c1"&gt;# ========================&lt;/span&gt;
  &lt;span class="c1"&gt;# Backend microservices&lt;/span&gt;
  &lt;span class="c1"&gt;# Each service is built from its own Dockerfile&lt;/span&gt;
  &lt;span class="c1"&gt;# ========================&lt;/span&gt;

  &lt;span class="na"&gt;notification-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/Services/NotificationService/Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fintrack-notification&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;rabbitmq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;mailhog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Development&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_URLS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://+:8080&lt;/span&gt;
      &lt;span class="na"&gt;RabbitMQ__Host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rabbitmq&lt;/span&gt;
      &lt;span class="na"&gt;SMTP__Host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mailhog&lt;/span&gt;
      &lt;span class="na"&gt;SMTP__Port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1025&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5103:8080"&lt;/span&gt;


  &lt;span class="c1"&gt;# ========================&lt;/span&gt;
  &lt;span class="c1"&gt;# API Gateway&lt;/span&gt;
  &lt;span class="c1"&gt;# Acts as the entry point for backend APIs&lt;/span&gt;
  &lt;span class="c1"&gt;# ========================&lt;/span&gt;

  &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/Services/GatewayService/Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fintrack-gateway&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;identity-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
      &lt;span class="na"&gt;transaction-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
      &lt;span class="na"&gt;auditlog-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
      &lt;span class="na"&gt;notification-service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Development&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_URLS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://+:8080&lt;/span&gt;

      &lt;span class="c1"&gt;# Redis used for caching&lt;/span&gt;
      &lt;span class="na"&gt;ConnectionStrings__Redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:6379&lt;/span&gt;

      &lt;span class="c1"&gt;# Reverse proxy routing configuration&lt;/span&gt;
      &lt;span class="na"&gt;ReverseProxy__Clusters__identityCluster__Destinations__identity__Address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://identity-service:8080/&lt;/span&gt;
      &lt;span class="na"&gt;ReverseProxy__Clusters__transactionCluster__Destinations__transaction__Address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://transaction-service:8080/&lt;/span&gt;
      &lt;span class="na"&gt;ReverseProxy__Clusters__auditlogCluster__Destinations__destination1__Address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auditlog-service:8080/&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5193:8080"&lt;/span&gt;


  &lt;span class="c1"&gt;# ========================&lt;/span&gt;
  &lt;span class="c1"&gt;# Frontend applications&lt;/span&gt;
  &lt;span class="c1"&gt;# ========================&lt;/span&gt;

  &lt;span class="na"&gt;web-portal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./apps/web-portal&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fintrack-web-portal&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:80"&lt;/span&gt;

&lt;span class="c1"&gt;# ========================&lt;/span&gt;
&lt;span class="c1"&gt;# Named volumes&lt;/span&gt;
&lt;span class="c1"&gt;# Used to persist database data&lt;/span&gt;
&lt;span class="c1"&gt;# ========================&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Some Practical Notes
&lt;/h2&gt;

&lt;p&gt;These concepts are simple, but understanding their boundaries helps avoid many common Docker misunderstandings.&lt;/p&gt;

&lt;p&gt;When using Docker in a microservice project, it is helpful to clearly understand the scope of several concepts: images, containers, and networks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Image vs Container
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;image&lt;/strong&gt; is a packaged environment that contains the application and everything it needs to run.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;container&lt;/strong&gt; is a running instance of that image.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;p&gt;image → blueprint&lt;br&gt;&lt;br&gt;
container → running process&lt;/p&gt;

&lt;p&gt;Multiple containers can be created from the same image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container Network
&lt;/h3&gt;

&lt;p&gt;When using Docker Compose, all services are placed on the same internal Docker network.&lt;/p&gt;

&lt;p&gt;Inside this network, containers can communicate using &lt;strong&gt;service names as hostnames&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;identity-service can reach MySQL using:&lt;/p&gt;

&lt;p&gt;mysql:3306&lt;/p&gt;

&lt;p&gt;This works because Docker Compose automatically creates an internal network and provides DNS resolution between services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container vs Host
&lt;/h3&gt;

&lt;p&gt;Containers run in an isolated environment.&lt;/p&gt;

&lt;p&gt;Services inside containers are &lt;strong&gt;not automatically accessible from the host machine&lt;/strong&gt; unless a port is exposed.&lt;/p&gt;

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

&lt;p&gt;ports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"5100:8080"&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;host:5100 → container:8080&lt;/p&gt;

&lt;p&gt;After this mapping, the service can be accessed from the host machine using:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:5100" rel="noopener noreferrer"&gt;http://localhost:5100&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Container vs Server
&lt;/h3&gt;

&lt;p&gt;In development, containers run on the local machine.&lt;/p&gt;

&lt;p&gt;In production, the same images can run on a remote server or cloud environment.&lt;/p&gt;

&lt;p&gt;Since the application and its dependencies are packaged inside the image, the runtime environment remains consistent.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>dotnet</category>
      <category>microservices</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>.NET Learning Notes: DelegatingHandler and Harmony (HTTPMataki logging library)</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Tue, 03 Mar 2026 09:45:33 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-delegatinghandler-and-harmony-httpmataki-logging-library-cbg</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-delegatinghandler-and-harmony-httpmataki-logging-library-cbg</guid>
      <description>&lt;h3&gt;
  
  
  Exploring Runtime HTTP Logging in .NET
&lt;/h3&gt;

&lt;p&gt;Recently, I experimented with an automatic HTTP request logging library:&lt;a href="https://github.com/yangzhongke/HttpMataki.NET" rel="noopener noreferrer"&gt;HttpMataki.NET&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This library can capture HTTP request and response information &lt;strong&gt;without modifying the original application code&lt;/strong&gt;, which makes it especially useful during development and debugging. Being able to inspect request and response details transparently is extremely convenient when diagnosing issues or researching process.&lt;/p&gt;

&lt;p&gt;While reviewing its implementation, I noticed that it relies on two interesting .NET runtime techniques. I took this opportunity to explore and better understand how these mechanisms work under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  DelegatingHandler
&lt;/h3&gt;

&lt;p&gt;Before diving into more advanced runtime techniques, the first concept worth clarifying is &lt;strong&gt;DelegatingHandler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A natural question comes up:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If we can log requests using middleware, why do we need DelegatingHandler at all?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To answer that, we need to distinguish between &lt;strong&gt;inbound requests&lt;/strong&gt; and &lt;strong&gt;outbound requests&lt;/strong&gt; in a .NET web application.&lt;/p&gt;

&lt;h4&gt;
  
  
  Inbound Requests: Handled by Kestrel
&lt;/h4&gt;

&lt;p&gt;When an application receives an HTTP request:&lt;br&gt;
Client → Kestrel → Middleware Pipeline → Controller&lt;/p&gt;

&lt;p&gt;The request is processed by the web server (typically &lt;strong&gt;Kestrel&lt;/strong&gt;) and flows through the middleware pipeline.&lt;/p&gt;

&lt;p&gt;This is where we commonly implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logging&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Exception handling&lt;/li&gt;
&lt;li&gt;Metrics collection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Middleware operates on &lt;strong&gt;incoming requests&lt;/strong&gt;.&lt;/p&gt;




&lt;h4&gt;
  
  
  Outbound Requests: Handled by HttpClient
&lt;/h4&gt;

&lt;p&gt;However, when our application calls an external service:&lt;br&gt;
Our App → HttpClient → External API&lt;/p&gt;

&lt;p&gt;This is a completely different pipeline.&lt;/p&gt;

&lt;p&gt;Middleware does &lt;strong&gt;not&lt;/strong&gt; participate in outbound HTTP calls.&lt;/p&gt;

&lt;p&gt;Instead, outbound requests are handled by &lt;code&gt;HttpClient&lt;/code&gt;, and this is where &lt;strong&gt;DelegatingHandler&lt;/strong&gt; comes in.&lt;/p&gt;




&lt;h3&gt;
  
  
  What is DelegatingHandler?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;DelegatingHandler&lt;/code&gt; acts as a message handler in the &lt;code&gt;HttpClient&lt;/code&gt; pipeline. It forms a responsibility chain around outbound HTTP requests.&lt;/p&gt;

&lt;p&gt;You can think of it as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A wrapper around outgoing requests&lt;/li&gt;
&lt;li&gt;A way to intercept and modify requests&lt;/li&gt;
&lt;li&gt;A way to inspect responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It allows you to log, modify, or short-circuit outbound HTTP traffic.&lt;/p&gt;

&lt;p&gt;This is how the logging library is able to inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests sent via &lt;code&gt;HttpClient&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Responses received from external services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without modifying application business logic.&lt;/p&gt;

&lt;p&gt;For detailed implementation, you can refer to the official documentation, but conceptually it behaves like a chain-of-responsibility pattern for outbound HTTP traffic.&lt;/p&gt;




&lt;h3&gt;
  
  
  Simple Demonstration
&lt;/h3&gt;

&lt;p&gt;I created a minimal example to demonstrate where middleware and DelegatingHandler sit in the request lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/liananddandan/DotNet-Mechanisms-Lab/tree/main/DelegatingHandler.Server" rel="noopener noreferrer"&gt;Server example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/liananddandan/DotNet-Mechanisms-Lab/tree/main/DelegatingHandler.Client" rel="noopener noreferrer"&gt;Client example&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These examples clearly show the difference in execution position between inbound middleware processing and outbound HttpClient interception.&lt;/p&gt;




&lt;h3&gt;
  
  
  Harmony: Runtime Method Interception in .NET
&lt;/h3&gt;

&lt;p&gt;The second technique I explored is &lt;strong&gt;Harmony&lt;/strong&gt;, a runtime method patching library.&lt;/p&gt;

&lt;p&gt;Harmony allows developers to modify the behavior of existing methods at runtime without changing the original source code. It works by intercepting method execution and injecting custom logic before, after, or even in place of the original implementation.&lt;/p&gt;

&lt;p&gt;However, it is important to understand its scope and limitations.&lt;/p&gt;

&lt;p&gt;Harmony can only operate &lt;strong&gt;within the same process&lt;/strong&gt;. It does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inject code into other processes&lt;/li&gt;
&lt;li&gt;Modify unmanaged/native applications&lt;/li&gt;
&lt;li&gt;Change type structure (such as adding fields or extending enums)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, Harmony modifies behavior — not structure.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Simple Example
&lt;/h3&gt;

&lt;p&gt;To better understand how it works, I created a minimal example demonstrating:&lt;a href="https://github.com/liananddandan/DotNet-Mechanisms-Lab/tree/main/ReflectionAndHarmony" rel="noopener noreferrer"&gt;example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Overall, Harmony is quite straightforward to use once you understand its parameter binding conventions and runtime patching model.&lt;/p&gt;

&lt;p&gt;In this case, I encountered Harmony while reviewing the implementation of the Mataki library and took the opportunity to explore how it works under the hood.&lt;/p&gt;

&lt;p&gt;While it is not a tool I would reach for in everyday application development, understanding its mechanics provides valuable insight into how runtime interception works in .NET.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>monitoring</category>
      <category>tooling</category>
    </item>
    <item>
      <title>.NET Learning Notes: Custom In-Memory Provider(5) - Include &amp; ThenInclude — Navigation Loading and Fix-Up</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 19 Feb 2026 02:51:35 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-custom-in-memory-provider5-include-theninclude-navigation-loading-and-13ga</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-custom-in-memory-provider5-include-theninclude-navigation-loading-and-13ga</guid>
      <description>&lt;p&gt;In EF Core, Include and ThenInclude are not just “query operators that add extra columns.” They are a loading contract: “when the outer entities are materialized, also load the related entities and make the relationship consistent in the tracked graph.” Relational providers often implement this by producing SQL that happens to look like LEFT JOIN, but the real goal is not the join itself. The real goal is: load related rows and run relationship fix-up so that navigation properties on both sides point to the correct tracked instances.&lt;/p&gt;

&lt;p&gt;This is why Include feels deceptively simple at the API level but becomes difficult at provider level. It sits right at the boundary between query execution, entity materialization, tracking/identity resolution, and navigation fix-up. If you get any of those wrong, you either populate navigations with duplicate instances, break EF Core’s identity map guarantees, or end up with “loaded but not marked loaded” behaviors that cause EF Core to issue unexpected additional loads later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reference Include vs Collection Include vs ThenInclude
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A reference include populates a single navigation property&lt;/strong&gt; such as Order.Customer. Conceptually it is “1:0..1” or “many-to-one”: each outer entity points to at most one related entity. The fix-up is straightforward: once both entities are materialized and tracked, the outer reference can be assigned, and the inverse navigation (if present) can also be set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A collection include populates a collection navigation&lt;/strong&gt; such as Customer.Orders. This is “1:N”. The key difference is that collection include is not just “attach one inner row.” It is “attach potentially many related rows,” and that immediately introduces grouping semantics. A join-shaped result naturally duplicates outer rows; EF Core must prevent that duplication from turning into duplicated outer instances and duplicated navigation items. This is why collection includes are harder: the provider must either group inner rows per outer key or build a correlated subquery per outer row, and then apply fix-up consistently.&lt;/p&gt;

&lt;p&gt;ThenInclude is not a third separate mechanic; it is simply “Include on an entity that is itself included.” In a reference chain, it looks like Blog -&amp;gt; Owner -&amp;gt; Address. In a collection chain, it looks like Blog -&amp;gt; Posts -&amp;gt; Comments. The difficulty is that it composes include paths and requires the provider to apply fix-up in the correct sequence across multiple relationship edges, while still respecting identity resolution.&lt;/p&gt;

&lt;h3&gt;
  
  
  How EF Core Represents Include Internally
&lt;/h3&gt;

&lt;p&gt;At the surface level, Include looks simple. You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context.Customers.Include(c =&amp;gt; c.Orders)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and expect the related entities to be loaded. But internally, EF Core does not treat Include as a normal LINQ operator like Where or Select. So internally, Include lives inside the shaping layer, not the filtering layer. &lt;/p&gt;

&lt;h4&gt;
  
  
  reference navigation
&lt;/h4&gt;

&lt;p&gt;For a reference navigation such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context.Orders.Include(o =&amp;gt; o.Customer)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF Core translates the include into a LEFT JOIN during the translation phase. Internally, the query is rewritten so that the related entity is brought into scope through a join operation. The translation visitor typically calls into &lt;code&gt;TranslateLeftJoin&lt;/code&gt;, producing a join expression that temporarily carries both the outer and inner elements.&lt;/p&gt;

&lt;p&gt;Conceptually, the transformation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Orders
    .LeftJoin(
        Customers,
        o =&amp;gt; o.CustomerId,
        c =&amp;gt; c.Id,
        (o, c) =&amp;gt; new TransparentIdentifier&amp;lt;Order, Customer&amp;gt;(o, c)
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;TransparentIdentifier&amp;lt;Outer, Inner&amp;gt;&lt;/code&gt; is an internal carrier that keeps both sides of the join available for the next projection step.&lt;/p&gt;

&lt;p&gt;After the join, EF Core applies a Select projection that restores the final result shape to the outer entity type. At this stage, the query element type becomes Order again, not a pair of (Order, Customer). The join is used only to retrieve the related row.&lt;/p&gt;

&lt;p&gt;The important part is how Include itself is represented. It is not treated as a normal executable LINQ operator. Instead, EF Core inserts an IncludeExpression (or equivalent marker) into the &lt;strong&gt;ShaperExpression&lt;/strong&gt;, not the main query expression. The shaper is responsible for materialization. During execution, the shaper uses the joined inner entity to populate the navigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order.Customer = customer;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If tracking is enabled, EF Core’s identity resolution and relationship fix-up mechanisms are triggered automatically. &lt;/p&gt;

&lt;p&gt;This is the essential pattern for reference include: LEFT JOIN to fetch the related entity, projection back to the outer type, and navigation assignment handled during shaping and tracking.&lt;/p&gt;

&lt;h4&gt;
  
  
  collection navigation &amp;amp; ThenInclude
&lt;/h4&gt;

&lt;p&gt;In this provider, collection navigation and ThenInclude are implemented using the same underlying rewriting mechanism. Instead of relying on EF Core’s relational join-based pipeline, the provider intercepts IncludeExpression during the compilation stage and replaces it with executable logic that performs a correlated subquery and explicit navigation fix-up.&lt;/p&gt;

&lt;p&gt;For collection navigation (for example, Blog.Include(b =&amp;gt; b.Posts)), EF Core represents the include inside the shaper as an &lt;code&gt;IncludeExpression&lt;/code&gt;. When the navigation is a collection, the NavigationExpression is typically a &lt;code&gt;MaterializeCollectionNavigationExpression&lt;/code&gt;, which contains a subquery describing how to load the related entities. This subquery is not directly executable and cannot be left in the final expression tree. It must be transformed.&lt;/p&gt;

&lt;p&gt;During compilation, the provider scans Queryable.Select calls and detects whether the selector body contains an &lt;code&gt;IncludeExpression&lt;/code&gt;. If found, it rewrites the selector. For collection includes, the subquery stored in MaterializeCollectionNavigationExpression. Subquery is first rewritten into a provider-executable query expression. This step ensures that any ShapedQueryExpression, query roots, or provider-specific expressions are converted into an expression that returns an &lt;code&gt;IEnumerable&amp;lt;TElement&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead of embedding the subquery expression directly into the final tree, the provider compiles it into a delegate of type &lt;code&gt;Func&amp;lt;QueryContext, TOuter, IEnumerable&amp;lt;TElement&amp;gt;&amp;gt;&lt;/code&gt;. At runtime, this delegate is invoked to execute the correlated subquery for each outer entity instance. The result is materialized as a list.&lt;/p&gt;

&lt;p&gt;The original IncludeExpression is then replaced with a call to a helper method such as LoadCollection. This method receives the current entity instance, the navigation metadata, and the materialized related entities. It retrieves or creates the navigation collection on the entity, clears any existing contents, adds the loaded elements, and optionally sets IsLoaded = true on the navigation entry. In this way, the navigation is populated explicitly during materialization.&lt;/p&gt;

&lt;p&gt;ThenInclude does not require a separate mechanism. It works naturally through recursion. When an include subquery itself contains further includes, the same rewriting logic is applied to the nested query during compilation. Because subqueries are rewritten into executable delegates before being invoked, any nested IncludeExpression encountered inside them is also transformed. In effect, ThenInclude is just another include applied within a deeper subquery scope, and the same rewrite-and-fix-up process is reused.&lt;/p&gt;

&lt;p&gt;This approach avoids join-based row duplication and does not depend on TransparentIdentifier shapes. Instead of flattening data through joins and reconstructing object graphs from duplicated rows, the provider executes a correlated subquery per outer entity and performs fix-up directly on entity instances. The final query shape remains a flat sequence of outer entities, with collection navigations populated after materialization.&lt;/p&gt;

&lt;p&gt;The main limitation of this strategy is that it relies on LINQ-to-Objects execution of subqueries and explicit fix-up logic, rather than fully integrating with EF Core’s internal include pipeline. To implement include in a fully “native” EF Core way, the provider would need to participate in navigation expansion, entity key resolution factories, include metadata binding, and shaper-based fix-up integration. That would require significantly more work and much deeper alignment with EF Core’s internal query pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Include/ThenInclude Is Difficult for a Custom Provider
&lt;/h3&gt;

&lt;p&gt;The core difficulty is that Include spans multiple EF Core subsystems at once.&lt;/p&gt;

&lt;p&gt;First, it depends on correct identity resolution. If the included inner entity is materialized as a new object each time it appears, the graph becomes inconsistent immediately.&lt;/p&gt;

&lt;p&gt;Second, it depends on navigation metadata and FK matching. For reference include, you need to locate the inner entity by foreign key from the outer entity. For collection include, you need to locate many inner entities by matching their FK to the outer PK. EF Core has metadata for this (keys, foreign keys, navigations, inverse navigations), but providers must wire enough services so that EF can ask for entity finders and key factories consistently.&lt;/p&gt;

&lt;p&gt;Finally, ThenInclude multiplies the complexity because the provider must apply include logic repeatedly across the included entities, not only the root query entity, while keeping graph identity stable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why LINQ-to-Objects Was the Practical Choice
&lt;/h3&gt;

&lt;p&gt;In your current approach, you treat Include as “metadata in the shaper,” not as something you execute directly. The core pipeline still compiles the main query (QueryRows → TrackFromRow → replay steps → apply terminal). For Include, you extract the navigation names from the shaper before stripping the Include markers, then you run fix-up outside the formal translation/compilation of Include itself.&lt;/p&gt;

&lt;p&gt;That is exactly what this part of your code implies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var includeNavs = ExtractIncludeNavigationNames(shapedQueryExpression.ShaperExpression);
executor = CompileQueryRowsPipeline(q);
// executor = new IncludeStrippingVisitor().Visit(executor)!;
executor = ApplyMarkLoadedWrapper(executor, QueryCompilationContext.QueryContextParameter, includeNavArrayExpr);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sequence matters. You extract include information while it still exists in the shaper. Then you compile your own executable pipeline that does not rely on EF Core’s IncludeExpression being executable. Then you apply a wrapper to ensure EF Core sees navigations as loaded (and/or you apply your own fix-up step).&lt;/p&gt;

&lt;p&gt;Chosing LINQ-to-Objects because it’s the shortest path to a correct demonstration: you can run the main query normally, materialize and track root entities correctly, then for each root entity perform additional in-memory lookups against your IMemoryDatabase tables to populate navigations. This avoids having to fully model EF Core’s internal include shaping contracts while still producing correct final graphs for a demo provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  What “Formal EF Core Pipeline Include” Would Require
&lt;/h3&gt;

&lt;p&gt;If we wanted to implement Include using EF Core’s formal pipeline rather than rewriting it into LINQ-to-Objects logic, the provider would need to participate much deeper in EF Core’s shaping and materialization infrastructure.&lt;/p&gt;

&lt;p&gt;First, the provider would need to fully support EF Core’s IncludeExpression inside the shaper expression. That means not stripping or rewriting it, but allowing it to flow into the shaped query compilation stage. The provider’s IShapedQueryCompilingExpressionVisitor would then be responsible for generating materialization code that cooperates with EF Core’s navigation fix-up system rather than performing manual assignment.&lt;/p&gt;

&lt;p&gt;Second, reference include would require proper join translation support. The translation visitor would need to implement TranslateLeftJoin (and possibly other join patterns) so that EF Core can express reference navigations as join-based query shapes. The provider must then handle TransparentIdentifier-style intermediate projections correctly and ensure that both outer and inner entities are materialized in a coordinated way.&lt;/p&gt;

&lt;p&gt;Third, collection include would require correlated subquery or grouping support at the query model level. Instead of executing a compiled delegate per outer entity, the provider would need to preserve EF Core’s subquery representation and let the shaped query compiler build the correct collection materialization pipeline. This involves supporting MaterializeCollectionNavigationExpression and ensuring that collection shapers cooperate with tracking and fix-up.&lt;/p&gt;

&lt;p&gt;Fourth, the provider would need to integrate fully with EF Core’s tracking hooks. That includes correct usage of key factories, entity finder services, navigation metadata, and tracking behavior flags. Fix-up must occur through the StateManager automatically during materialization rather than through explicit property assignment.&lt;/p&gt;

&lt;p&gt;Finally, the provider would need to guarantee that the final expression tree produced by compilation contains only reducible, executable nodes. IncludeExpression and other internal markers must be handled in the shaper phase, not left in the executable pipeline.&lt;/p&gt;

&lt;p&gt;In short, a formal pipeline implementation would require full join translation support, collection shaping support, proper handling of EF Core’s internal include expressions, and tight integration with the StateManager’s fix-up system. It is significantly more complex than a LINQ-to-Objects rewriting approach because it requires the provider to implement EF Core’s relational-style shaping semantics rather than delegating navigation loading to post-processing logic.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>database</category>
      <category>dotnet</category>
      <category>learning</category>
    </item>
    <item>
      <title>.NET Learning Notes: Custom In-Memory Provider(4) - ReadPath - From IQueryable to Result Execution</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 19 Feb 2026 00:56:46 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-custom-in-memory-provider4-readpath-from-iqueryable-to-result-execution-f9l</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-custom-in-memory-provider4-readpath-from-iqueryable-to-result-execution-f9l</guid>
      <description>&lt;p&gt;The read path in this provider follows EF Core’s standard query pipeline: expression construction, translation, compilation, and execution. The provider does not implement individual LINQ operators as standalone execution logic. Instead, it integrates into EF Core’s existing pipeline and participates at the appropriate extension points.&lt;/p&gt;

&lt;p&gt;When a query starts, no execution occurs. Methods such as Where, Select, OrderBy, ThenBy, Skip, and Take are non-terminal operations. They only build or transform expression trees. Each call composes additional structure onto the existing query expression. At this stage, nothing is executed and no entities are materialized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translation
&lt;/h2&gt;

&lt;p&gt;The provider integrates during the translation phase through &lt;code&gt;IQueryableMethodTranslatingExpressionVisitor&lt;/code&gt;. EF Core parses the LINQ method calls and invokes this visitor to produce a ShapedQueryExpression. In this implementation, the provider does not translate LINQ into a custom DSL or separate query representation. Instead, it constructs a ShapedQueryExpression that carries two things: the underlying row-level query expression and the associated shaper expression. The structure of the query records each translated function's parameters.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is important to understand that translation is not initiated by the provider. EF Core drives the entire process. LINQ method calls are first parsed into method-call expressions, and EF Core then invokes the appropriate TranslateXXX methods on the provider’s visitor. The provider does not “scan” or interpret LINQ directly — it simply responds to EF Core’s translation callbacks and records the query semantics accordingly.&lt;/p&gt;
&lt;h3&gt;
  
  
  QueryRoot and ShapedQuery
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;QueryRoot&lt;/strong&gt; represents the &lt;em&gt;starting point&lt;/em&gt; of a query. When application code writes:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context.Set&amp;lt;TestEntity&amp;gt;()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF Core does &lt;strong&gt;not&lt;/strong&gt; interpret this as “give me a collection of TestEntity”. Instead, it treats it as a &lt;strong&gt;declarative query origin&lt;/strong&gt;: “This query starts from the entity type TestEntity.” Internally, EF Core represents this as a &lt;em&gt;query root expression&lt;/em&gt; associated with the entity type. At this stage: No data is accessed, No enumeration occurs, No filtering or projection happens. A QueryRoot is &lt;strong&gt;purely symbolic&lt;/strong&gt;. It describes &lt;em&gt;what&lt;/em&gt; is being queried, not &lt;em&gt;how&lt;/em&gt; to retrieve it. Every EF Core query—no matter how simple—must begin with a QueryRoot.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Shaped Query&lt;/strong&gt; is the &lt;strong&gt;executable form of a query&lt;/strong&gt;. It represents the result of translating a QueryRoot (and any LINQ operators) into something EF Core can actually run. A shaped query combines two essential components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Query Expression&lt;/strong&gt;: Describes &lt;em&gt;how to retrieve raw data&lt;/em&gt; (rows, objects, records) from the data source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shaper Expression&lt;/strong&gt;: Describes &lt;em&gt;how to transform each retrieved item&lt;/em&gt; into the final result type returned to the user.
Together, they define: “How the query executes” and “What shape the results take.”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why EF Core uses the term &lt;strong&gt;“shaped”&lt;/strong&gt;—the query not only fetches data, but also defines how that data is molded into entities, projections, or scalar values.&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;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Entry point from QueryRoot to ShapedQuery.&lt;/span&gt;
&lt;span class="c1"&gt;/// For a full table scan:&lt;/span&gt;
&lt;span class="c1"&gt;/// QueryExpression = CustomMemoryQueryExpression(entityType)&lt;/span&gt;
&lt;span class="c1"&gt;/// ShaperExpression = CustomMemoryEntityShaperExpression(entityType)&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;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="n"&gt;ShapedQueryExpression&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;CreateShapedQueryExpression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEntityType&lt;/span&gt; &lt;span class="n"&gt;entityType&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;entityType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entityType&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;queryExpression&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;CustomMemoryQueryExpression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entityType&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;shaperExpression&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;CustomMemoryEntityShaperExpression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ShapedQueryExpression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shaperExpression&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;
  
  
  Recording Query Semantics
&lt;/h3&gt;

&lt;p&gt;In this provider, the translation stage is intentionally “record-only”. EF Core parses a LINQ query into method-call expressions and invokes &lt;code&gt;CustomMemoryQueryableMethodTranslatingExpressionVisitor&lt;/code&gt; to translate each operator, but the provider does not execute anything at this point. Instead, it stores the translated intent inside &lt;code&gt;CustomMemoryQueryExpression&lt;/code&gt;, and passes that forward to the compilation stage.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CustomMemoryQueryExpression&lt;/code&gt; acts as a lightweight query model owned by the provider. It does not try to become a DSL, and it does not attempt to replace EF Core’s pipeline. Its job is simpler: capture the entity type being queried and record every non-terminal operator in the exact order it appears in the original LINQ chain. The &lt;strong&gt;ordering requirement&lt;/strong&gt; is the key reason this type exists. Operators like OrderBy, Skip, and Take are not commutative, and their meaning depends on sequencing. OrderBy().Skip(10).Take(5) is a different query from Skip(10).Take(5).OrderBy(). If the provider only stored “there is a skip and a take somewhere” without preserving their positions, it would not be able to reproduce correct behavior during execution.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;CustomMemoryQueryExpression&lt;/code&gt; keeps Steps as an ordered list rather than a set of flags. Each translation method appends a new step and returns a new &lt;code&gt;CustomMemoryQueryExpression&lt;/code&gt; instance, so the expression remains immutable and composable while still preserving the original operator sequence.&lt;/p&gt;

&lt;p&gt;For example, when EF Core encounters a Where(...) call, visitor handles it by reading the existing CustomMemoryQueryExpression from the shaped query, appending a WhereStep(predicate), and returning a new ShapedQueryExpression that carries the updated query expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;protected override ShapedQueryExpression? TranslateWhere(
    ShapedQueryExpression source, 
    LambdaExpression predicate)
{
    if (source.QueryExpression is not CustomMemoryQueryExpression q)
    {
        return null;
    }

    var q2 = q.AddStep(new WhereStep(predicate));

    return new ShapedQueryExpression(q2, source.ShaperExpression);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A concrete example makes the “record in order” design easier to see. Suppose the user writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var query =
    context.Set&amp;lt;Product&amp;gt;()
        .Where(p =&amp;gt; p.Price &amp;gt; 1000)
        .OrderBy(p =&amp;gt; p.Id)
        .Skip(10)
        .Take(5);

protected override ShapedQueryExpression? TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)  
{  
    if (source.QueryExpression is not CustomMemoryQueryExpression q)  
    {        return null;  
    }  
    var q2 = q.AddStep(new WhereStep(predicate));  

    return new ShapedQueryExpression(q2, source.ShaperExpression);  
}

protected override ShapedQueryExpression? TranslateOrderBy(ShapedQueryExpression source,  
    LambdaExpression keySelector, bool ascending)  
{  
    if (source.QueryExpression is not CustomMemoryQueryExpression q)  
        return null;  
    var kind = ascending ? CustomMemoryQueryStepKind.OrderBy : CustomMemoryQueryStepKind.OrderByDescending;  
    // OrderBy: descending = !ascending, thenBy = false  
    var q2 = q.AddStep(new OrderStep(kind, keySelector));  

    return new ShapedQueryExpression(q2, source.ShaperExpression);  
}

protected override ShapedQueryExpression? TranslateSkip(ShapedQueryExpression source, Expression count)  
{  
    if (source.QueryExpression is not CustomMemoryQueryExpression q)  
        return null;  

    // record skip  
    var q2 = q.AddStep(new SkipStep(count));  

    return new ShapedQueryExpression(q2, source.ShaperExpression);  
}

protected override ShapedQueryExpression? TranslateTake(ShapedQueryExpression source, Expression count)  
{  
    if (source.QueryExpression is not CustomMemoryQueryExpression q)  
        return null;  

    var q2 = q.AddStep(new TakeStep(count));  

    return new ShapedQueryExpression(q2, source.ShaperExpression);  
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During translation, this does not run a scan, does not apply filters, and does not materialize entities. Instead, the visitor transforms the query into a single &lt;code&gt;CustomMemoryQueryExpression&lt;/code&gt; whose Steps sequence matches the LINQ chain: first a &lt;code&gt;WhereStep(Price &amp;gt; 1000)&lt;/code&gt;, then an &lt;code&gt;OrderByStep(Id)&lt;/code&gt;, then a &lt;code&gt;SkipStep(10)&lt;/code&gt;, then a &lt;code&gt;TakeStep(5)&lt;/code&gt;. That ordered list is the only thing your compilation stage needs in order to replay the semantics correctly over the underlying row source. In other words, translation is where the provider captures “what the query means”, and compilation is where it later decides “how to execute it”.&lt;/p&gt;

&lt;p&gt;This also explains why your translation methods return new ShapedQueryExpression(q2, source.ShaperExpression) rather than producing results. The shaped query is the carrier EF Core expects at the boundary between translation and compilation. Your provider simply plugs its recorded query model (CustomMemoryQueryExpression) into that carrier, so the next phase can compile it into executable logic without losing any of the original LINQ intent.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In short, translation is not a provider-controlled stage but an EF Core-driven callback sequence. The provider participates by responding to translation hooks and capturing semantics, rather than independently parsing or executing LINQ.&lt;/p&gt;
&lt;h3&gt;
  
  
  Compilation Stage and Identity Resolution Integration
&lt;/h3&gt;

&lt;p&gt;The compilation stage transforms the recorded query semantics into an executable expression tree. At this point, translation has already produced a ShapedQueryExpression containing a CustomMemoryQueryExpression. That object records the entity type, the ordered non-terminal steps, and any terminal operator. No execution has occurred yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A ShapedQueryExpression consists of two parts: a query expression and a shaper expression. The query expression describes how raw data should be retrieved. The shaper expression describes how each element should be shaped into the expected result type. Translation constructs both pieces, but neither is executed during that phase.&lt;/p&gt;

&lt;p&gt;When a terminal operation such as ToList, First, Single, Count, or Any is invoked, EF Core enters the compilation phase and calls the provider’s IShapedQueryCompilingExpressionVisitor. The responsibility of this phase is to construct an expression tree that EF Core will later compile into a delegate. Execution only happens after that delegate is produced.&lt;/p&gt;

&lt;p&gt;Compilation begins by constructing the row-level source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IMemoryTable&amp;lt;TEntity&amp;gt;.QueryRows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This yields an &lt;code&gt;IQueryable&amp;lt;SnapshotRow&amp;gt;&lt;/code&gt;, representing the storage-level rows. No entity instances exist at this stage.&lt;/p&gt;

&lt;p&gt;The provider then builds a projection that converts each SnapshotRow into a tracked entity instance. This is done by inserting a Queryable.Select call whose selector invokes a helper method such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TrackFromRow&amp;lt;TEntity&amp;gt;(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting pipeline conceptually resembles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QueryRows
    .Select(row =&amp;gt; TrackFromRow&amp;lt;TEntity&amp;gt;(...))
    .Where(...)
    .OrderBy(...)
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The non-terminal steps recorded during translation are replayed in their original order. Each recorded step is translated into the corresponding Queryable method call expression and appended to the expression tree. For example, a recorded WhereStep becomes a Queryable.Where call over the current source expression. Terminal operators are appended as the final node, shaping the overall result form.&lt;/p&gt;

&lt;p&gt;The final result of compilation is a complete expression tree describing the entire query. EF Core compiles this tree into a delegate and executes it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Reusing EF Core Identity Resolution and Fix-Up&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Identity management is not implemented by the provider. Instead, the provider integrates with EF Core’s tracking infrastructure at materialization time.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;TrackFromRow&amp;lt;TEntity&amp;gt;&lt;/code&gt;, the provider first ensures that the QueryContext has an initialized state manager. It then attempts identity resolution using the entity’s primary key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var entry = qc.TryGetEntry(
    primaryKey,
    row.Key,
    throwOnNullKey: false,
    out var hasNullKey);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a tracked instance already exists for that key, it is returned immediately. This guarantees that within a single DbContext, only one object instance exists per primary key.&lt;/p&gt;

&lt;p&gt;If no tracked entry is found, the provider materializes a new instance using EF Core’s IEntityMaterializerSource. The snapshot is converted into a ValueBuffer, and EF Core’s materializer is invoked to construct the entity. After materialization, the provider calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qc.StartTracking(entityType, instance, originalValues);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This inserts the entity into EF Core’s identity map. From that point forward, identity resolution, navigation fix-up, and relationship consistency are handled entirely by EF Core.&lt;/p&gt;

&lt;p&gt;The provider does not maintain its own identity map and does not implement fix-up logic. It supplies stable row identity and defers object identity semantics to EF Core. Because tracking is injected at materialization time, the provider remains fully compatible with change tracking and relationship fix-up without reimplementing those mechanisms.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Architectural Separation&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The responsibilities remain clearly separated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The storage layer provides SnapshotRow and guarantees stable row identity.&lt;br&gt;
  The compilation layer constructs the executable query pipeline.&lt;br&gt;
  The tracking layer, including identity resolution and fix-up, is fully managed by EF Core.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By delegating object identity to EF Core and keeping the storage model snapshot-based, the provider preserves EF Core’s semantics while maintaining a clean separation between row storage and entity instantiation.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>database</category>
      <category>dotnet</category>
      <category>learning</category>
    </item>
    <item>
      <title>.NET Learning Notes: Custom In-Memory Provider(3) - Storage Write Model and Key-Based Retrieval</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Wed, 18 Feb 2026 04:44:12 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-custom-in-memory-provider3-storage-write-model-and-key-based-retrieval-1i7m</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-custom-in-memory-provider3-storage-write-model-and-key-based-retrieval-1i7m</guid>
      <description>&lt;h3&gt;
  
  
  Write Model
&lt;/h3&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;e&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;TestEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SmokeTest User"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;  
&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&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;addCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF Core does not immediately write changes to the database when &lt;code&gt;Add&lt;/code&gt;, &lt;code&gt;Update&lt;/code&gt;, or &lt;code&gt;Remove&lt;/code&gt; is called. These operations only modify the internal ChangeTracker. Every tracked entity is assigned an EntityState, such as Added, Modified, or Deleted. The &lt;strong&gt;StateManager&lt;/strong&gt; accumulates these transitions in memory and maintains a consistent view of the unit of work.&lt;/p&gt;

&lt;p&gt;When SaveChanges() is invoked, EF Core collects all pending changes from the ChangeTracker, transforms them into a list of IUpdateEntry instances, and forwards that list to the provider through Database(&lt;code&gt;public class CustomMemoryEfDatabase : Database&lt;/code&gt;)'s SaveChanges(&lt;code&gt;IList&amp;lt;IUpdateEntry&amp;gt; entries&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;At this moment, control shifts from EF Core to the provider.&lt;/p&gt;

&lt;p&gt;In the custom in-memory provider, the CustomMemoryEfDatabase class receives the accumulated entries and iterates through them. For each entry, the provider inspects EntityState and routes the entity to the appropriate table-level operation (Add, Update, or Remove). After all entries are processed, the provider commits those pending table changes to the underlying storage by calling IMemoryDatabase.SaveChanges().&lt;/p&gt;

&lt;p&gt;All of detecting changes logic is delegated to EF Core. The provider’s responsibility begins only when EF Core decides to persist changes. EF Core owns state management and change detection. The provider owns storage persistence. SaveChanges() becomes the clear write boundary between these two layers.&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;CustomMemoryEfDatabase&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;SaveChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IUpdateEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;affected&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="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="c1"&gt;// 先只做 Added，跑通闭环  &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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityState&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt;  
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;.&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;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;.&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="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;.&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;Deleted&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  
                &lt;span class="k"&gt;continue&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;entityEntry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToEntityEntry&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;entity&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entityEntry&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runtimeType&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="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;();&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;{&lt;/span&gt;            
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;.&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="p"&gt;:&lt;/span&gt;  
                    &lt;span class="nf"&gt;InvokeTableMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtimeType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Add"&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;affected&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;  
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;.&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="nf"&gt;InvokeTableMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtimeType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Update"&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;affected&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;.&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;Deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  
                    &lt;span class="nf"&gt;InvokeTableMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtimeType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Remove"&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;affected&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt; 
                    &lt;span class="k"&gt;break&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;_memoryDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChanges&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;affected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key-Based Retrieval (Find)
&lt;/h3&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;e&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;TestEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"FinderTest"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;  
&lt;span class="n"&gt;ctx1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
&lt;span class="n"&gt;ctx1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChanges&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;e&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;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;$"CTX1: Added entity id=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

&lt;span class="c1"&gt;// Same context: Find twice (2nd time should be TRACKED HIT)  &lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;f1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TestEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this provider, Find is not merely a dictionary lookup. It acts as a controlled bridge between the storage layer and EF Core’s tracking system. The operation consists of two conceptual steps: resolving a row by primary key and ensuring the resulting entity participates in EF Core’s identity map.&lt;/p&gt;

&lt;p&gt;When Find is invoked, the provider first validates the supplied key values against the entity’s primary key metadata. It then checks the StateManager to determine whether an entity with the same key is already tracked. If so, that tracked instance is returned directly. This preserves EF Core’s guarantee that only one entity instance per key exists within a DbContext.&lt;/p&gt;

&lt;p&gt;If the entity is not tracked, the provider performs a storage-level lookup. In this implementation, the in-memory table already materializes entities when retrieving rows, so table.Find(...) returns an entity instance rather than a raw snapshot. In other words, materialization happens inside the storage layer rather than through the query pipeline.&lt;/p&gt;

&lt;p&gt;However, returning the instance alone would bypass EF Core’s tracking guarantees. Therefore, the provider explicitly registers the materialized entity with the StateManager, marking it as Unchanged with acceptChanges: true. This ensures the entity becomes part of EF Core’s identity map and behaves consistently with other tracked entities.&lt;/p&gt;

&lt;p&gt;It is important to emphasize that Find does not implement identity resolution itself. Identity resolution is enforced by EF Core’s tracking infrastructure. The provider’s responsibility is to supply a stable key-based lookup and to integrate the resulting entity into the tracking system.&lt;/p&gt;

&lt;p&gt;Unlike LINQ queries, Find bypasses the query translation pipeline entirely. It is a specialized, key-based retrieval path that integrates directly with the storage layer while still honoring EF Core’s tracking model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomMemoryEntityFinder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&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;IEntityFinder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TEntity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?[]?&lt;/span&gt; &lt;span class="n"&gt;keyValues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FindCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyValues&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;TEntity&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;FindCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?[]?&lt;/span&gt; &lt;span class="n"&gt;keyValues&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;keyValues&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="k"&gt;null&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;pk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_entityType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindPrimaryKey&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;$"Entity type '&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_entityType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;' has no primary key."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

        &lt;span class="c1"&gt;// 1) tracked hit  &lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tracked&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_stateManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyValues&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;tracked&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tracked&lt;/span&gt;&lt;span class="p"&gt;.&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;Microsoft&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityFrameworkCore&lt;/span&gt;&lt;span class="p"&gt;.&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;Deleted&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="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;tracked&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="c1"&gt;// 2) store lookup  &lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_entityType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClrType&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;foundObj&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ToObjectArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyValues&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;foundObj&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="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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;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;TEntity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;foundObj&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

        &lt;span class="c1"&gt;// 3) attach  &lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;internalEntry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_stateManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrCreateEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_entityType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

        &lt;span class="n"&gt;internalEntry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetEntityState&lt;/span&gt;&lt;span class="p"&gt;(&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;Unchanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acceptChanges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In EF Core, Find exists as a dedicated identity-based retrieval path rather than as part of the normal query pipeline. Its purpose is fundamentally different from LINQ-based queries.&lt;/p&gt;

&lt;p&gt;A standard query is expression-driven. It flows through translation, compilation, and shaped execution before entities are materialized. Find, by contrast, is key-driven. Its semantic contract is simple and strict: retrieve an entity by its primary key while preserving identity consistency within the current DbContext.&lt;/p&gt;

&lt;p&gt;Performance is another reason for the separation. Primary key lookup is one of the most common operations in data access. It does not require expression tree translation or query compilation. Treating it as a specialized path avoids unnecessary overhead.&lt;/p&gt;

&lt;p&gt;In this provider, the distinction is explicit. LINQ queries operate over &lt;code&gt;IQueryable&amp;lt;SnapshotRow&amp;gt;&lt;/code&gt; and participate fully in translation and compilation. Find bypasses that mechanism and directly performs key-based lookup, then integrates the result into EF Core’s tracking system. The storage layer supplies row identity; the tracking layer enforces object identity.&lt;/p&gt;

&lt;p&gt;In short, Find is intentionally independent from the query pipeline because it serves a different architectural role. It is not expression-based retrieval — it is identity-based retrieval.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌──────────────────────┐
                 │      DbContext       │
                 └─────────┬────────────┘
                           │
         ┌─────────────────┴─────────────────┐
         │                                   │
     Write Path                            Find
 (Add/Update/Delete)                   (Key-Based Lookup)
         │                                   │
         ▼                                   ▼
   ChangeTracker                       StateManager
 (EntityState entries)             (Identity Map check)
         │                                   │
         ▼                                   ▼
  SaveChanges() call                   Store Lookup
         │                                   │
         ▼                                   ▼
 CustomMemoryEfDatabase                MemoryTable.Find
         │                                   │
         ▼                                   ▼
     MemoryTable                        Entity Attach
 (Snapshot + pending)               (SetEntityState = Unchanged)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>csharp</category>
      <category>database</category>
      <category>dotnet</category>
      <category>learning</category>
    </item>
    <item>
      <title>.NET Learning Notes: Custom In-Memory Provider(2) - In-Memory Database Runtime</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 16 Feb 2026 08:55:35 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-custom-in-memory-provider2-in-memory-database-runtime-254p</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-custom-in-memory-provider2-in-memory-database-runtime-254p</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/liananddandan/InMemoryProviderForEFCore/tree/main/CustomMemoryEFProvider.Core" rel="noopener noreferrer"&gt;CustomMemoryEFProvider Database Runtime GitHub&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This section describes the runtime architecture of the in-memory storage layer, independent from EF Core’s translation and compilation pipeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  MemoryDatabaseRoot (Store Registry / Database Name Isolation)
&lt;/h3&gt;

&lt;p&gt;MemoryDatabaseRoot is the global registry of in-memory stores. It ensures that DbContext instances using the same database name share the same underlying store, while different names result in isolated databases.&lt;/p&gt;

&lt;p&gt;It is registered as a singleton and caches MemoryDatabase instances by databaseName, providing name-based store isolation consistent with EF Core’s in-memory behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  IMemoryDatabase / MemoryDatabase (Table Registry + Transaction Routing)
&lt;/h3&gt;

&lt;p&gt;MemoryDatabase acts as the coordination layer between entity tables and transactions. It maintains a registry of tables keyed by CLR type, ensuring that each entity type corresponds to a single in-memory table instance.&lt;/p&gt;

&lt;p&gt;MemoryDatabase itself does not implement storage logic. Its responsibility is orchestration—table resolution and transaction routing—while the actual data storage behavior is delegated to &lt;code&gt;MemoryTable&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  IMemoryTable / MemoryTable — Snapshot Rows and Change Merging
&lt;/h3&gt;

&lt;p&gt;MemoryTable is designed as a snapshot-based storage unit. It does not store entity instances directly. Instead, each row is represented as a SnapshotRow, which contains a primary key and a ScalarSnapshot. This keeps the storage layer independent from tracked entity objects and prevents object graph leakage into the persistence layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reason for introducing SnapshotRow — and exposing &lt;code&gt;IQueryable&amp;lt;SnapshotRow&amp;gt;&lt;/code&gt; via QueryRows — is to clearly separate storage representation from entity instances, while allowing EF Core to fully control materialization, tracking, and identity resolution.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instead of storing entity objects directly, the table stores ScalarSnapshot data. This avoids persisting full object graphs, navigation properties, collections, and circular references. By limiting storage to scalar values only, the provider keeps the storage layer deterministic, lightweight, and easy to clone during transactions. Snapshot-based storage also makes transaction isolation much simpler, since copy-on-write operates on row data rather than entity instances.&lt;/p&gt;

&lt;p&gt;SnapshotRow encapsulates both the primary key (Key) and the scalar data (Snapshot). The key provides a stable identity for the row, which is essential for tracking and identity resolution. The snapshot represents the raw column values. This structure mirrors how relational providers conceptually operate: rows are independent data records, and entity instances are created later during materialization.&lt;/p&gt;

&lt;p&gt;Returning &lt;code&gt;IQueryable&amp;lt;SnapshotRow&amp;gt;&lt;/code&gt; preserves deferred execution and composability. EF Core can continue building expression trees on top of it (Where, Select, Join, Include, etc.), and the provider participates in the normal translation and compilation pipeline. Only during the shaped query compilation phase are rows materialized into entity instances. This ensures the provider does not prematurely instantiate entities or bypass EF Core’s tracking pipeline.&lt;/p&gt;

&lt;p&gt;Disabling direct &lt;code&gt;IQueryable&amp;lt;TEntity&amp;gt;&lt;/code&gt; access enforces this design. All queries must go through QueryRows, ensuring that entity materialization, identity resolution, and tracking remain under EF Core’s control rather than leaking storage-level objects directly.&lt;/p&gt;

&lt;p&gt;In short, SnapshotRow establishes a clean row-level storage contract. It enables clear separation of concerns, simplifies transaction isolation, and maximizes reuse of EF Core’s existing tracking and identity resolution mechanisms.**&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Internally, the table maintains two logical states: committed data and pending changes. Committed data represents the stable, persisted state of the table. Pending changes record modifications before SaveChanges is called, and each entry is marked as Added, Modified, or Deleted.&lt;/p&gt;

&lt;p&gt;Only scalar properties are included in ScalarSnapshot. Navigation properties, collections, and complex object graphs are intentionally excluded. This avoids circular references, deep cloning problems, and recursive graph persistence. The table remains a flat, value-based storage model, while relationship fix-up and identity resolution are handled later in the EF Core query pipeline rather than inside the storage layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In ExtractSnapshot, scalar property values are stored in a dictionary keyed by property name rather than by positional index. This design avoids relying on property ordering and instead preserves a stable name-based mapping. EF Core’s query pipeline does not guarantee that property ordering during materialization will always match CLR reflection order. By storing values keyed by property name, snapshot reconstruction becomes resilient to ordering differences and aligns naturally with EF Core’s metadata-driven property resolution model. In other words, the snapshot format is name-oriented rather than position-oriented, which makes materialization deterministic and decoupled from reflection order assumptions.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Transactions and Isolation Model — MemoryTransaction and Transactional Clone
&lt;/h3&gt;

&lt;p&gt;The transaction model in this provider follows a simplified isolation strategy based on table-level cloning. When a transaction begins, the system creates transactional tables by cloning the current visible state of each base table. The cloned state is not a raw object copy of entity instances, but a snapshot-level copy based on the merged result of committed data and pending changes as exposed by QueryRows. This ensures that the transaction starts from a consistent logical view of the database.&lt;/p&gt;

&lt;p&gt;Once a transaction is active, all table access is routed to the transactional tables. Changes are applied only to these transactional copies, leaving the base tables untouched during the transaction scope.&lt;/p&gt;

&lt;p&gt;On Commit, the transactional tables replace the corresponding base tables. In practice, this means clearing the base table and re-importing all rows from the transactional table, effectively promoting the transactional state to committed state.&lt;/p&gt;

&lt;p&gt;On Rollback, the transactional tables are simply discarded. Because base tables were never modified during the transaction, no additional undo logic is required.&lt;/p&gt;

&lt;p&gt;This design represents a single-transaction, table-level copy-on-write model. It does not support nested transactions or fine-grained row versioning. Instead, it prioritizes architectural clarity and predictable isolation behavior suitable for a demonstration provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    [DbContext] 

--&amp;gt; [MemoryDatabaseRoot]

--&amp;gt; [MemoryDatabase]

--&amp;gt; [MemoryTable&amp;lt;T&amp;gt;]

--&amp;gt; [SnapshotRow]

--&amp;gt; [ScalarSnapshot]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>architecture</category>
      <category>database</category>
      <category>dotnet</category>
      <category>learning</category>
    </item>
    <item>
      <title>.NET Learning Notes: Custom In-Memory Provider(1) - Registration and Discovery</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Fri, 13 Feb 2026 04:28:39 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/c-learning-notes-custom-in-memory-provider1-registration-and-discovery-12i0</link>
      <guid>https://dev.to/alexleeeeeeeeee/c-learning-notes-custom-in-memory-provider1-registration-and-discovery-12i0</guid>
      <description>&lt;h1&gt;
  
  
  1. Custom In-Memory Provider Registration and Discovery in EF Core
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Core Goal&lt;/strong&gt;: The objective of this section is to establish a proper EF Core provider registration entry point, allowing EF Core to discover the custom provider, integrate it into the options pipeline, and later invoke its service registration logic during DbContext initialization.&lt;/p&gt;

&lt;h1&gt;
  
  
  Key Implementations
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1.1 Provider Extension Entry
&lt;/h2&gt;

&lt;p&gt;I implement EF Core-style extension methods as the entry point for the custom provider. Following EF Core's naming conventions: Use &lt;strong&gt;&lt;code&gt;Use[ProviderName]&lt;/code&gt;&lt;/strong&gt; (e.g., &lt;code&gt;UseCustomMemoryDb&lt;/code&gt;)—the standard convention for EF Core provider activation methods.&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;// Configures the DbContext to use the custom in-memory database provider&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;DbContextOptionsBuilder&lt;/span&gt; &lt;span class="nf"&gt;UseCustomMemoryDb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
    &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;DbContextOptionsBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&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;databaseName&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;clearOnCreate&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="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;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"DbContextOptionsBuilder cannot be null."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="c1"&gt;// Create extension instance with user-provided configuration  &lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;extension&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;Options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FindExtension&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomMemoryDbContextOptionsExtension&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;new&lt;/span&gt; &lt;span class="nf"&gt;CustomMemoryDbContextOptionsExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;databaseName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clearOnCreate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add or update the extension in EF Core's options (EF Core internal API)  &lt;/span&gt;
    &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;IDbContextOptionsBuilderInfrastructure&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;AddOrUpdateExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extension&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;builder&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;AddOrUpdateExtension&lt;/code&gt; injects the custom IDbContextOptionsExtension into EF Core’s immutable options model.&lt;/p&gt;

&lt;p&gt;During service provider construction, EF Core scans registered extensions and invokes their ApplyServices method. This is the moment where provider-specific services (query compilation, execution pipeline, database abstractions, etc.) are wired into the internal DI container.&lt;/p&gt;

&lt;p&gt;And we can use it by:&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;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseCustomMemoryDb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TestDatabase"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step only integrates the extension into EF core's configuration pipeline. The actual behavior is defined later in the &lt;code&gt;IDbContextOptionsExtension.ApplyServices&lt;/code&gt; implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  1.2 Provider Extension Class
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;CustomMemoryDbContextOptionsExtension&lt;/code&gt; class implements &lt;code&gt;IDbContextOptionsExtension&lt;/code&gt;. This extension is the provider's configuration carrier and the formal signal EF Core uses to discover and activate a database provider. EF Core recognizes it as a database provider via the DbContextOptionsExtensionInfo metadata (e.g. IsDatabaseProvider =&amp;gt; true), and later calls &lt;code&gt;ApplyServices&lt;/code&gt; as the entry hook to register provider services. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;ApplyServices&lt;/code&gt; runs during EF Core’s internal service-provider construction for a given DbContextOptions instance. This is important: provider services are registered into EF Core’s internal service collection for that context (a scoped “internal container”), not the application’s global DI container. In other words, the provider wiring is isolated to the DbContext’s internal dependency graph.&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;void&lt;/span&gt; &lt;span class="nf"&gt;ApplyServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="c1"&gt;// Register configuration as singleton (available to all provider services)  &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;AddSingleton&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;CustomMemoryDbConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_databaseName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_clearOnCreate&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;AddEntityFrameworkCustomMemoryDatabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="nf"&gt;AddEntityFrameworkCustomMemoryDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
    &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;// Validate input (align with official provider patterns)&lt;/span&gt;
    &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  
    &lt;span class="c1"&gt;// NEW: register SnapshotValueBufferFactory for compiling visitor factory  &lt;/span&gt;
    &lt;span class="n"&gt;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SnapshotValueBufferFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; 
&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;// Step 1: register EF Core framework-facing services (core pipeline slots)&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EntityFrameworkServicesBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  

&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;// These are EF Core "framework services" that must be present for a database provider&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;TryAdd&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ITypeMappingSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryTypeMappingSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggingDefinitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryLoggingDefinitions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IQueryableMethodTranslatingExpressionVisitorFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="n"&gt;CustomMemoryQueryableMethodTranslatingExpressionVisitorFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IShapedQueryCompilingExpressionVisitorFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="n"&gt;CustomMemoryShapedQueryCompilingExpressionVisitorFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IValueGeneratorSelector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryValueGeneratorSelector&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDatabaseProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryDatabaseProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Required: provider must supply an IDatabase implementation &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;TryAdd&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IDatabase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryEfDatabase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt; 
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAdd&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IQueryContextFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryQueryContextFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;// Register EF Core core services (fills remaining defaults) &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;TryAddCoreServices&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Override specific EF services when default behavior is not compatible with this provider&lt;/span&gt;
    &lt;span class="n"&gt;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
        &lt;span class="n"&gt;ServiceDescriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEntityFinderSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryEntityFinderSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;  
    &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;// Step 2: provider-specific extension services (provider-only abstractions)&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;TryAddProviderSpecificServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEntityFinderFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryEntityFinderFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ICustomMemoryDatabaseProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CustomMemoryDatabaseProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;  
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;// Step 3: provider storage services (actual in-memory database implementation)&lt;/span&gt;
    &lt;span class="n"&gt;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryAddSingleton&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;MemoryDatabaseRoot&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  
    &lt;span class="n"&gt;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TryAddScoped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IMemoryDatabase&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&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;cfg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomMemoryDbConfig&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;root&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MemoryDatabaseRoot&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;db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DatabaseName&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;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClearOnCreate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="p"&gt;{&lt;/span&gt;            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ClearAllTables&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;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;serviceCollection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryAddScoped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMemoryTable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;),&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MemoryTable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&amp;gt;));&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;serviceCollection&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;Provider registers config as singleton (&lt;code&gt;services.AddSingleton(new CustomMemoryDbConfig(_databaseName, _clearOnCreate));&lt;/code&gt;). Because EF Core can cache or reuse the internal service provider based on the options fingerprint. &lt;/p&gt;

&lt;p&gt;EF Core assumes your provider fully participates in the internal pipeline. That pipeline has specific service slots (translation, compilation, execution, tracking helpers, value generation, type mapping, etc.). These slots are not optional in practice: EF Core will hit them during normal operations such as translating LINQ, compiling shaped queries, creating query contexts, generating keys, and materializing results.&lt;/p&gt;

&lt;p&gt;TryAddCoreServices() fills many of those slots with EF Core defaults. That’s useful, but it also means you must register your provider implementations &lt;em&gt;before&lt;/em&gt; calling TryAddCoreServices() when you intend to override defaults (or when defaults are provider-incompatible). Otherwise, EF Core may wire up default services that either do not work for your provider, or silently bypass your implementation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IQueryableMethodTranslatingExpressionVisitorFactory&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the core LINQ-to-provider translation entry. EF Core parses LINQ into expression trees, then uses this factory to create a translator visitor that converts LINQ method calls (Where/Select/Join/etc.) into a provider-specific query representation. If you want to control what LINQ patterns are supported and how they are interpreted, this is one of the most important extension points.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IShapedQueryCompilingExpressionVisitorFactory&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After translation, EF Core still needs to compile a “shaped query”: a runnable delegate that materializes results into entity instances (or projections), handles tracking, identity resolution, and include fix-up. This factory is where a provider plugs into the compilation phase and controls how snapshots/rows become materialized objects.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IValueGeneratorSelector&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the function of this Selector is value generation. By default, EF Core’s built-in ValueGeneratorSelector only knows how to generate: Guid, string, byte[]. It does &lt;strong&gt;not&lt;/strong&gt; generate int identities unless a provider supplies that behavior. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>csharp</category>
      <category>database</category>
      <category>dotnet</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>.NET Learning Notes: YARP</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Wed, 05 Nov 2025 20:54:56 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-yarp-14n4</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-yarp-14n4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A reverse proxy is a server that sits in fronts of one or more backend servers and handles requests from clients on behalf of those servers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A reverse proxy provides several benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routing&lt;/li&gt;
&lt;li&gt;Load balancing&lt;/li&gt;
&lt;li&gt;Scalability: By distributing traffic across multiple serves, a reverse proxy helps scale apps to handle more users and higher loads.&lt;/li&gt;
&lt;li&gt;SSL/TLS Termination&lt;/li&gt;
&lt;li&gt;Incoming URLs can be transformed before forwarding to the backend. Internal service endpoints can change without affecting external URLs.&lt;/li&gt;
&lt;li&gt;Security: Internal service endpoints can be hidden from external exposure.&lt;/li&gt;
&lt;li&gt;Caching
# YARP
## 1. Load Balancing
Load balancing policies are registered in the DI container via the &lt;code&gt;AddLoadBalancingPolicies()&lt;/code&gt; method, which is automatically called by &lt;code&gt;AddReverseProxy()&lt;/code&gt;.
The middleware is added with &lt;code&gt;UseLoadBalancing&lt;/code&gt;, which is included by default in the parameterless &lt;code&gt;MapReverseProxy&lt;/code&gt; method.
### Cluster Configuration
The algorithm used to determine the destination can be configured by setting the &lt;code&gt;LoadBalancingPolicy&lt;/code&gt;. If no policy is specified, &lt;code&gt;PowerOfTwoChoices&lt;/code&gt; will be used.
Built-in policies:&lt;/li&gt;
&lt;li&gt;FirstAlphabetical&lt;/li&gt;
&lt;li&gt;Random&lt;/li&gt;
&lt;li&gt;PowerOfTwoChoices (default)&lt;/li&gt;
&lt;li&gt;RoundRobin&lt;/li&gt;
&lt;li&gt;LeastRequests
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"ReverseProxy": {
  "Clusters": {
    "cluster1": {
      "LoadBalancingPolicy": "RoundRobin",
      "Destinations": {
        "cluster1/destination1": {
          "Address": "https://localhost:10000/"
        },
        "cluster1/destination2": {
          "Address": "https://localhost:10010/"
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Session Affinity
&lt;/h2&gt;

&lt;p&gt;Session affinity is a mechanism to bind a causally related request sequence to the destination that handled the first request when the load is balanced among several destinations.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is an Affinity Key
&lt;/h3&gt;

&lt;p&gt;In YARP, &lt;strong&gt;an Affinity Key&lt;/strong&gt; is simply the identifier that ties a client's subsequent requests to the same backend destination.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key itself is just a container name.&lt;/li&gt;
&lt;li&gt;The value stored inside that key is generated dynamically by YARP at runtime and is different for each client or session.&lt;/li&gt;
&lt;li&gt;This value encodes the target destination ID - or sometimes a group of destinations - so that the proxy knows where to route the next request.
### Affinity failure policy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redistribute (default)&lt;/strong&gt;- tries to establish a new affinity to one of available healthy destinations by skipping the affinity lookup step and passing all healthy destination to the load balancer the same way it is done for a request without any affinity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return503Error&lt;/strong&gt; - send a &lt;code&gt;503&lt;/code&gt; response back to the client and request processing is terminated.
## 3. Destination health checks
### Active health checks
YARP can proactively monitor destination health by sending periodic probing requests to designated health endpoints and analyzing responses.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Clusters": {
  "cluster1": {
    "HealthCheck": {
      "Active": {
        "Enabled": "true",
        "Interval": "00:00:10",
        "Timeout": "00:00:10",
        "Policy": "ConsecutiveFailures",
        "Path": "/api/health",
        "Query": "?foo=bar"
      }
    },
    "Metadata": {
      "ConsecutiveFailuresHealthPolicy.Threshold": "3"
    },
    "Destinations": {
      "cluster1/destination1": {
        "Address": "https://localhost:10000/"
      },
      "cluster1/destination2": {
        "Address": "http://localhost:10010/",
        "Health": "http://localhost:10020/"
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Passive health checks
&lt;/h3&gt;

&lt;p&gt;YARP can passively watch for successes and failures in client request proxying to reactively evaluate destination health states.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Clusters": {
  "cluster1": {
    "HealthCheck": {
      "Passive": {
        "Enabled": "true",
        "Policy": "TransportFailureRate",
        "ReactivationPeriod": "00:02:00"
      }
    },
    "Metadata": {
      "TransportFailureRateHealthPolicy.RateLimit": "0.5"
    },
    "Destinations": {
      "cluster1/destination1": {
        "Address": "https://localhost:10000/"
      },
      "cluster1/destination2": {
        "Address": "http://localhost:10010/"
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a destination gets marked as unhealthy, it stops receiving new requests until it gets reactivated after a configured period.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Rate Limiting
&lt;/h2&gt;

&lt;p&gt;The reverse proxy can be used to rate-limit requests before they are proxied to the destination servers. This can reduce load on the destination servers, add a layer of protection, and ensure consistent policies are implemented across your applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global Limiter
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;globalLimiter&lt;/code&gt; is an illustration here, we need to implement it by ourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services.AddRateLimiter(options =&amp;gt; options.GlobalLimiter = globalLimiter);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Route Limiter
&lt;/h3&gt;

&lt;p&gt;Rate limiter policies can be specified per route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "ReverseProxy": {
    "Routes": {
      "route1" : {
        "ClusterId": "cluster1",
        "RateLimiterPolicy": "customPolicy",
        "Match": {
          "Hosts": [ "localhost" ]
        }
      }
    },
    "Clusters": {
      "cluster1": {
        "Destinations": {
          "cluster1/destination1": {
            "Address": "https://localhost:10001/"
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RateLimiter policies can be configured in services as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services.AddRateLimiter(options =&amp;gt;
{
    options.AddFixedWindowLimiter("customPolicy", opt =&amp;gt;
    {
        opt.PermitLimit = 4;
        opt.Window = TimeSpan.FromSeconds(12);
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 2;
    });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add the RateLimiter middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.UseRateLimiter();

app.MapReverseProxy();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Disable Rate Limiting
&lt;/h3&gt;

&lt;p&gt;Specifying the value &lt;code&gt;disable&lt;/code&gt; in a route's &lt;code&gt;RateLimiterPolicy&lt;/code&gt; parameter means the rate limiter middleware will not apply any policies to this route, even the default policy.&lt;/p&gt;

</description>
      <category>microservices</category>
      <category>dotnet</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>.NET Learning Notes: How to use Redis in ASP.NET</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 04 Aug 2025 08:23:39 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-how-to-use-redis-in-aspnet-3if</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-how-to-use-redis-in-aspnet-3if</guid>
      <description>&lt;h4&gt;
  
  
  1. Install NuGet package
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet add package StackExchange.Redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Configuration connection string
&lt;/h4&gt;

&lt;p&gt;Here, I config it in appsettings.json&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  "ConnectionStrings": {
    "Redis": "localhost:6379"
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Configure connectiong string in Program.cs
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddSingleton&amp;lt;IConnectionMultiplexer&amp;gt;(sp =&amp;gt;
{
    var redisCon = builder.Configuration.GetConnectionString("Redis")!;
    var configuration = ConfigurationOptions.Parse(redisCon, true);
    configuration.ResolveDns = true;
    return ConnectionMultiplexer.Connect(configuration);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4. DI IConnectionMultiplexer to service
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class BasicJwtTokenValidationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly List&amp;lt;string&amp;gt; _authWhiteList;
    private readonly IConnectionMultiplexer _redis;

    public BasicJwtTokenValidationMiddleware(RequestDelegate next, 
        IOptions&amp;lt;AuthenticationOptions&amp;gt; authOptions,
        IConnectionMultiplexer redis)
    {
        _next = next;
        _authWhiteList = authOptions.Value.AuthWhiteList;
        _redis = redis;
    }

    public Task Invoke(HttpContext context)
    {
      // *****
      var redisVersion = await _redis.GetDatabase().StringGetAsync($"jwt:version:{userPublicId}");

      // ***
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  5. How to mock it in unit test (use AutoFixture + Moq)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Theory, AutoMoqData]
public async Task IsValidVersionAsync_ReturnsTrue_WhenVersionMatches(
    [Frozen] Mock&amp;lt;IConnectionMultiplexer&amp;gt; redisMock,
    [Frozen] Mock&amp;lt;IDatabase&amp;gt; dbMock)
{
    // Arrange
    var userId = "user-123";
    var version = "3";

    dbMock.Setup(db =&amp;gt; db.StringGetAsync($"jwt:version:{userId}", It.IsAny&amp;lt;CommandFlags&amp;gt;()))
        .ReturnsAsync(version);

    redisMock.Setup(r =&amp;gt; r.GetDatabase(It.IsAny&amp;lt;int&amp;gt;(), null))
        .Returns(dbMock.Object);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why do we mock GetDatabase with parameters even though we call it without parameters in code?
&lt;/h4&gt;

&lt;p&gt;Answer: Because GetDatabase uses default parameter values in C#;&lt;br&gt;
Method definition of GetDatabase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IDatabase GetDatabase(int db = -1, object? asyncState = null);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, if we mock with our parameters like:&lt;code&gt;redisMock.Setup(c =&amp;gt; c.GetDatabase())&lt;/code&gt;. It won't match the actual call to &lt;code&gt;GetDatabase(-1, null)&lt;/code&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>.NET Learning Notes: Using MailHog with .NET for Email Testing</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sun, 06 Jul 2025 10:09:54 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-using-mailhog-with-net-for-email-testing-op4</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-using-mailhog-with-net-for-email-testing-op4</guid>
      <description>&lt;p&gt;MailHog is a simple and powerful tool for capturing and viewing emails sent from your application during development or testing. It acts as a local SMTP server and provides a web interface for viewing the captured messages. This blog explains how to use MailHog with a .NET project for email sending and testing, and how to integrate it into CI pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Use MailHog?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Captures emails sent from development apps.&lt;/li&gt;
&lt;li&gt;Web UI for viewing emails at &lt;code&gt;http://localhost:8025&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Excellent for integration testing.&lt;/li&gt;
&lt;li&gt;No real emails sent — safe for all environments.&lt;/li&gt;
&lt;li&gt;Free!!&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Running MailHog with Docker
&lt;/h2&gt;

&lt;p&gt;The easiest way to start MailHog is with Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 1025:1025 &lt;span class="nt"&gt;-p&lt;/span&gt; 8025:8025 mailhog/mailhog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;SMTP Server: &lt;code&gt;localhost:1025&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Web UI: &lt;code&gt;http://localhost:8025&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or with a &lt;code&gt;docker-compose.mailhog.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mailhog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mailhog/mailhog&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1025:1025"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8025:8025"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.mailhog.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Sending Email with .NET and MailHog
&lt;/h2&gt;

&lt;p&gt;Here’s how you can configure and use MailHog to send test emails in a .NET service.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. SMTP Options
&lt;/h3&gt;

&lt;p&gt;You can configure MailHog with no credentials and unencrypted SMTP:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;appsettings.Development.json&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;"Smtp"&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;"Host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"User"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"Password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The Email Service Class
&lt;/h3&gt;

&lt;p&gt;This service uses &lt;code&gt;MailKit&lt;/code&gt; and &lt;code&gt;MimeKit&lt;/code&gt; to send email messages:&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;SmtpEmailService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEmailService&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;SmtpOptions&lt;/span&gt; &lt;span class="n"&gt;_smtpOptions&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;SmtpEmailService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;smtpOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpEmailService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_smtpOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smtpOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="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;SendEmailAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailSendRequestedEvent&lt;/span&gt; &lt;span class="n"&gt;emailEvent&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;message&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;MimeMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MailboxAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"FinTrack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"no-reply@fintrack.com"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MailboxAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emailEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToName&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;emailEvent&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;emailEvent&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsHtml&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;TextPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TextPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"plain"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&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;SmtpClient&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;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_smtpOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_smtpOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SecureSocketOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&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;AuthenticateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_smtpOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_smtpOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&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;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="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;DisconnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;SecureSocketOptions.None&lt;/code&gt; because MailHog does not support SSL or STARTTLS.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Unit Test Example
&lt;/h3&gt;

&lt;p&gt;Use mock &lt;code&gt;IOptions&lt;/code&gt; and &lt;code&gt;ILogger&lt;/code&gt;, then run a real test that talks to MailHog:&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;Fact&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;SendAsync_WithValidInput_SendsEmailWithoutException&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;smtpOptions&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;SmtpOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Password&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;optionsMock&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;optionsMock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&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="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smtpOptions&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;loggerMock&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpEmailService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;emailService&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;SmtpEmailService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optionsMock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loggerMock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&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;emailEvent&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;EmailSendRequestedEvent&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;From&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"from@test.com"&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="s"&gt;"to@test.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ToName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Receiver"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Subject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test Subject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test Body"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IsHtml&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;emailService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendEmailAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emailEvent&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;Once this test runs, visit &lt;code&gt;http://localhost:8025&lt;/code&gt; and you’ll see the message inside MailHog's web UI. No real emails will be sent — perfect for development and CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Can You Set Password for MailHog?
&lt;/h2&gt;

&lt;p&gt;MailHog does &lt;strong&gt;not support SMTP authentication&lt;/strong&gt; or TLS encryption. It’s intentionally insecure for local use only.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI Integration
&lt;/h2&gt;

&lt;p&gt;You can easily run MailHog inside GitHub Actions with this snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mailhog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mailhog/mailhog&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;1025:1025&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;8025:8025&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then point your tests to &lt;code&gt;localhost:1025&lt;/code&gt;, and run them just like local.&lt;/p&gt;




&lt;p&gt;MailHog is a powerful and easy-to-use tool for safely testing email logic without sending real emails. It's perfect for local development, integration testing, and even CI environments.&lt;/p&gt;

</description>
      <category>mailhog</category>
      <category>dotnet</category>
      <category>email</category>
      <category>testing</category>
    </item>
    <item>
      <title>.NET Learning Notes: Implementing EventBus with CAP and RabbitMQ in Microservices</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Sun, 29 Jun 2025 09:19:11 +0000</pubDate>
      <link>https://dev.to/alexleeeeeeeeee/net-learning-notes-implementing-eventbus-with-cap-and-rabbitmq-in-microservices-2e</link>
      <guid>https://dev.to/alexleeeeeeeeee/net-learning-notes-implementing-eventbus-with-cap-and-rabbitmq-in-microservices-2e</guid>
      <description>&lt;p&gt;&lt;a href="https://cap.dotnetcore.xyz/user-guide/en/getting-started/quick-start/" rel="noopener noreferrer"&gt;CAP Doc&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What Is CAP and Why Do We Need It?
&lt;/h2&gt;

&lt;p&gt;In microservices architectures, services often need to communicate with each other asynchronously to remain decoupled, scalable, and fault-tolerant. Event-driven communication using message queues like RabbitMQ or Kafka is a popular solution — but it comes with its own set of challenges, especially around reliability and distributed transaction consistency.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;CAP (Distributed Transaction Solution in .NET Core)&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAP&lt;/strong&gt; is an open-source .NET library designed to help developers manage reliable event publishing and consumption across services, while also handling &lt;strong&gt;eventual consistency&lt;/strong&gt; in database transactions. It follows the &lt;strong&gt;Outbox Pattern&lt;/strong&gt;, allowing you to store events in your local database as part of a transaction and forward them to a message queue only after the transaction succeeds.&lt;/p&gt;

&lt;h4&gt;
  
  
  Problems CAP Solves
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distributed Transaction Complexity&lt;/strong&gt;: Ensures that data changes and event publishing happen atomically, without requiring a distributed transaction coordinator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Loss Prevention&lt;/strong&gt;: Persist events locally before sending, so that messages are not lost even if the broker or consumer service is temporarily down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry &amp;amp; Monitoring&lt;/strong&gt;: Built-in retry logic and a web dashboard for tracking sent and received messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With just a few lines of configuration, CAP lets your services publish and consume events reliably, while abstracting away the complexities of queue connection, message delivery, error handling, and storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. How to Integrate CAP in a .NET Microservice
&lt;/h2&gt;

&lt;p&gt;In this section, we'll cover how to add CAP to your .NET microservice, including the required NuGet packages, how to configure RabbitMQ and the database, and the meaning of the &lt;code&gt;DefaultGroup&lt;/code&gt; setting for subscribers.&lt;/p&gt;




&lt;h3&gt;
  
  
  2.1 Install Required Packages
&lt;/h3&gt;

&lt;p&gt;You need to install the following NuGet packages:&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;EF Core + RabbitMQ&lt;/strong&gt; setup:&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 DotNetCore.CAP
dotnet add package DotNetCore.CAP.RabbitMQ
dotnet add package DotNetCore.CAP.EntityFrameworkCore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2.2 Configure CAP in Program.cs
&lt;/h3&gt;

&lt;p&gt;First, make sure your &lt;code&gt;DbContext&lt;/code&gt; is already registered with EF Core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseMySql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DefaultConnection"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;ServerVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AutoDetect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DefaultConnection"&lt;/span&gt;&lt;span class="p"&gt;))));&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseEntityFramework&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Use EF Core to store CAP events&lt;/span&gt;

    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseRabbitMQ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&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;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HostName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CAP:RabbitMQ:HostName"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CAP:RabbitMQ:UserName"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CAP:RabbitMQ:Password"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2.3 Configuration File
&lt;/h3&gt;

&lt;p&gt;You can store the CAP configuration in &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"CAP"&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;"DefaultGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order.service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"RabbitMQ"&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;"HostName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UserName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"guest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"guest"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2.4 What Is DefaultGroup?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;DefaultGroup&lt;/code&gt; defines the &lt;strong&gt;consumer group name&lt;/strong&gt; for this service. It's used to distinguish multiple subscribers of the same topic in different services. CAP ensures that &lt;strong&gt;only one subscriber within the same group&lt;/strong&gt; will consume the message.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Services with &lt;strong&gt;different group names&lt;/strong&gt; can receive the same event (fan-out).&lt;/li&gt;
&lt;li&gt;Services with the &lt;strong&gt;same group name&lt;/strong&gt; will compete to consume the event (load balancing).&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2.5 Do Publisher and Subscriber Use the Same Configuration?
&lt;/h3&gt;

&lt;p&gt;Yes, both the sender and receiver services use the same basic CAP configuration. The only difference is that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;publisher&lt;/strong&gt; uses &lt;code&gt;ICapPublisher&lt;/code&gt; to send messages.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;subscriber&lt;/strong&gt; uses the &lt;code&gt;[CapSubscribe]&lt;/code&gt; attribute to receive messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both sides must be properly configured with the same transport (e.g., RabbitMQ) and the same storage (e.g., MySQL via EF Core) to work together.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Running RabbitMQ with Docker (Local Setup)
&lt;/h2&gt;

&lt;p&gt;To enable CAP to work properly, we need a running instance of a message broker. RabbitMQ is one of the supported brokers and works well for local development and testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Pull the Official RabbitMQ Image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull rabbitmq:3-management
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This image includes the RabbitMQ server &lt;strong&gt;and&lt;/strong&gt; the Management UI plugin.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Run the RabbitMQ Container
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hostname&lt;/span&gt; rabbitmq &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; rabbitmq &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5672:5672 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 15672:15672 &lt;span class="se"&gt;\&lt;/span&gt;
  rabbitmq:3-management
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Explanation:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--name rabbitmq&lt;/code&gt;: Names the container so you can manage it easily.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--hostname rabbitmq&lt;/code&gt;: Sets the hostname inside the container (optional).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-p 5672:5672&lt;/code&gt;: Exposes the RabbitMQ AMQP port.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-p 15672:15672&lt;/code&gt;: Exposes the RabbitMQ Management UI port.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-d&lt;/code&gt;: Runs the container in detached mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Access the RabbitMQ Management UI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Open your browser and go to: &lt;a href="http://localhost:15672" rel="noopener noreferrer"&gt;http://localhost:15672&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Default login credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Username:&lt;/strong&gt; &lt;code&gt;guest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password:&lt;/strong&gt; &lt;code&gt;guest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;With just two commands (&lt;code&gt;pull&lt;/code&gt; and &lt;code&gt;run&lt;/code&gt;), you’ll have a fully functional RabbitMQ server running locally, along with a powerful UI for inspecting exchanges, queues, and messages — ideal for working with CAP.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Publish and Subscribe with CAP
&lt;/h2&gt;

&lt;p&gt;In the &lt;strong&gt;sender service&lt;/strong&gt;, inject &lt;code&gt;ICapPublisher&lt;/code&gt; and use it to publish 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;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&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;ICapPublisher&lt;/span&gt; &lt;span class="n"&gt;_capBus&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;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ICapPublisher&lt;/span&gt; &lt;span class="n"&gt;capBus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_capBus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capBus&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;PlaceOrderAsync&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;_capBus&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="s"&gt;"order.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;99.9&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;In the &lt;strong&gt;receiver service&lt;/strong&gt;, add a method with &lt;code&gt;[CapSubscribe]&lt;/code&gt; to handle the 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;class&lt;/span&gt; &lt;span class="nc"&gt;OrderEventHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CapSubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order.created"&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;HandleOrderCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;dynamic&lt;/span&gt; &lt;span class="n"&gt;data&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;$"Order received: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Amount: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Both services need to be connected to the same message broker and configured with CAP.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. CAP Message Tables
&lt;/h2&gt;

&lt;p&gt;CAP relies on two key tables in your database to ensure message reliability and traceability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;cap.published&lt;/code&gt;&lt;/strong&gt;: This table stores all the messages that have been published. When a message is sent via &lt;code&gt;ICapPublisher&lt;/code&gt;, it first gets recorded here. CAP will attempt to deliver this message based on the retry policy if the subscriber is not ready.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;cap.received&lt;/code&gt;&lt;/strong&gt;: Once a message is successfully consumed by a subscriber, it will be recorded in this table. This acts as a log of all successfully handled messages.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're using in-memory storage instead of a real database, these records will not be persisted after a service restart.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CAP ensures that messages are reliably transferred and tracked between microservices using this storage mechanism.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  6. Why CAP Is Powerful
&lt;/h2&gt;

&lt;p&gt;One of the most powerful features of CAP is its simplicity.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You &lt;strong&gt;only need to configure&lt;/strong&gt; the database and message broker once, and CAP will automatically handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reliable message delivery&lt;/li&gt;
&lt;li&gt;Retry mechanisms&lt;/li&gt;
&lt;li&gt;Failure tracking&lt;/li&gt;
&lt;li&gt;Distributed transaction support (Outbox pattern)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;No extra queue management&lt;/strong&gt; or manual retry logic is needed.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;With just &lt;code&gt;[CapSubscribe]&lt;/code&gt; and &lt;code&gt;ICapPublisher&lt;/code&gt;, microservices can communicate reliably and efficiently.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;CAP abstracts away much of the complexity of building a robust EventBus system. Once configured, your services can send and receive events without worrying about infrastructure details.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
  </channel>
</rss>
