<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged core</title>
<link>https://coderlegion.com/tag/core</link>
<description>Powered by Question2Answer</description>
<item>
<title>How to Fix Core Web Vitals: LCP, CLS &amp; INP | FloxoLab</title>
<link>https://coderlegion.com/25677/how-to-fix-core-web-vitals-lcp-cls-inp-floxolab</link>
<description>
&lt;p&gt;
        Core Web Vitals are three numbers Google uses to measure how a page feels to a real person: how fast the main content loads, whether the layout jumps around while it does, and how quickly the page reacts when you tap or click. They are one part of page experience, which Google says its core ranking systems seek to reward. They do not override relevance or useful content.
      &lt;/p&gt;
      &lt;p&gt;
        This guide explains each of the three, plus the supporting numbers you will see in the same report, and then shows the real fixes that took this site's mobile Lighthouse performance score from 77 to 98. Nothing here is theory. It is the same work I run on client sites in an &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo-audit&quot;&gt;SEO audit&lt;/a&gt;.
      &lt;/p&gt;

      &lt;h2 id=&quot;what-a-good-result-looks-like&quot;&gt;What a good result looks like&lt;/h2&gt;
      &lt;p&gt;
        In the July 2026 PageSpeed Insights test shown below, this site scored 98 on mobile and 100 on desktop using the same code. The two tests look different because mobile is run on a throttled, slower simulated device, so it always has less room to work with. Mobile is often the harder target, and it deserves priority when most of your visitors use phones. Google measures mobile and desktop field data separately, so check both instead of treating one Lighthouse score as the final answer.
      &lt;/p&gt;

      &lt;div class=&quot;figure-pair&quot;&gt;
        &lt;figure class=&quot;article-figure&quot;&gt;
          &lt;img width=&quot;980&quot; height=&quot;891&quot; src=&quot;https://floxolab.com/uploads/seo/guides/how-to-fix-core-web-vitals/mobile.webp?v=20260707&quot; alt=&quot;PageSpeed Insights mobile report for this site: performance 98, LCP 1.9 seconds, CLS 0, total blocking time 40 milliseconds, all green&quot;&gt;
          &lt;figcaption&gt;Mobile: performance 98, LCP 1.9s, CLS 0, all vitals in the green.&lt;/figcaption&gt;
        &lt;/figure&gt;
        &lt;figure class=&quot;article-figure&quot;&gt;
          &lt;img width=&quot;979&quot; height=&quot;887&quot; src=&quot;https://floxolab.com/uploads/seo/guides/how-to-fix-core-web-vitals/desktop.webp?v=20260707&quot; alt=&quot;PageSpeed Insights desktop report for this site: performance 100, LCP 0.5 seconds, CLS 0, and speed index 0.4 seconds&quot;&gt;
          &lt;figcaption&gt;Desktop: performance 100, LCP 0.5s, CLS 0, speed index 0.4s.&lt;/figcaption&gt;
        &lt;/figure&gt;
      &lt;/div&gt;

      &lt;h2 id=&quot;the-three-core-web-vitals-one-at-a-time&quot;&gt;The three Core Web Vitals, one at a time&lt;/h2&gt;
      &lt;p&gt;
        Everything on the scoreboard comes down to three real-user metrics. A page passes when 75 percent of real visitors fall inside the &quot;good&quot; band for each.
      &lt;/p&gt;

      &lt;div class=&quot;article-table-wrap&quot;&gt;
        &lt;table class=&quot;article-table&quot;&gt;
          &lt;thead&gt;
            &lt;tr&gt;
              &lt;th&gt;Metric&lt;/th&gt;
              &lt;th&gt;Measures&lt;/th&gt;
              &lt;th&gt;Good&lt;/th&gt;
              &lt;th&gt;Poor&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            &lt;tr&gt;
              &lt;td&gt;LCP (Largest Contentful Paint)&lt;/td&gt;
              &lt;td&gt;Loading&lt;/td&gt;
              &lt;td&gt;2.5s or less&lt;/td&gt;
              &lt;td&gt;Over 4s&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr&gt;
              &lt;td&gt;CLS (Cumulative Layout Shift)&lt;/td&gt;
              &lt;td&gt;Visual stability&lt;/td&gt;
              &lt;td&gt;0.1 or less&lt;/td&gt;
              &lt;td&gt;Over 0.25&lt;/td&gt;
            &lt;/tr&gt;
            &lt;tr&gt;
              &lt;td&gt;INP (Interaction to Next Paint)&lt;/td&gt;
              &lt;td&gt;Responsiveness&lt;/td&gt;
              &lt;td&gt;200ms or less&lt;/td&gt;
              &lt;td&gt;Over 500ms&lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;

      &lt;h3&gt;LCP: how fast the main thing loads&lt;/h3&gt;
      &lt;p&gt;
        Largest Contentful Paint is the moment the biggest element in view finishes rendering, usually the hero image or the headline block. It is the reader's gut sense of &quot;has this page loaded yet&quot;. What drags it down is nearly always one of four things: a heavy, uncompressed hero image, a slow server or host, render-blocking CSS and JavaScript that must run before anything paints, and web fonts that arrive late.
      &lt;/p&gt;
      &lt;p&gt;
        The fixes follow the causes. Compress and serve the hero as WebP at the right size, and preload it so the browser fetches it early. Use srcset so a phone pulls a small version and a desktop a larger one, instead of shipping one heavy file to every device. That is a separate lever from preloading: preload fetches the image sooner, srcset makes sure it is the right weight, and on a slow mobile connection the srcset part is often what saves the most. Cut or defer render-blocking scripts, and self-host your fonts instead of pulling them from a third-party domain. On this site LCP is 1.9s on mobile and 0.5s on desktop, and the single biggest lever was the hero image: right format, right size per device, preloaded.
      &lt;/p&gt;

      &lt;h3&gt;CLS: whether the page holds still&lt;/h3&gt;
      &lt;p&gt;
        Cumulative Layout Shift measures how much the content jumps around as the page loads. You have felt it: you go to tap a link, an image or banner loads above it, and the whole page shoves down. The usual culprits are images and embeds with no width and height set, ad or banner slots injected after load, and fonts that swap and reflow the text.
      &lt;/p&gt;
      &lt;p&gt;
        This is the most fixable vital, and the cheapest win on the list. Put explicit width and height (or an aspect ratio) on every image and iframe so the browser reserves the space before the file arrives. Give injected elements a fixed slot. Set font-display so a font swap does not reshuffle the layout. This site sits at a CLS of 0, and it got there almost entirely by adding size attributes to images.
      &lt;/p&gt;

      &lt;h3&gt;INP: how fast the page answers you&lt;/h3&gt;
      &lt;p&gt;
        Interaction to Next Paint replaced First Input Delay in March 2024. It measures the delay between you doing something, a tap, a click, a keypress, and the page actually showing a response, taking close to the worst interaction across the whole visit. A high INP is a page that feels sluggish and unresponsive, and the cause is almost always JavaScript hogging the main thread: heavy frameworks, big third-party scripts, and long tasks that block the browser from reacting. It is also the vital the most sites fail, so it is worth the attention.
      &lt;/p&gt;
      &lt;p&gt;
        The fix is to give the main thread less to do. Defer or split heavy JavaScript, hold third-party scripts off the critical path until the user needs them, and break long tasks into smaller pieces. This site now runs without client-side behavioral analytics, which removes one common source of third-party main-thread work. On sites that need analytics or other third-party scripts, keep them off the critical path and verify that delayed loading does not break consent or measurement. In the lab result shown above, total blocking time, the lab stand-in for INP, was 40 milliseconds.
      &lt;/p&gt;

      &lt;h2 id=&quot;the-other-numbers-in-the-report&quot;&gt;The other numbers in the report&lt;/h2&gt;
      &lt;p&gt;
        The same Lighthouse report shows three more metrics. These are lab diagnostics, useful for finding problems, but they are not the vitals Google ranks on.
      &lt;/p&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;strong&gt;First Contentful Paint (FCP).&lt;/strong&gt; When the first text or image appears. An early signal that something is happening.&lt;/li&gt;
        &lt;li&gt;&lt;strong&gt;Total Blocking Time (TBT).&lt;/strong&gt; How long the main thread was blocked during load. It is the lab proxy for INP, so lowering TBT usually lowers INP for real users too.&lt;/li&gt;
        &lt;li&gt;&lt;strong&gt;Speed Index.&lt;/strong&gt; How quickly the page visibly fills in. On this site it shows 3.5s on mobile and lands in the amber band, while every actual vital is green. That is a fair reminder that the overall score is a blend, and a single amber lab number is not worth panicking over when the real-user vitals all pass.&lt;/li&gt;
      &lt;/ul&gt;

      &lt;h2 id=&quot;lab-data-and-field-data-are-not-the-same&quot;&gt;Lab data and field data are not the same&lt;/h2&gt;
      &lt;p&gt;
        There are two ways to measure all of this, and mixing them up causes a lot of confusion. &lt;strong&gt;Lab data&lt;/strong&gt; is a single simulated test, like the Lighthouse run in the screenshots above, on a fixed device and connection. It is repeatable and great for debugging. &lt;strong&gt;Field data&lt;/strong&gt; is what real Chrome users actually experienced over the last 28 days, and it is what Google uses for ranking. You can see your own field data in the Core Web Vitals report inside Search Console.
      &lt;/p&gt;
      &lt;p&gt;
        The practical order: use the lab test to find and fix problems fast, then watch the field data in Search Console to confirm real users feel the change. A lab score can be perfect while the field still lags, especially on a page that only just got fixed, because the field window is a rolling average. Reading that report is part of the wider habit covered in &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo/guides/how-to-read-your-monthly-seo-report&quot;&gt;how to read your monthly SEO report&lt;/a&gt;.
      &lt;/p&gt;
      &lt;p&gt;
        When performance is only one concern on a specific URL, place this diagnosis inside an &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo/guides/on-page-seo-audit-checklist&quot;&gt;on-page audit of its indexability, search task, content, links, media, and validation&lt;/a&gt;. That keeps a Lighthouse number from replacing the wider page decision.
      &lt;/p&gt;
      &lt;p&gt;
        A mobile performance score also does not prove that Googlebot and customers receive complete content, usable navigation, accessible controls, or a working form. Check those separately with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo/guides/mobile-seo-checklist&quot;&gt;mobile SEO checklist for rendering, content parity, and real phone tasks&lt;/a&gt;.
      &lt;/p&gt;
      &lt;p&gt;
        If the site has not launched yet, set media, script, layout-stability, and representative-template budgets in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo/guides/seo-friendly-web-design-checklist&quot;&gt;SEO-friendly web design checklist&lt;/a&gt;. Performance is cheaper to protect in the design contract than to recover after every component is approved.
      &lt;/p&gt;

      &lt;h2 id=&quot;what-actually-moved-this-site-from-77-to-98&quot;&gt;What actually moved this site from 77 to 98&lt;/h2&gt;
      &lt;p&gt;
        The jump was not one clever trick. It was a short list of unglamorous fixes, each aimed at a specific metric:
      &lt;/p&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;strong&gt;Self-hosted the fonts.&lt;/strong&gt; Pulling fonts from a third-party domain adds a connection and a render delay. Serving them from the same domain, preloaded, cut straight into LCP.&lt;/li&gt;
        &lt;li&gt;&lt;strong&gt;Reduced third-party JavaScript.&lt;/strong&gt; The current site does not load client-side behavioral analytics. Removing or deferring non-essential third-party scripts reduces main-thread competition; verify consent and measurement before changing a production setup.&lt;/li&gt;
        &lt;li&gt;&lt;strong&gt;Responsive images in WebP.&lt;/strong&gt; Every image ships as WebP at several sizes with srcset, so a phone downloads a small file and a desktop a larger one, never the wrong one.&lt;/li&gt;
        &lt;li&gt;&lt;strong&gt;Width and height on everything.&lt;/strong&gt; Explicit dimensions on images and figures reserve their space, which is why CLS is 0.&lt;/li&gt;
      &lt;/ul&gt;
      &lt;p&gt;
        None of that touched the design or the content. It is exactly the kind of finite, technical work that a small site needs once, not every month, which is the point I make in &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo/guides/technical-seo-fixes-agencies-wont-touch&quot;&gt;the technical SEO fixes agencies won't touch&lt;/a&gt;.
      &lt;/p&gt;

      &lt;h2 id=&quot;a-quick-checklist&quot;&gt;A quick checklist&lt;/h2&gt;
      &lt;ul&gt;
        &lt;li&gt;Compress the hero image, serve it as WebP, and preload it.&lt;/li&gt;
        &lt;li&gt;Set width and height on every image, iframe, and embed.&lt;/li&gt;
        &lt;li&gt;Self-host fonts and set font-display so text does not reflow.&lt;/li&gt;
        &lt;li&gt;Remove unnecessary third-party scripts and defer the ones the site genuinely needs.&lt;/li&gt;
        &lt;li&gt;Test in the lab with PageSpeed Insights, then confirm in Search Console field data.&lt;/li&gt;
      &lt;/ul&gt;

      &lt;div class=&quot;article-callout&quot;&gt;
        &lt;p&gt;
          Core Web Vitals are one part of a technical SEO check, alongside indexing, schema, and titles. If you would rather have someone measure yours, find what is dragging them down, and hand you a prioritized fix list, that is exactly what the &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo-audit&quot;&gt;fixed-price SEO audit&lt;/a&gt; does: one report, a flat $150, no retainer, no ranking promises. If the work turns out to be ongoing, the &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo/retainer&quot;&gt;monthly retainer&lt;/a&gt; picks it up from there. &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/#contact&quot;&gt;Get in touch&lt;/a&gt; to start.
        &lt;/p&gt;
      &lt;/div&gt;

      &lt;h2 id=&quot;frequently-asked-questions&quot;&gt;Frequently asked questions&lt;/h2&gt;

      &lt;h3&gt;What are Core Web Vitals?&lt;/h3&gt;
      &lt;p&gt;
        Three metrics Google uses to measure real-world page experience: Largest Contentful Paint (LCP) for loading, Cumulative Layout Shift (CLS) for visual stability, and Interaction to Next Paint (INP) for responsiveness. They come from real Chrome users and feed into ranking as a page experience signal.
      &lt;/p&gt;

      &lt;h3&gt;What is a good Core Web Vitals score?&lt;/h3&gt;
      &lt;p&gt;
        A page passes when 75 percent of real users see LCP at 2.5s or less, CLS at 0.1 or less, and INP at 200ms or less. In the Lighthouse lab test the overall score is good from 90 to 100. In the recorded July 2026 test, this site scored 98 on mobile and 100 on desktop.
      &lt;/p&gt;

      &lt;h3&gt;Why is my mobile score lower than desktop?&lt;/h3&gt;
      &lt;p&gt;
        Lighthouse tests mobile on a throttled, slower simulated device and connection, so the same page has less CPU and bandwidth to work with. Mobile is often the harder target, and it deserves priority when most of your visitors use phones. Google measures mobile and desktop field data separately, so check both instead of treating one Lighthouse score as the final answer.
      &lt;/p&gt;

      &lt;h3&gt;Do Core Web Vitals affect SEO rankings?&lt;/h3&gt;
      &lt;p&gt;
        Google says Core Web Vitals, together with other aspects of page experience, align with what its core ranking systems seek to reward. They are not a separate shortcut to higher rankings, and good scores do not override relevance or useful content.
      &lt;/p&gt;

      &lt;h2 id=&quot;sources-checked&quot;&gt;Sources checked&lt;/h2&gt;
      &lt;ul&gt;
        &lt;li&gt;&lt;a href=&quot;https://web.dev/articles/vitals&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;web.dev: Web Vitals and the Core Web Vitals thresholds&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;https://developers.google.com/search/docs/appearance/page-experience&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;Google Search Central: page experience and Core Web Vitals in ranking&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;https://pagespeed.web.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&gt;PageSpeed Insights: the lab and field report used above&lt;/a&gt;&lt;/li&gt;
      &lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://floxolab.com/seo/guides/how-to-fix-core-web-vitals&quot;&gt;FloxoLab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/25677/how-to-fix-core-web-vitals-lcp-cls-inp-floxolab</guid>
<pubDate>Fri, 16 Oct 2026 15:50:00 +0000</pubDate>
</item>
<item>
<title>What Did We Lose With Abundant RAM?</title>
<link>https://coderlegion.com/22002/what-did-we-lose-with-abundant-ram</link>
<description>
&lt;h2 id=&quot;reclaiming-efficiency-a-tutorial-on-the-core-memory-mindset&quot;&gt;Reclaiming Efficiency: A Tutorial on the Core Memory Mindset&lt;/h2&gt;

&lt;p&gt;In an era defined by terabytes of RAM, multi-core processors, and infinitely scalable cloud infrastructure, the concept of &amp;ldquo;resource scarcity&amp;rdquo; often feels like a relic of a bygone era. We build sophisticated systems atop layers of abstraction, confident that hardware will simply catch up or cloud providers will scale away any inefficiencies. But what if this abundance has led to a subtle, yet significant, loss in fundamental engineering discipline? This tutorial delves into the &amp;ldquo;core memory mindset&amp;rdquo; of early computing&amp;mdash;a world where every byte and every clock cycle was meticulously accounted for&amp;mdash;and explores how its principles remain profoundly relevant for today&amp;rsquo;s bloated binaries and skyrocketing cloud bills.&lt;/p&gt;

&lt;h3 id=&quot;introduction-the-ghosts-of-mainframes-past&quot;&gt;Introduction: The Ghosts of Mainframes Past&lt;/h3&gt;

&lt;p&gt;Imagine the 1960s: computing was physical, tangible. Magnetic core memory wasn&amp;rsquo;t a virtual concept; it was a woven grid of tiny ferrite donuts, each a literal magnetic flip representing a bit. Debugging could involve probing these physical components. This wasn&amp;rsquo;t merely a technical constraint; it fostered a unique engineering culture. Programmers weren&amp;rsquo;t just coding software; they were practically sculpting logic from raw physics. Their mental models of execution were tactile, almost mechanical, driven by an intimate, often painful, understanding of hardware limitations.&lt;/p&gt;

&lt;p&gt;This isn&amp;rsquo;t just nostalgia. It&amp;rsquo;s a stark contrast to our current reality, where resource leaks are often just &amp;ldquo;scaled away,&amp;rdquo; and performance bottlenecks are met with &amp;ldquo;add more RAM.&amp;rdquo; The core memory mindset wasn&amp;rsquo;t about deprivation; it was about precision, foresight, and a profound respect for limited resources. By examining this approach, we can uncover valuable lessons that can inform our modern development practices, leading to more efficient, robust, and sustainable software.&lt;/p&gt;

&lt;h3 id=&quot;conceptual-walkthrough-practicing-the-core-memory-mindset&quot;&gt;Conceptual Walkthrough: Practicing the Core Memory Mindset&lt;/h3&gt;

&lt;p&gt;Applying the core memory mindset today doesn&amp;rsquo;t mean reverting to assembly language or manually threading wires. Instead, it&amp;rsquo;s a conceptual &amp;ldquo;code layout&amp;rdquo; &amp;ndash; a way of thinking about your software&amp;rsquo;s interaction with underlying resources, regardless of the language or platform. Let&amp;rsquo;s walk through how this mindset translates into actionable principles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Principle 1: Extreme Data Economy &amp;ndash; Every Bit Counts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the core memory era, data representation was a meticulous art. There was no luxury for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HashMap&amp;lt;String, Object&amp;gt;&lt;/code&gt; for simple configurations or redundant data structures.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Modern Tendency:&lt;/strong&gt; Storing boolean flags as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bool&lt;/code&gt; (often occupying a full byte or word), using large objects for simple values, or relying on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;String&lt;/code&gt; keys for small lookup tables.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Core Memory Approach:&lt;/strong&gt; Bit packing. If you have several boolean flags, pack them into a single byte. Use fixed-size integers (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint8&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int16&lt;/code&gt;) where appropriate. Pre-calculate and store lookup tables instead of runtime computation. Represent complex states with carefully crafted bitmasks.&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// Conceptual Pseudocode: Modern vs. Core Memory Data Representation

// Modern (potentially inefficient)
class UserPreferences {
    bool sendEmailNotifications;
    bool enableDarkMode;
    int  themeId; // Often int takes 4 bytes
    // ... many other individual fields
}

// Core Memory Mindset (efficient packing)
// Assume 8 bits per byte
byte userPreferencesFlags; // A single byte for many flags
const byte FLAG_SEND_EMAIL     = 0x01; // 00000001
const byte FLAG_DARK_MODE      = 0x02; // 00000010
const byte THEME_ID_MASK       = 0x0C; // 00001100 (2 bits for 4 themes)
const byte THEME_ID_SHIFT      = 2;

// To set/check:
userPreferencesFlags |= FLAG_SEND_EMAIL; // Set email flag
if (userPreferencesFlags &amp;amp; FLAG_DARK_MODE) { /* Dark mode is on */ }
userPreferencesFlags = (userPreferencesFlags &amp;amp; ~THEME_ID_MASK) | (newThemeId &amp;lt;&amp;lt; THEME_ID_SHIFT); // Set theme
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Principle 2: Predictable and Minimal Execution Paths &amp;ndash; Optimize the Flow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Early engineers meticulously charted control flow to minimize CPU cycles and memory access. Jumps were expensive, and memory accesses were a carefully managed resource.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Modern Tendency:&lt;/strong&gt; Relying on complex ORM queries that generate verbose SQL, dynamic dispatch where simpler &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;switch&lt;/code&gt; statements would suffice, or excessive function calls and object instantiations in tight loops.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Core Memory Approach:&lt;/strong&gt; Linear, predictable code. Avoid unnecessary allocations within hot paths. Use array indexing over linked list traversal for better cache performance. Think about the order of operations to minimize temporary variables or redundant calculations. Choose algorithms that are known for low memory footprint and predictable execution.&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;// Conceptual Pseudocode: Modern vs. Core Memory Algorithm Choice

// Modern (potentially allocates new list repeatedly)
List&amp;lt;Item&amp;gt; filterItems(List&amp;lt;Item&amp;gt; allItems, Predicate&amp;lt;Item&amp;gt; condition) {
    List&amp;lt;Item&amp;gt; filtered = new ArrayList&amp;lt;&amp;gt;();
    for (Item item : allItems) {
        if (condition.test(item)) {
            filtered.add(item);
        }
    }
    return filtered;
}

// Core Memory Mindset (in-place modification or explicit buffer reuse)
// Requires pre-allocated buffer or direct array manipulation
int filterItemsInPlace(Item[] items, int count, Predicate&amp;lt;Item&amp;gt; condition, Item[] outputBuffer) {
    int writeIndex = 0;
    for (int i = 0; i &amp;lt; count; i++) {
        if (condition.test(items[i])) {
            outputBuffer[writeIndex++] = items[i]; // Or move to front in-place
        }
    }
    return writeIndex; // New count
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Principle 3: Explicit Resource Management &amp;ndash; Master Your Domain&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a world without garbage collectors or infinite virtual memory, every byte of RAM was explicitly allocated and deallocated. Resource leaks meant system crashes.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Modern Tendency:&lt;/strong&gt; Relying on garbage collection for memory, assuming database connections will close themselves, or letting frameworks implicitly manage file handles.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Core Memory Approach:&lt;/strong&gt; Manual allocation/deallocation (even if conceptual in higher-level languages), aggressive reuse of memory buffers, and clear ownership models for resources. Always consider the lifecycle of an object or data structure: when is it created, used, and explicitly released? Avoid creating transient objects in performance-critical loops.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff of this conceptual walkthrough is a deep, almost tactile understanding of how your code behaves at a machine level. It&amp;rsquo;s about designing with the hardware in mind, even when that hardware is abstracted away.&lt;/p&gt;

&lt;h3 id=&quot;conclusion-a-discipline-for-the-modern-age&quot;&gt;Conclusion: A Discipline for the Modern Age&lt;/h3&gt;

&lt;p&gt;Adopting the core memory mindset isn&amp;rsquo;t about forsaking high-level languages or abandoning productive abstractions. It&amp;rsquo;s about integrating a fundamental engineering discipline back into our development process. It&amp;rsquo;s a call to periodically &amp;ldquo;zoom out&amp;rdquo; from our comfortable layers of abstraction and consider the physical implications of our code.&lt;/p&gt;

&lt;p&gt;By asking &amp;ldquo;What would an engineer with only 64KB of RAM do?&amp;rdquo; we cultivate habits that lead to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Smaller Binaries:&lt;/strong&gt; Less code, less data, reduced memory footprint.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Lower Cloud Bills:&lt;/strong&gt; Efficient resource utilization means fewer instances, less memory, and lower CPU usage.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Improved Performance:&lt;/strong&gt; Faster execution, better cache utilization, and reduced latency.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;More Resilient Systems:&lt;/strong&gt; Fewer hidden resource leaks or unexpected memory pressure spikes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The convenience of abundant RAM has, perhaps, allowed us to neglect a crucial aspect of engineering. By thoughtfully re-engaging with the principles born from resource scarcity, we can build more robust, efficient, and environmentally conscious software for the present and future. Sometimes, a little bit of that old &amp;ldquo;ferrite donut&amp;rdquo; thinking is exactly what our modern systems need.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/22002/what-did-we-lose-with-abundant-ram</guid>
<pubDate>Mon, 06 Jul 2026 20:15:09 +0000</pubDate>
</item>
<item>
<title>🚀 EF Core Query Optimization for Beginners</title>
<link>https://coderlegion.com/21593/ef-core-query-optimization-for-beginners</link>
<description>&lt;p&gt;When developers first start using Entity Framework Core, it's easy to write queries that work—but not all queries perform well.&lt;/p&gt;
&lt;p&gt;The good news? A few simple practices can make your queries &lt;strong&gt;much faster&lt;/strong&gt;, &lt;strong&gt;use less memory&lt;/strong&gt;, and &lt;strong&gt;generate better SQL&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We'll use this real-world example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var offerDtos = await db.Offers
    .AsNoTracking()
    .Where(o =&amp;gt; o.DeliveryDate &amp;gt;= date &amp;amp;&amp;amp;
                o.DeliveryDate &amp;lt; nextDay &amp;amp;&amp;amp;
                (o.IsCompleted == null || o.IsCompleted == 0))
    .OrderBy(o =&amp;gt; o.Id)
    .Select(o =&amp;gt; new OfferForWarehouseDto
    {
        OfferId = o.Id,
        CustomerName = o.Customer.Name,
        DeliveryDate = o.DeliveryDate,
        OfferDate = o.OfferDate,
        Status = o.Status,
        Comments = o.Comments,
        WarehouseStatus = &quot;Pending&quot;,
        Lines = o.OfferLines
            .Select(l =&amp;gt; new WarehouseLineDto
            {
                OfferLineId = l.Id,
                ItemCode = l.ItemCode,
                ItemDescription = l.ItemDescription,
                Color = l.Color,
                Unit = l.Unit,
                OrderedQuantity = l.Quantity,
                CurrentStock = l.Product.Stock
            })
            .ToList()
    })
    .ToListAsync(cancellationToken);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's break it down step by step.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-use-asnotracking-for-read-only-queries-&quot;&gt;1. Use &lt;code&gt;AsNoTracking()&lt;/code&gt; for Read-Only Queries ⭐&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;.AsNoTracking()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One of EF Core's most useful features is the &lt;strong&gt;Change Tracker&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Whenever you load entities, EF Core tracks them in memory so it can detect changes and generate the appropriate &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt; statements when you call &lt;code&gt;SaveChanges()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var offer = await db.Offers.FirstAsync();

offer.Status = &quot;Completed&quot;;

await db.SaveChangesAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though you never tell EF Core that &lt;code&gt;Status&lt;/code&gt; changed, it knows because the &lt;strong&gt;Change Tracker&lt;/strong&gt; has been monitoring the entity.&lt;/p&gt;
&lt;p&gt;This is extremely convenient when you're editing data.&lt;/p&gt;
&lt;p&gt;However, tracking every entity comes with a cost.&lt;/p&gt;
&lt;p&gt;For each tracked entity, EF Core stores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The original values&lt;/li&gt;
&lt;li&gt;The current values&lt;/li&gt;
&lt;li&gt;The entity state (&lt;code&gt;Added&lt;/code&gt;, &lt;code&gt;Modified&lt;/code&gt;, &lt;code&gt;Deleted&lt;/code&gt;, &lt;code&gt;Unchanged&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Relationship information&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're only displaying data in a grid or returning it from an API, all of this tracking is unnecessary.&lt;/p&gt;
&lt;p&gt;That's why read-only queries should use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;.AsNoTracking()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells EF Core:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;I'm only reading this data. Don't track it.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;benefits&quot;&gt;Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;✅ Lower memory usage&lt;/li&gt;
&lt;li&gt;✅ Faster query execution&lt;/li&gt;
&lt;li&gt;✅ Less CPU overhead&lt;/li&gt;
&lt;li&gt;✅ Better performance for APIs, dashboards, and reports&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Learn more&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Change Tracking: &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/change-tracking/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/change-tracking/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tracking vs. No-Tracking Queries: &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/tracking&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/tracking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-filter-data-before-loading-it&quot;&gt;2. Filter Data Before Loading It&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;.Where(o =&amp;gt;
    o.DeliveryDate &amp;gt;= date &amp;amp;&amp;amp;
    o.DeliveryDate &amp;lt; nextDay &amp;amp;&amp;amp;
    (o.IsCompleted == null || o.IsCompleted == 0))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always filter data in the database.&lt;/p&gt;
&lt;p&gt;Good:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;db.Offers.Where(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;db.Offers.ToList().Where(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first generates a SQL &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;
&lt;p&gt;The second loads the entire table into memory before filtering.&lt;/p&gt;
&lt;p&gt;The database is optimized for filtering large datasets, so let it do the work.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-project-only-what-you-need&quot;&gt;3. Project Only What You Need&lt;/h2&gt;
&lt;p&gt;Notice that we're not returning &lt;code&gt;Offer&lt;/code&gt; entities.&lt;/p&gt;
&lt;p&gt;Instead, we're creating DTOs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;.Select(o =&amp;gt; new OfferForWarehouseDto
{
    OfferId = o.Id,
    CustomerName = o.Customer.Name
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells EF Core exactly which columns to retrieve.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT *
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EF Core generates SQL that selects only the properties used in the DTO.&lt;/p&gt;
&lt;p&gt;Benefits include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Smaller SQL results&lt;/li&gt;
&lt;li&gt;Less network traffic&lt;/li&gt;
&lt;li&gt;Lower memory usage&lt;/li&gt;
&lt;li&gt;Faster execution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Learn more&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying#project-only-properties-you-need&quot;&gt;https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying#project-only-properties-you-need&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-don-t-use-include-when-projecting&quot;&gt;4. Don't Use &lt;code&gt;Include()&lt;/code&gt; When Projecting&lt;/h2&gt;
&lt;p&gt;Many developers write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;.Include(o =&amp;gt; o.Customer)
.Include(o =&amp;gt; o.OfferLines)
.ThenInclude(l =&amp;gt; l.Product)
.Select(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is unnecessary.&lt;/p&gt;
&lt;p&gt;When projecting into a DTO, EF Core automatically generates the required SQL joins.&lt;/p&gt;
&lt;p&gt;Simply write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;.Select(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The generated SQL is usually smaller, cleaner, and more efficient.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-sort-in-sql&quot;&gt;5. Sort in SQL&lt;/h2&gt;
&lt;p&gt;Sorting should happen before materializing the results.&lt;/p&gt;
&lt;p&gt;Good:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;.OrderBy(o =&amp;gt; o.Id)
.ToListAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;.ToList()
.OrderBy(o =&amp;gt; o.Id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;SQL Server is optimized for sorting data.&lt;/p&gt;
&lt;p&gt;Don't move that work into your application.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-project-child-collections&quot;&gt;6. Project Child Collections&lt;/h2&gt;
&lt;p&gt;EF Core can also project related collections.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;Lines = o.OfferLines
    .Select(l =&amp;gt; new WarehouseLineDto
    {
        ItemCode = l.ItemCode,
        CurrentStock = l.Product.Stock
    })
    .ToList()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that we never load the entire &lt;code&gt;OfferLine&lt;/code&gt; or &lt;code&gt;Product&lt;/code&gt; entities.&lt;/p&gt;
&lt;p&gt;Only the required columns are selected.&lt;/p&gt;
&lt;p&gt;This keeps queries efficient while still returning nested objects.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;7-use-async-queries&quot;&gt;7. Use Async Queries&lt;/h2&gt;
&lt;p&gt;Always prefer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;await ToListAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Async queries don't make the database faster, but they prevent your application thread from blocking while waiting for SQL Server.&lt;/p&gt;
&lt;p&gt;This improves scalability in ASP.NET Core applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;8-create-proper-database-indexes&quot;&gt;8. Create Proper Database Indexes&lt;/h2&gt;
&lt;p&gt;Even the best LINQ query can be slow if the database has no indexes.&lt;/p&gt;
&lt;p&gt;This query filters by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DeliveryDate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IsCompleted&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A composite index can dramatically improve performance.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX IX_Offers_DeliveryDate_IsCompleted
ON Offers (DeliveryDate, IsCompleted);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;EF Core generates SQL.&lt;br&gt;
SQL Server executes SQL.&lt;br&gt;
Indexes make SQL fast.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-optimized-query&quot;&gt;Final Optimized Query&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var offerDtos = await db.Offers
    .AsNoTracking()
    .Where(o =&amp;gt; o.DeliveryDate &amp;gt;= date &amp;amp;&amp;amp;
                o.DeliveryDate &amp;lt; nextDay &amp;amp;&amp;amp;
                (o.IsCompleted == null || o.IsCompleted == 0))
    .OrderBy(o =&amp;gt; o.Id)
    .Select(o =&amp;gt; new OfferForWarehouseDto
    {
        OfferId = o.Id,
        CustomerName = o.Customer.Name,
        DeliveryDate = o.DeliveryDate,
        OfferDate = o.OfferDate,
        Status = o.Status,
        Comments = o.Comments,
        WarehouseStatus = &quot;Pending&quot;,
        Lines = o.OfferLines
            .Select(l =&amp;gt; new WarehouseLineDto
            {
                OfferLineId = l.Id,
                ItemCode = l.ItemCode,
                ItemDescription = l.ItemDescription,
                Color = l.Color,
                Unit = l.Unit,
                OrderedQuantity = l.Quantity,
                CurrentStock = l.Product.Stock
            })
            .ToList()
    })
    .ToListAsync(cancellationToken);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;-key-takeaways&quot;&gt;🎯 Key Takeaways&lt;/h2&gt;
&lt;p&gt;Whenever you write an EF Core query, ask yourself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Is this a read-only query? → Use &lt;code&gt;AsNoTracking()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;✅ Am I letting the database filter my data?&lt;/li&gt;
&lt;li&gt;✅ Am I selecting only the columns I need?&lt;/li&gt;
&lt;li&gt;✅ Am I avoiding unnecessary &lt;code&gt;Include()&lt;/code&gt; calls?&lt;/li&gt;
&lt;li&gt;✅ Am I using async methods like &lt;code&gt;ToListAsync()&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;✅ Does my database have the right indexes?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Following these simple habits will help you write faster, cleaner, and more scalable EF Core applications.&lt;/p&gt;
&lt;h3 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;EF Core Performance: &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/performance/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/performance/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Efficient Querying: &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying&quot;&gt;https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Change Tracking: &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/change-tracking/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/change-tracking/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tracking vs. No-Tracking Queries: &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/tracking&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/tracking&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21593/ef-core-query-optimization-for-beginners</guid>
<pubDate>Mon, 29 Jun 2026 19:47:13 +0000</pubDate>
</item>
<item>
<title>5 EF Core Features Every Enterprise Developer Should Know</title>
<link>https://coderlegion.com/19745/5-ef-core-features-every-enterprise-developer-should-know</link>
<description>&lt;p&gt;Entity Framework Core (EF Core) is often introduced as an ORM that simplifies data access, but its real value in enterprise applications comes from features that improve reliability, scalability, and maintainability. &lt;/p&gt;
&lt;p&gt;As applications grow and operate in cloud environments, these capabilities become essential rather than optional.&lt;/p&gt;
&lt;h3 id=&quot;1-connection-resiliency-and-retry-policies&quot;&gt;1. Connection Resiliency and Retry Policies&lt;/h3&gt;
&lt;p&gt;Enterprise systems frequently run on cloud-hosted databases where transient network failures, failovers, and throttling can occur. EF Core provides built-in connection resiliency by automatically retrying failed database operations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;options.UseSqlServer(connectionString, sqlOptions =&amp;gt;
    sqlOptions.EnableRetryOnFailure(
        maxRetryCount: 5,
        maxRetryDelay: TimeSpan.FromSeconds(10),
        errorNumbersToAdd: null));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This helps applications recover gracefully from temporary infrastructure issues without requiring custom retry logic throughout the codebase.&lt;/p&gt;
&lt;h3 id=&quot;2-optimistic-concurrency-control&quot;&gt;2. Optimistic Concurrency Control&lt;/h3&gt;
&lt;p&gt;In multi-user systems, preventing lost updates is critical. EF Core supports optimistic concurrency through concurrency tokens such as SQL Server's &lt;code&gt;rowversion&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class Order
{
    public int Id { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When another user modifies the same record, EF Core detects the conflict and throws a &lt;code&gt;DbUpdateConcurrencyException&lt;/code&gt;, allowing the application to reload, merge, or notify the user. This ensures data integrity while avoiding expensive database locks.&lt;/p&gt;
&lt;h3 id=&quot;3-performance-optimization-with-compiled-queries&quot;&gt;3. Performance Optimization with Compiled Queries&lt;/h3&gt;
&lt;p&gt;High-throughput APIs and microservices can execute the same queries thousands of times per second. EF Core offers several performance features, including compiled queries, &lt;code&gt;AsNoTracking&lt;/code&gt;, query splitting, and batching.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;private static readonly Func&amp;lt;AppDbContext, int, Task&amp;lt;Order&amp;gt;&amp;gt; GetOrder =
    EF.CompileAsyncQuery((AppDbContext ctx, int id) =&amp;gt;
        ctx.Orders.AsNoTracking()
                  .FirstOrDefault(o =&amp;gt; o.Id == id));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compiled queries eliminate repeated LINQ-to-SQL translation overhead, while &lt;code&gt;AsNoTracking&lt;/code&gt; reduces change-tracking costs for read-only operations. These optimizations can significantly improve throughput under load.&lt;/p&gt;
&lt;h3 id=&quot;4-transactions-and-unit-of-work&quot;&gt;4. Transactions and Unit of Work&lt;/h3&gt;
&lt;p&gt;Business operations often involve multiple database changes that must either succeed together or fail together. EF Core supports explicit transactions to guarantee atomicity.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;await using var tx = await context.Database.BeginTransactionAsync();

try
{
    await context.SaveChangesAsync();
    await otherContext.SaveChangesAsync();
    await tx.CommitAsync();
}
catch
{
    await tx.RollbackAsync();
    throw;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This capability is fundamental for maintaining consistency across aggregates, repositories, and critical business workflows.&lt;/p&gt;
&lt;h3 id=&quot;5-global-query-filters&quot;&gt;5. Global Query Filters&lt;/h3&gt;
&lt;p&gt;Global query filters allow rules to be applied automatically to every query for a given entity. They are especially valuable for multi-tenant SaaS applications and soft-delete implementations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity&amp;lt;Customer&amp;gt;()
        .HasQueryFilter(c =&amp;gt; !c.IsDeleted);

    modelBuilder.Entity&amp;lt;Invoice&amp;gt;()
        .HasQueryFilter(x =&amp;gt; x.TenantId == _currentTenant.Id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By centralizing these concerns, developers reduce duplication and eliminate the risk of accidentally exposing data across tenants or returning deleted records.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;While EF Core is easy to start with, enterprise applications require more than basic CRUD operations. Features such as connection resiliency, concurrency control, performance optimization, transactions, and global query filters help teams build systems that are robust, scalable, and maintainable in production.&lt;/p&gt;
&lt;p&gt;Mastering these five capabilities will provide a strong foundation for building enterprise-grade applications with EF Core.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19745/5-ef-core-features-every-enterprise-developer-should-know</guid>
<pubDate>Fri, 05 Jun 2026 08:47:24 +0000</pubDate>
</item>
<item>
<title>When IDbContextFactory enters the chat, and what it actually fixes.</title>
<link>https://coderlegion.com/11754/when-idbcontextfactory-enters-the-chat-and-what-it-actually-fixes</link>
<description>&lt;h3 id=&quot;when-idbcontextfactory-enters-the-chat-and-what-it-actually-fixes&quot;&gt;When &lt;code&gt;IDbContextFactory&lt;/code&gt; enters the chat, and what it actually fixes&lt;/h3&gt;
&lt;p&gt;Most EF Core examples use a scoped &lt;code&gt;DbContext&lt;/code&gt;. In a normal Web API request, that’s fine, the request ends, the context is disposed.&lt;/p&gt;
&lt;p&gt;Then you build Blazor Server.&lt;/p&gt;
&lt;p&gt;A Blazor Server circuit can stay alive for a long time, minutes or hours. If your &lt;code&gt;DbContext&lt;/code&gt; is scoped, it can also stay alive that long. That’s when things start to break in annoying ways.&lt;/p&gt;
&lt;h4 id=&quot;what-problems-show-up&quot;&gt;What problems show up&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1) “A second operation was started on this context…”&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;DbContext&lt;/code&gt; is not thread-safe. In Blazor Server it’s easy to trigger overlapping work (clicks, timers, loading states). If two queries happen at the same time on the same context, you get this error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) Tracking bloat&lt;/strong&gt;&lt;br&gt;
A long-lived context keeps tracking entities. Over time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;memory grows&lt;/li&gt;
&lt;li&gt;queries can feel slower&lt;/li&gt;
&lt;li&gt;SaveChanges can get heavy&lt;/li&gt;
&lt;li&gt;you hit “already being tracked” errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3) Stale or confusing data&lt;/strong&gt;&lt;br&gt;
Because the context tracks objects, you can end up reusing old instances without realizing it, the UI feels out of sync.&lt;/p&gt;
&lt;h4 id=&quot;what-idbcontextfactory-changes&quot;&gt;What &lt;code&gt;IDbContextFactory&lt;/code&gt; changes&lt;/h4&gt;
&lt;p&gt;It gives you a fresh &lt;code&gt;DbContext&lt;/code&gt; per operation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create context&lt;/li&gt;
&lt;li&gt;run the query or command&lt;/li&gt;
&lt;li&gt;dispose the context&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So each click, each load, each action gets its own clean context. No shared state, no concurrency fights, no growing tracker.&lt;/p&gt;
&lt;h4 id=&quot;when-to-use-it&quot;&gt;When to use it&lt;/h4&gt;
&lt;p&gt;Use &lt;code&gt;IDbContextFactory&lt;/code&gt; when you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Blazor Server&lt;/li&gt;
&lt;li&gt;background jobs or hosted services&lt;/li&gt;
&lt;li&gt;timers, polling, signalR style event flows&lt;/li&gt;
&lt;li&gt;any parallel DB work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In simple controller-per-request apps, scoped &lt;code&gt;DbContext&lt;/code&gt; is still perfectly fine.&lt;/p&gt;
&lt;h4 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;IDbContextFactory&lt;/code&gt; is not a “nice to have”, it’s the safer default when your app is long-lived and event-driven. It prevents a whole category of EF Core bugs before they happen.&lt;/p&gt;
&lt;p&gt;Source Code :&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/BlazorFactoryDemo&quot;&gt;https://github.com/stevsharp/BlazorFactoryDemo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;More to read  : &lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1?view=efcore-10.0&quot;&gt;https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.idbcontextfactory-1?view=efcore-10.0&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11754/when-idbcontextfactory-enters-the-chat-and-what-it-actually-fixes</guid>
<pubDate>Mon, 16 Feb 2026 09:47:23 +0000</pubDate>
</item>
<item>
<title>ASP.NET Core Custom Logger — Save Logs to Database (SQL)</title>
<link>https://coderlegion.com/10763/asp-net-core-custom-logger-save-logs-to-database-sql</link>
<description>&lt;p&gt;&lt;strong&gt;ASP.NET Core&lt;/strong&gt; already gives you solid logging out of the box, and if you want richer sinks and structured logs, Serilog is a common choice.&lt;/p&gt;
&lt;p&gt;But sometimes you cannot use a ready made sink, because the company already has a legacy logging table with a fixed schema. In that case, you build a custom database logger that writes into that table.&lt;/p&gt;
&lt;p&gt;This tutorial includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database table&lt;/li&gt;
&lt;li&gt;Logger options&lt;/li&gt;
&lt;li&gt;Custom logger&lt;/li&gt;
&lt;li&gt;Logger provider&lt;/li&gt;
&lt;li&gt;Background queue (Channel)&lt;/li&gt;
&lt;li&gt;Dapper SQL insert&lt;/li&gt;
&lt;li&gt;Registration in Program.cs&lt;/li&gt;
&lt;li&gt;Usage example&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1-create-the-sql-table&quot;&gt;1️⃣ Create the SQL table&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE dbo.AppLogs
(
    LogId     BIGINT IDENTITY PRIMARY KEY,
    Ts        DATETIME2(3) NOT NULL,
    Level     NVARCHAR(20) NOT NULL,
    Category  NVARCHAR(200) NOT NULL,
    TraceId   NVARCHAR(64) NULL,
    Message   NVARCHAR(MAX) NOT NULL,
    Exception NVARCHAR(MAX) NULL,
    Scopes    NVARCHAR(MAX) NULL
);

CREATE INDEX IX_AppLogs_Ts    ON dbo.AppLogs(Ts);
CREATE INDEX IX_AppLogs_Level ON dbo.AppLogs(Level);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-add-nuget-packages&quot;&gt;2️⃣ Add NuGet packages&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package Dapper
dotnet add package Microsoft.Data.SqlClient
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-logger-options&quot;&gt;3️⃣ Logger options&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class DbLogOptions
{
    public string ConnectionString { get; set; } = string.Empty;
    public LogLevel MinLevel { get; set; } = LogLevel.Information;

    // Max number of log entries waiting in memory
    public int MaxQueue { get; set; } = 5000;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-log-entry-model&quot;&gt;4️⃣ Log entry model&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class DbLogEntry
{
    public DateTime Ts { get; set; }
    public string Level { get; set; } = &quot;&quot;;
    public string Category { get; set; } = &quot;&quot;;
    public string? TraceId { get; set; }
    public string Message { get; set; } = &quot;&quot;;
    public string? Exception { get; set; }
    public string? Scopes { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-custom-database-logger&quot;&gt;5️⃣ Custom database logger&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Diagnostics;
using System.Text.Json;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;

public sealed class DbLogger : ILogger
{
    private readonly string _category;
    private readonly DbLogOptions _options;
    private readonly Channel&amp;lt;DbLogEntry&amp;gt; _channel;
    private readonly IExternalScopeProvider? _scopeProvider;

    public DbLogger(
        string category,
        DbLogOptions options,
        Channel&amp;lt;DbLogEntry&amp;gt; channel,
        IExternalScopeProvider? scopeProvider)
    {
        _category = category;
        _options = options;
        _channel = channel;
        _scopeProvider = scopeProvider;
    }

    public IDisposable BeginScope&amp;lt;TState&amp;gt;(TState state) =&amp;gt;
        _scopeProvider?.Push(state) ?? NullScope.Instance;

    public bool IsEnabled(LogLevel logLevel) =&amp;gt; logLevel &amp;gt;= _options.MinLevel;

    public void Log&amp;lt;TState&amp;gt;(
        LogLevel logLevel,
        EventId eventId,
        TState state,
        Exception? exception,
        Func&amp;lt;TState, Exception?, string&amp;gt; formatter)
    {
        if (!IsEnabled(logLevel)) return;
        if (formatter is null) return;

        var traceId = Activity.Current?.TraceId.ToString();

        string? scopesJson = null;
        if (_scopeProvider is not null)
        {
            var scopes = new List&amp;lt;object?&amp;gt;();
            _scopeProvider.ForEachScope((s, list) =&amp;gt; list.Add(s), scopes);

            if (scopes.Count &amp;gt; 0)
                scopesJson = JsonSerializer.Serialize(scopes);
        }

        var entry = new DbLogEntry
        {
            Ts = DateTime.UtcNow,
            Level = logLevel.ToString(),
            Category = _category,
            TraceId = traceId,
            Message = formatter(state, exception),
            Exception = exception?.ToString(),
            Scopes = scopesJson
        };

        // If queue is full, this returns false and the log is dropped
        _ = _channel.Writer.TryWrite(entry);
    }

    private sealed class NullScope : IDisposable
    {
        public static readonly NullScope Instance = new();
        public void Dispose() { }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6-logger-provider-writes-to-sql-using-dapper-&quot;&gt;6️⃣ Logger provider (writes to SQL using Dapper)&lt;/h3&gt;
&lt;p&gt;This provider uses a bounded channel and a single background reader so request threads never block on SQL.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Threading.Channels;
using Dapper;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

public sealed class DbLoggerProvider : ILoggerProvider, ISupportExternalScope
{
    private readonly DbLogOptions _options;
    private readonly Channel&amp;lt;DbLogEntry&amp;gt; _channel;
    private readonly CancellationTokenSource _cts = new();
    private IExternalScopeProvider? _scopeProvider;

    public DbLoggerProvider(IOptions&amp;lt;DbLogOptions&amp;gt; options)
    {
        _options = options.Value;

        var channelOptions = new BoundedChannelOptions(_options.MaxQueue)
        {
            SingleReader = true,
            SingleWriter = false,
            FullMode = BoundedChannelFullMode.DropOldest
        };

        _channel = Channel.CreateBounded&amp;lt;DbLogEntry&amp;gt;(channelOptions);

        _ = Task.Run(ProcessQueueAsync);
    }

    public ILogger CreateLogger(string categoryName) =&amp;gt;
        new DbLogger(categoryName, _options, _channel, _scopeProvider);

    public void SetScopeProvider(IExternalScopeProvider scopeProvider) =&amp;gt;
        _scopeProvider = scopeProvider;

    public void Dispose()
    {
        _cts.Cancel();
        _channel.Writer.TryComplete();
        _cts.Dispose();
    }

    private async Task ProcessQueueAsync()
    {
        const string sql = @&quot;
INSERT INTO dbo.AppLogs (Ts, Level, Category, TraceId, Message, Exception, Scopes)
VALUES (@Ts, @Level, @Category, @TraceId, @Message, @Exception, @Scopes);&quot;;

        try
        {
            while (await _channel.Reader.WaitToReadAsync(_cts.Token))
            {
                while (_channel.Reader.TryRead(out var entry))
                {
                    await using var conn = new SqlConnection(_options.ConnectionString);
                    await conn.ExecuteAsync(sql, entry);
                }
            }
        }
        catch (OperationCanceledException)
        {
            // graceful shutdown
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;7-add-registration-extension&quot;&gt;7️⃣ Add registration extension&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public static class DbLoggerExtensions
{
    public static ILoggingBuilder AddDbLogger(
        this ILoggingBuilder builder,
        Action&amp;lt;DbLogOptions&amp;gt; configure)
    {
        builder.Services.Configure(configure);
        builder.Services.AddSingleton&amp;lt;ILoggerProvider, DbLoggerProvider&amp;gt;();
        return builder;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;8-register-in-program-cs&quot;&gt;8️⃣ Register in Program.cs&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var builder = WebApplication.CreateBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConsole();

builder.Logging.AddDbLogger(options =&amp;gt;
{
    options.ConnectionString = builder.Configuration.GetConnectionString(&quot;DefaultConnection&quot;)!;
    options.MinLevel = LogLevel.Information;
    options.MaxQueue = 5000;
});

var app = builder.Build();

app.MapGet(&quot;/test&quot;, (ILogger&amp;lt;Program&amp;gt; logger) =&amp;gt;
{
    logger.LogInformation(&quot;Hello from DB Logger&quot;);
    logger.LogError(new Exception(&quot;Test&quot;), &quot;Something failed&quot;);
    return Results.Ok(&quot;OK&quot;);
});

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-we-used-channels-in-the-db-logger&quot;&gt;Why we used Channels in the DB logger&lt;/h3&gt;
&lt;p&gt;Your logger needs to accept logs from many producers (web requests, background jobs, services) and write them to the DB via one efficient writer without blocking request threads or losing control when traffic spikes. Channels are a great fit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Non blocking producers&lt;/li&gt;
&lt;li&gt;Backpressure with bounded capacity&lt;/li&gt;
&lt;li&gt;Single reader, many writers&lt;/li&gt;
&lt;li&gt;Async, allocation friendly&lt;/li&gt;
&lt;li&gt;Graceful shutdown&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Source code:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/DbLoggerSample&quot;&gt;https://github.com/stevsharp/DbLoggerSample&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10763/asp-net-core-custom-logger-save-logs-to-database-sql</guid>
<pubDate>Fri, 30 Jan 2026 09:30:43 +0000</pubDate>
</item>
<item>
<title>Building a Simple Cron Scheduler in .NET with Cronos</title>
<link>https://coderlegion.com/7920/building-a-simple-cron-scheduler-in-net-with-cronos</link>
<description>&lt;h3 id=&quot;1-introduction&quot;&gt;1. Introduction&lt;/h3&gt;
&lt;p&gt;Most .NET apps need some kind of background work. Sending emails, generating reports, cleaning data, all of these are better when they run automatically on a schedule, instead of being triggered by users.&lt;/p&gt;
&lt;p&gt;You do not always need a heavy framework like Quartz or Hangfire. For many scenarios a lightweight cron based scheduler in a console or worker service is more than enough.&lt;/p&gt;
&lt;p&gt;In this tutorial you will build a small but powerful console scheduler that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uses &lt;strong&gt;Cronos&lt;/strong&gt;, a library for parsing cron expressions and calculating next run times&lt;/li&gt;
&lt;li&gt;Runs &lt;strong&gt;multiple jobs&lt;/strong&gt; with different cron expressions&lt;/li&gt;
&lt;li&gt;Executes jobs &lt;strong&gt;in parallel&lt;/strong&gt;, so a slow job does not block the rest&lt;/li&gt;
&lt;li&gt;Handles &lt;strong&gt;graceful shutdown&lt;/strong&gt; using &lt;code&gt;CancellationToken&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cronos itself does not execute jobs, it only parses cron expressions and calculates occurrences, which makes it perfect as a building block for your own scheduler. (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/NetCoreCronSchedulerDemo&quot;&gt;GitHub&lt;/a&gt;)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-prerequisites&quot;&gt;2. Prerequisites&lt;/h3&gt;
&lt;p&gt;You will need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.NET 8 SDK or newer&lt;/li&gt;
&lt;li&gt;Basic C# knowledge&lt;/li&gt;
&lt;li&gt;NuGet package &lt;strong&gt;Cronos&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-project-setup&quot;&gt;3. Project setup&lt;/h3&gt;
&lt;p&gt;Create a new console project and add Cronos.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet new console -n ConsoleCronSchedulerDemo
cd ConsoleCronSchedulerDemo

dotnet add package Cronos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your final structure will look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ConsoleCronSchedulerDemo/
 ├─ Program.cs
 ├─ Jobs/
 │   ├─ ICronJob.cs
 │   ├─ EmailJob.cs
 │   ├─ ReportJob.cs
 │   └─ Scheduler.cs
 └─ README.md  (optional)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-defining-the-cron-job-contract&quot;&gt;4. Defining the cron job contract&lt;/h3&gt;
&lt;p&gt;First, create a &lt;code&gt;Jobs&lt;/code&gt; folder and add &lt;code&gt;ICronJob.cs&lt;/code&gt;. This interface describes what every scheduled job must provide.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Jobs/ICronJob.cs
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleCronSchedulerDemo.Jobs;

public interface ICronJob
{
    /// &amp;lt;summary&amp;gt;
    /// Human friendly name, used in logs.
    /// &amp;lt;/summary&amp;gt;
    string Name { get; }
    /// &amp;lt;summary&amp;gt;
    /// Cron expression in standard 5 field format
    /// for example &quot;*/1 * * * *&quot;.
    /// &amp;lt;/summary&amp;gt;
    string CronExpression { get; }
    /// &amp;lt;summary&amp;gt;
    /// Job logic.
    /// &amp;lt;/summary&amp;gt;
    Task ExecuteAsync(CancellationToken stoppingToken);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each job knows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Its &lt;strong&gt;name&lt;/strong&gt;, for logging&lt;/li&gt;
&lt;li&gt;Its &lt;strong&gt;cron expression&lt;/strong&gt;, a simple string&lt;/li&gt;
&lt;li&gt;&lt;h3 id=&quot;the-work-it-needs-to-perform-exposed-as-executeasync&quot;&gt;The &lt;strong&gt;work&lt;/strong&gt; it needs to perform, exposed as &lt;code&gt;ExecuteAsync&lt;/code&gt;&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;5-implementing-concrete-jobs&quot;&gt;5. Implementing concrete jobs&lt;/h3&gt;
&lt;p&gt;Now create two simple demo jobs.&lt;/p&gt;
&lt;h4 id=&quot;5-1-emailjob&quot;&gt;5.1 EmailJob&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Jobs/EmailJob.cs
using System;
using System.Threading;
using System.Threading.Tasks;

public class EmailJob(string cronExpression, IEmailService emailService) : ICronJob
{
    private readonly IEmailService _emailService = emailService;
    public string Name =&amp;gt; &quot;EmailJob&quot;;
    public string CronExpression { get; } = cronExpression;

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await _emailService.SendReportAsync();

        Console.WriteLine($&quot;[{Name}] Email sent at {DateTimeOffset.Now}&quot;);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;5-2-reportjob&quot;&gt;5.2 ReportJob&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Jobs/ReportJob.cs
using System;
using System.Threading;
using System.Threading.Tasks;

public class ReportJob(string cronExpression, IReportService reportService) : ICronJob
{
    private readonly IReportService _reportService = reportService;
    public string Name =&amp;gt; &quot;ReportJob&quot;;
    public string CronExpression { get; } = cronExpression;

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await _reportService.GenerateReportAsync();

        Console.WriteLine($&quot;[{Name}] Report generated at {DateTimeOffset.Now}&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These jobs are just writing to the console, but in a real application they could send emails, generate reports, call APIs, clean a database, or whatever your business needs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-the-scheduler-that-uses-cronos&quot;&gt;6. The scheduler that uses Cronos&lt;/h3&gt;
&lt;p&gt;Now you create a scheduler that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Takes a list of &lt;code&gt;ICronJob&lt;/code&gt; implementations&lt;/li&gt;
&lt;li&gt;Uses &lt;strong&gt;Cronos&lt;/strong&gt; to parse each cron expression and compute the next occurrence&lt;/li&gt;
&lt;li&gt;Loops continuously, checking if any job is due&lt;/li&gt;
&lt;li&gt;Runs each due job in its own &lt;code&gt;Task&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Respects a &lt;code&gt;CancellationToken&lt;/code&gt; so you can shut down cleanly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Create &lt;code&gt;Jobs/Scheduler.cs&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Jobs/Scheduler.cs
public sealed class Scheduler
{
    private readonly ILogger&amp;lt;Scheduler&amp;gt; _logger;
    private readonly List&amp;lt;(ICronJob job, CronExpression expression)&amp;gt; _jobs;
    private readonly TimeZoneInfo _timeZoneInfo;
    private readonly SemaphoreSlim _semaphore;
    private readonly SchedulerSettings _settings;

    public Scheduler(
        IEnumerable&amp;lt;ICronJob&amp;gt; cronJobs,
        ILogger&amp;lt;Scheduler&amp;gt; logger,
        SchedulerSettings settings)
    {
        _logger = logger;
        _settings = settings ?? new SchedulerSettings();
        _jobs = [.. cronJobs.Select(job =&amp;gt; (job, CronExpression.Parse(job.CronExpression)))];
        _timeZoneInfo = TimeZoneInfo.Local;

        var max = _settings.MaxConcurrentJobs &amp;gt; 0 ? _settings.MaxConcurrentJobs : 1;

        _semaphore = new SemaphoreSlim(max);
    }
    /// &amp;lt;summary&amp;gt;
    /// 
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;stoppingToken&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public async Task StartAsync(CancellationToken stoppingToken)
    {
        var nextRuns = _jobs.ToDictionary(
            j =&amp;gt; j.job.Name,
            j =&amp;gt; j.expression.GetNextOccurrence(DateTimeOffset.Now, _timeZoneInfo));

        _logger.LogInformation(&quot;Scheduler started with {JobCount} jobs, max {Max} concurrent.&quot;, _jobs.Count, _settings.MaxConcurrentJobs);

        while (!stoppingToken.IsCancellationRequested)
        {
            var now = DateTimeOffset.Now;

            foreach (var (job, expression) in _jobs)
            {
                var next = nextRuns[job.Name];

                if (!next.HasValue || next.Value &amp;gt; now)
                    continue;

                _logger.LogInformation(&quot;Running {Job} at {Now}&quot;, job.Name, now);

                _ = Task.Run(() =&amp;gt; RunJobWithRetryAsync(job, stoppingToken), stoppingToken);

                nextRuns[job.Name] = expression.GetNextOccurrence(now.AddSeconds(1), _timeZoneInfo);

                _logger.LogInformation(&quot;Next run of {Job} scheduled at {Next}&quot;,job.Name, nextRuns[job.Name]);
            }

            try
            {
                await Task.Delay(1000, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                _logger.LogInformation(&quot;Scheduler cancellation requested.&quot;);

                break;
            }
        }

        _logger.LogInformation(&quot;Scheduler stopped.&quot;);
    }
    /// &amp;lt;summary&amp;gt;
    /// 
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;job&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;ct&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private async Task RunJobWithRetryAsync(ICronJob job, CancellationToken ct)
    {
        bool acquired = false;
        try
        {
            await _semaphore.WaitAsync(ct);

            acquired = true;

            var maxRetries = Math.Max(0, _settings.MaxRetries);
            var attempt = 0;

            while (!ct.IsCancellationRequested)
            {
                attempt++;

                try
                {
                    await job.ExecuteAsync(ct);

                    _logger.LogInformation(&quot;{Job} completed successfully (attempt {Attempt}).&quot;, job.Name, attempt);

                    return;
                }
                catch (OperationCanceledException) when (ct.IsCancellationRequested)
                {
                    _logger.LogInformation(&quot;{Job} cancelled during execution.&quot;, job.Name);

                    return;
                }
                catch (Exception ex)
                {
                    if (attempt &amp;gt; maxRetries)
                    {
                        _logger.LogError(ex, &quot;{Job} failed after {Attempts} attempts.&quot;, job.Name, attempt - 1);
                        return;
                    }

                    var delay = GetBackoffDelay(attempt);

                    _logger.LogWarning(ex, &quot;{Job} failed on attempt {Attempt}. Retrying in {Delay}...&quot;, job.Name, attempt, delay);

                    await Task.Delay(delay, ct);
                }
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation(&quot;{Job} scheduling cancelled before start.&quot;, job.Name);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Unexpected error while scheduling/executing {Job}.&quot;, job.Name);
        }
        finally
        {
            if (acquired)
            {
                _semaphore.Release();
            }
        }
    }
    /// &amp;lt;summary&amp;gt;
    /// 
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;attempt&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private TimeSpan GetBackoffDelay(int attempt)
    {
        var baseSeconds = Math.Max(1, _settings.RetryDelaySeconds);
        // Exponential backoff: 1, 2, 4, 8, ...
        var exponent = Math.Min(attempt - 1, 10);

        var seconds = baseSeconds * (int)Math.Pow(2, exponent);

        // Add small jitter to avoid thundering herd
        var jitterMs = Random.Shared.Next(100, 500);

        var delay = TimeSpan.FromSeconds(seconds) + TimeSpan.FromMilliseconds(jitterMs);

        var maxDelay = TimeSpan.FromMinutes(2);

        return delay &amp;lt;= maxDelay ? delay : maxDelay;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-use-cronos-here&quot;&gt;Why use Cronos here&lt;/h4&gt;
&lt;p&gt;Cronos gives you &lt;code&gt;CronExpression.Parse(string)&lt;/code&gt; and &lt;code&gt;GetNextOccurrence&lt;/code&gt; which calculates the next valid date time in a given time zone. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You do not have to manually calculate future dates&lt;/li&gt;
&lt;li&gt;Time zones and daylight saving changes are handled according to cron semantics (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/NetCoreCronSchedulerDemo&quot;&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your scheduler is now focused on one thing, deciding when to run jobs and starting tasks, and Cronos handles the calendar rules.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-wiring-everything-in-program-cs&quot;&gt;7. Wiring everything in Program.cs&lt;/h3&gt;
&lt;p&gt;Finally, wire everything together in &lt;code&gt;Program.cs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;using Microsoft.Extensions.Configuration;&lt;br&gt;
using Microsoft.Extensions.DependencyInjection;&lt;br&gt;
using Microsoft.Extensions.Logging;&lt;/p&gt;
&lt;p&gt;using NetCoreCronSchedulerDemo.Jobs;&lt;br&gt;
using NetCoreCronSchedulerDemo.Model;&lt;br&gt;
using NetCoreCronSchedulerDemo.Sevice;&lt;/p&gt;
&lt;p&gt;class Program&lt;br&gt;
{&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static async Task Main(string[] args)
{
    // Build configuration
    var configuration = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile(&quot;appsettings.json&quot;, optional: false, reloadOnChange: true)
        .Build();

    // Setup DI
    var services = new ServiceCollection();
    services.AddLogging(config =&amp;gt; config.AddConsole());
    services.AddSingleton&amp;lt;IEmailService, EmailService&amp;gt;();
    services.AddSingleton&amp;lt;IReportService, ReportService&amp;gt;();

    var schedulerSettings = configuration.GetSection(&quot;SchedulerSettings&quot;).Get&amp;lt;SchedulerSettings&amp;gt;() ?? throw new InvalidOperationException(&quot;SchedulerSettings section is missing in appsettings.json.&quot;);
    
    services.AddSingleton(schedulerSettings);
    
    // Load jobs from config
    var cronJobsConfig = configuration.GetSection(&quot;CronJobs&quot;).Get&amp;lt;List&amp;lt;CronJobConfig&amp;gt;&amp;gt;();

    if (cronJobsConfig == null || cronJobsConfig.Count == 0)
    {
        throw new InvalidOperationException(&quot;No cron jobs configured in appsettings.json.&quot;);
    }

    foreach (var jobConfig in cronJobsConfig)
    {
        if (jobConfig.Name == &quot;EmailJob&quot;)
        {
            var emailService = services.BuildServiceProvider().GetService&amp;lt;IEmailService&amp;gt;() 
                ?? throw new InvalidOperationException(&quot;IEmailService is not registered in the service collection.&quot;);
            
            services.AddSingleton&amp;lt;ICronJob&amp;gt;(new EmailJob(jobConfig.CronExpression, emailService));
        }
        else if (jobConfig.Name == &quot;ReportJob&quot;)
        {
            var reportService = services.BuildServiceProvider().GetService&amp;lt;IReportService&amp;gt;() 
                ?? throw new InvalidOperationException(&quot;IReportService is not registered in the service collection.&quot;);

            services.AddSingleton&amp;lt;ICronJob&amp;gt;(new ReportJob(jobConfig.CronExpression, reportService));
        }

    }

    services.AddSingleton&amp;lt;Scheduler&amp;gt;();

    var provider = services.BuildServiceProvider();

    var scheduler = provider.GetRequiredService&amp;lt;Scheduler&amp;gt;();

    Console.WriteLine(&quot;========================================&quot;);

    Console.WriteLine(&quot;Console Cron Scheduler (Advanced) started.&quot;);

    Console.WriteLine(&quot;========================================&quot;);

    await scheduler.StartAsync(CancellationToken.None);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Run the application.

```bash
dotnet run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see something similar to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Console Cron Scheduler started.
Running ReportJob at 2025-11-28T18:00:00.0000000+02:00
Next run of ReportJob scheduled at 2025-11-28T18:01:00.0000000+02:00
Running EmailJob at 2025-11-28T18:00:00.0000000+02:00
Next run of EmailJob scheduled at 2025-11-28T20:00:00.0000000+02:00
[2025-11-28T18:00:02.0000000+02:00] ReportJob finished.
[2025-11-28T18:00:05.0000000+02:00] EmailJob finished.
...
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-understanding-the-flow&quot;&gt;8. Understanding the flow&lt;/h3&gt;
&lt;p&gt;To recap how everything works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Program&lt;/code&gt; creates a list of &lt;code&gt;ICronJob&lt;/code&gt; implementations&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Scheduler&lt;/code&gt; wraps each job in a &lt;code&gt;ScheduledJob&lt;/code&gt; that holds the &lt;code&gt;CronExpression&lt;/code&gt; and &lt;code&gt;NextRun&lt;/code&gt; time&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The scheduler loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Looks at current time&lt;/li&gt;
&lt;li&gt;Checks which jobs are due&lt;/li&gt;
&lt;li&gt;Starts each due job in a &lt;code&gt;Task.Run&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Asks Cronos for the next occurrence and updates &lt;code&gt;NextRun&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The loop waits one second and repeats until &lt;code&gt;CancellationToken&lt;/code&gt; is cancelled&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;because-each-job-runs-on-its-own-task-a-long-running-email-job-will-not-block-a-quick-report-job-&quot;&gt;Because each job runs on its own task, a long running email job will not block a quick report job.&lt;/h3&gt;
&lt;h3 id=&quot;10-conclusion&quot;&gt;10. Conclusion&lt;/h3&gt;
&lt;p&gt;With a few small classes you have built a reusable cron scheduler around the Cronos library that can run multiple jobs in parallel, respect time zones, and shut down cleanly.&lt;/p&gt;
&lt;p&gt;Cronos gives you clean handling of cron expressions and their next occurrences, and your scheduler focuses on orchestrating job execution. This pattern can be reused in console apps, worker services, or even as part of a larger ASP.NET Core backend.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/NetCoreCronSchedulerDemo&quot;&gt;Source Code : https://github.com/stevsharp/NetCoreCronSchedulerDemo&lt;/a&gt;&lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/7920/building-a-simple-cron-scheduler-in-net-with-cronos</guid>
<pubDate>Fri, 28 Nov 2025 18:29:26 +0000</pubDate>
</item>
<item>
<title>Tutorial: Building a .NET 9 Console App with Hangfire and Channels</title>
<link>https://coderlegion.com/7559/tutorial-building-a-net-9-console-app-with-hangfire-and-channels</link>
<description>&lt;h2 id=&quot;tutorial-building-a-net-9-console-app-with-hangfire-and-channels&quot;&gt;Tutorial: Building a .NET 9 Console App with Hangfire and Channels&lt;/h2&gt;
&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;In this tutorial, you will build a .NET 8 console application that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Polls data from an API, or simulates it&lt;/li&gt;
&lt;li&gt;Uses &lt;strong&gt;System.Threading.Channels&lt;/strong&gt; for producer consumer messaging&lt;/li&gt;
&lt;li&gt;Runs background tasks using &lt;strong&gt;Hosted Services&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Enqueues jobs in &lt;strong&gt;Hangfire&lt;/strong&gt; using in memory storage&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-use-system-threading-channels&quot;&gt;Why Use System.Threading.Channels&lt;/h3&gt;
&lt;p&gt;Traditional producer consumer patterns often rely on &lt;strong&gt;BlockingCollection&lt;/strong&gt; or custom queues, which can introduce:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Thread contention&lt;/li&gt;
&lt;li&gt;Blocking calls&lt;/li&gt;
&lt;li&gt;Complex synchronization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;System.Threading.Channels&lt;/code&gt; provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Asynchronous, non blocking communication between producers and consumers&lt;/li&gt;
&lt;li&gt;Built in backpressure when using bounded channels&lt;/li&gt;
&lt;li&gt;High performance with low allocation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DataPollingService&lt;/strong&gt; is the producer, it writes work items into the channel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ChannelConsumerService&lt;/strong&gt; is the consumer, it reads items and enqueues Hangfire jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-hosted-services&quot;&gt;Why Hosted Services&lt;/h3&gt;
&lt;p&gt;Hosted services, &lt;code&gt;BackgroundService&lt;/code&gt;, integrate with the .NET Generic Host. You get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Graceful lifecycle management, start and stop&lt;/li&gt;
&lt;li&gt;Dependency injection&lt;/li&gt;
&lt;li&gt;Logging and configuration&lt;/li&gt;
&lt;li&gt;Non blocking background loops&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This app uses two hosted services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DataPollingService&lt;/code&gt; simulates fetching data and writes work items to the channel&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChannelConsumerService&lt;/code&gt; reads from the channel and enqueues Hangfire jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-backgroundservice&quot;&gt;What is &lt;code&gt;BackgroundService&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BackgroundService&lt;/code&gt; is a base class that implements &lt;code&gt;IHostedService&lt;/code&gt; and gives you a long running &lt;code&gt;ExecuteAsync&lt;/code&gt; loop. It plugs into the Generic Host, so you get DI, logging, and coordinated shutdown.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use cases&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Poll APIs or message brokers&lt;/li&gt;
&lt;li&gt;Drain a queue or channel&lt;/li&gt;
&lt;li&gt;Run internal maintenance tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Key points&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;ExecuteAsync(CancellationToken)&lt;/code&gt; as your main loop&lt;/li&gt;
&lt;li&gt;Respect the cancellation token for fast shutdown&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;PeriodicTimer&lt;/code&gt; over &lt;code&gt;Task.Delay&lt;/code&gt; in recurring loops&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-hangfire&quot;&gt;What is Hangfire&lt;/h3&gt;
&lt;p&gt;Hangfire is a background job processor for .NET. You enqueue a method call, Hangfire persists and executes it with workers. It supports fire and forget, delayed, recurring, continuations, and batches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it helps here&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The consumer stays lightweight, it only enqueues work&lt;/li&gt;
&lt;li&gt;Processing is retried and observable with a dashboard&lt;/li&gt;
&lt;li&gt;You can swap storage later for durability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Common job types&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fire and forget, &lt;code&gt;BackgroundJob.Enqueue(() =&amp;gt; DoWork())&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Delayed, &lt;code&gt;BackgroundJob.Schedule(() =&amp;gt; DoWork(), TimeSpan.FromMinutes(5))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Recurring, &lt;code&gt;RecurringJob.AddOrUpdate(&quot;nightly&quot;, () =&amp;gt; DoWork(), &quot;0 2 * * *&quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notes, in memory storage is fine for demos, not for production. Jobs should be idempotent.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-system-threading-channels&quot;&gt;What is &lt;code&gt;System.Threading.Channels&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Channels are high performance, async friendly, in process queues. You get a &lt;code&gt;ChannelWriter&amp;lt;T&amp;gt;&lt;/code&gt; for producers and a &lt;code&gt;ChannelReader&amp;lt;T&amp;gt;&lt;/code&gt; for consumers. Channels work with &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;, and avoid blocking threads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why they shine&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Async reads and writes&lt;/li&gt;
&lt;li&gt;Bounded channels provide backpressure&lt;/li&gt;
&lt;li&gt;Options like &lt;code&gt;SingleReader&lt;/code&gt; and &lt;code&gt;SingleWriter&lt;/code&gt; reduce overhead when true&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Fast read loop&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;while (await reader.WaitToReadAsync(ct))
    while (reader.TryRead(out var item))
        await HandleAsync(item, ct);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;steps-to-build&quot;&gt;Steps to Build&lt;/h3&gt;
&lt;h4 id=&quot;1-create-the-project&quot;&gt;1. Create the Project&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet new console -n CustomerSyncConsole
cd CustomerSyncConsole
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-add-nuget-packages-1&quot;&gt;2. Add NuGet Packages&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet add package Hangfire
dotnet add package Hangfire.MemoryStorage
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Logging.Console
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-implement-the-code&quot;&gt;3. Implement the Code&lt;/h4&gt;
&lt;p&gt;Below is a complete &lt;code&gt;Program.cs&lt;/code&gt; with comments. It uses an unbounded channel for simplicity. See the optional bounded channel section that follows if you want backpressure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;using System.Threading.Channels;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace CustomerSyncConsole;

public sealed record CustomerWorkItem(int CustomerId, string PayloadId, DateTimeOffset ReceivedAt, string RawJson);

public interface ICustomerJobs
{
    Task ProcessCustomer(int customerId, string payloadId, CancellationToken ct = default);
}

public sealed class CustomerJobs(ILogger&amp;lt;CustomerJobs&amp;gt; logger) : ICustomerJobs
{
    private readonly ILogger&amp;lt;CustomerJobs&amp;gt; _logger = logger;

    public async Task ProcessCustomer(int customerId, string payloadId, CancellationToken ct = default)
    {
        _logger.LogInformation(&quot;Hangfire job started for Customer {CustomerId}, Payload {PayloadId}&quot;, customerId, payloadId);
        await Task.Delay(TimeSpan.FromSeconds(2), ct);
        _logger.LogInformation(&quot;Hangfire job completed for Customer {CustomerId}, Payload {PayloadId}&quot;, customerId, payloadId);
    }
}

public interface IStorage
{
    Task SaveAsync(string payloadId, string rawJson, CancellationToken ct);
}

public sealed class InMemoryStorage(ILogger&amp;lt;InMemoryStorage&amp;gt; logger) : IStorage
{
    private readonly ILogger&amp;lt;InMemoryStorage&amp;gt; _logger = logger;
    private readonly Dictionary&amp;lt;string, string&amp;gt; _store = new();

    public Task SaveAsync(string payloadId, string rawJson, CancellationToken ct)
    {
        _store[payloadId] = rawJson;
        _logger.LogInformation(&quot;Saved payload {PayloadId} in-memory (size={Size}).&quot;, payloadId, _store.Count);
        return Task.CompletedTask;
    }
}

public sealed class DataPollingService(
    ILogger&amp;lt;DataPollingService&amp;gt; logger,
    Channel&amp;lt;CustomerWorkItem&amp;gt; channel,
    IConfiguration config,
    IStorage storage) : BackgroundService
{
    private readonly ILogger&amp;lt;DataPollingService&amp;gt; _logger = logger;
    private readonly ChannelWriter&amp;lt;CustomerWorkItem&amp;gt; _writer = channel.Writer;
    private readonly IConfiguration _config = config;
    private readonly IStorage _storage = storage;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var intervalSeconds = _config.GetValue(&quot;Polling:IntervalSeconds&quot;, 10);
        var timer = new PeriodicTimer(TimeSpan.FromSeconds(intervalSeconds));

        _logger.LogInformation(&quot;Polling started. Interval={Interval}s&quot;, intervalSeconds);

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                var simulated = new[]
                {
                    new IncomingDto { CustomerId = 1, PayloadId = Guid.NewGuid().ToString(&quot;N&quot;) },
                    new IncomingDto { CustomerId = 2, PayloadId = Guid.NewGuid().ToString(&quot;N&quot;) },
                };

                foreach (var dto in simulated)
                {
                    var raw = System.Text.Json.JsonSerializer.Serialize(dto);
                    await _storage.SaveAsync(dto.PayloadId, raw, stoppingToken);

                    var item = new CustomerWorkItem(dto.CustomerId, dto.PayloadId, DateTimeOffset.UtcNow, raw);
                    await _writer.WriteAsync(item, stoppingToken);
                    _logger.LogInformation(&quot;Queued work item for Customer {CustomerId}, Payload {PayloadId}&quot;, dto.CustomerId, dto.PayloadId);
                }
            }
        }
        finally
        {
            _writer.TryComplete();
        }
    }

    private sealed class IncomingDto
    {
        public int CustomerId { get; set; }
        public string PayloadId { get; set; } = Guid.NewGuid().ToString(&quot;N&quot;);
    }
}

public sealed class ChannelConsumerService(
    Channel&amp;lt;CustomerWorkItem&amp;gt; channel,
    IBackgroundJobClient backgroundJobClient,
    ILogger&amp;lt;ChannelConsumerService&amp;gt; logger) : BackgroundService
{
    private readonly ChannelReader&amp;lt;CustomerWorkItem&amp;gt; _reader = channel.Reader;
    private readonly IBackgroundJobClient _backgroundJobClient = backgroundJobClient;
    private readonly ILogger&amp;lt;ChannelConsumerService&amp;gt; _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(&quot;Channel consumer started.&quot;);
        await foreach (var item in _reader.ReadAllAsync(stoppingToken))
        {
            if (item.CustomerId == 1)
            {
                var jobId = _backgroundJobClient.Enqueue&amp;lt;ICustomerJobs&amp;gt;(job =&amp;gt; job.ProcessCustomer(item.CustomerId, item.PayloadId, CancellationToken.None));
                _logger.LogInformation(&quot;Enqueued Hangfire job {JobId} for Customer {CustomerId}, Payload {PayloadId}&quot;, jobId, item.CustomerId, item.PayloadId);
            }
        }
        _logger.LogInformation(&quot;Channel consumer stopping.&quot;);
    }
}

public static class Program
{
    public static async Task Main(string[] args)
    {
        using var host = Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration(cfg =&amp;gt;
            {
                cfg.AddInMemoryCollection(new Dictionary&amp;lt;string, string?&amp;gt;
                {
                    [&quot;Polling:IntervalSeconds&quot;] = &quot;10&quot;
                });
            })
            .ConfigureLogging(logging =&amp;gt;
            {
                logging.ClearProviders();
                logging.AddSimpleConsole(o =&amp;gt;
                {
                    o.TimestampFormat = &quot;HH:mm:ss &quot;;
                    o.SingleLine = true;
                });
                logging.SetMinimumLevel(LogLevel.Information);
            })
            .ConfigureServices((ctx, services) =&amp;gt;
            {
                // Unbounded for simplicity in this tutorial
                services.AddSingleton(Channel.CreateUnbounded&amp;lt;CustomerWorkItem&amp;gt;());

                // Hangfire in memory
                services.AddHangfire(config =&amp;gt; config.UseMemoryStorage());
                services.AddHangfireServer();

                services.AddSingleton&amp;lt;ICustomerJobs, CustomerJobs&amp;gt;();
                services.AddSingleton&amp;lt;IStorage, InMemoryStorage&amp;gt;();
                services.AddHostedService&amp;lt;DataPollingService&amp;gt;();
                services.AddHostedService&amp;lt;ChannelConsumerService&amp;gt;();
            })
            .Build();

        await host.RunAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;optional-switch-to-a-bounded-channel&quot;&gt;Optional, switch to a bounded channel&lt;/h3&gt;
&lt;p&gt;For services that can receive spikes, prefer bounded channels to get backpressure.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;services.AddSingleton(_ =&amp;gt;
{
    var opts = new BoundedChannelOptions(capacity: 512)
    {
        FullMode = BoundedChannelFullMode.Wait,
        SingleReader = true,   // one consumer service
        SingleWriter = true    // one producer service
    };
    return Channel.CreateBounded&amp;lt;CustomerWorkItem&amp;gt;(opts);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the unbounded registration with the code above.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;best-practices&quot;&gt;Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pass &lt;code&gt;CancellationToken&lt;/code&gt; to every &lt;code&gt;WriteAsync&lt;/code&gt; and &lt;code&gt;ReadAllAsync&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Complete the writer during shutdown, then await reader completion&lt;/li&gt;
&lt;li&gt;Make Hangfire jobs idempotent, retries can occur&lt;/li&gt;
&lt;li&gt;Consider persistent Hangfire storage for real workloads, for example SQL Server or SQLite&lt;/li&gt;
&lt;li&gt;Expose channel depth as a metric if you need visibility, current size versus capacity&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;run-it&quot;&gt;Run it&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dotnet run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see logs from both services. The producer writes items into the channel. The consumer enqueues Hangfire jobs for customer 1. The Hangfire server executes those jobs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/CustomerJobRunner&quot;&gt;Source Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/dotnet/api/system.threading.channels&quot;&gt;System.Threading.Channels Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/aspnet/core/fundamentals/host/hosted-services&quot;&gt;BackgroundService and Hosted Services in .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.hangfire.io/&quot;&gt;Hangfire Official Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/7559/tutorial-building-a-net-9-console-app-with-hangfire-and-channels</guid>
<pubDate>Thu, 20 Nov 2025 07:56:42 +0000</pubDate>
</item>
<item>
<title>Decorate services in ASP.NET Core, step by step</title>
<link>https://coderlegion.com/7347/decorate-services-in-asp-net-core-step-by-step</link>
<description>&lt;p&gt;Decorator pattern, in short&lt;/p&gt;
&lt;p&gt;The decorator pattern is a structural design pattern that lets you add behavior to an object at runtime by wrapping it with another object that has the same interface. The wrapper forwards calls to the inner object, then adds extra work before or after the call. You keep the original contract, you can stack multiple decorators, and you avoid subclass explosion by preferring composition over inheritance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key points&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Intent, attach additional responsibilities to an object dynamically&lt;/li&gt;
&lt;li&gt;How, wrap an object that implements the same interface and forward&lt;br&gt;
   calls&lt;/li&gt;
&lt;li&gt;Good for cross cutting concerns like logging, retry, caching,&lt;br&gt;
metrics, authorization&lt;/li&gt;
&lt;li&gt;Benefits, core code stays simple, behaviors are pluggable and&lt;br&gt;
testable, decorators compose&lt;/li&gt;
&lt;li&gt;Trade offs, more indirection and wiring, the order of decorators&lt;br&gt;
matters&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Why decorate in ASP.NET Core&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Decoration lets you add cross cutting behavior around a service without touching its core code, for example retry, logging, metrics, caching. It keeps concerns separated and easy to test.&lt;/p&gt;
&lt;p&gt;Packages&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Scrutor
dotnet add package Polly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Core service&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;

public interface IInvoicesClient
{
    Task&amp;lt;PagedResultDtoOfInvoice&amp;gt; GetInvoicesAsync(
        string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding,
        CancellationToken ct = default);
}

public sealed class InvoicesClient : IInvoicesClient
{
    private readonly HttpClient _http;
    private readonly string _baseUrl;
    private readonly Func&amp;lt;string&amp;gt; _getToken;
    private static readonly JsonSerializerOptions _json = new() { PropertyNameCaseInsensitive = true };

    public InvoicesClient(HttpClient http, string baseUrl, Func&amp;lt;string&amp;gt; getToken)
    {
        _http = http;
        _baseUrl = baseUrl.TrimEnd('/');
        _getToken = getToken;
    }

    public async Task&amp;lt;PagedResultDtoOfInvoice&amp;gt; GetInvoicesAsync(
        string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding,
        CancellationToken ct = default)
    {
        var url = BuildUrl(_baseUrl, filter, orderBy, include, pageIndex, pageSize, transactionCurrency, skipRounding);

        using var req = new HttpRequestMessage(HttpMethod.Get, url)
        {
#if NET5_0_OR_GREATER
            VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
#endif
        };

        var token = _getToken?.Invoke();
        if (!string.IsNullOrEmpty(token))
            req.Headers.Authorization = new AuthenticationHeaderValue(&quot;Bearer&quot;, token);

        using var resp = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false);
        resp.EnsureSuccessStatusCode();

        await using var stream = await resp.Content.ReadAsStreamAsync(ct).ConfigureAwait(false);
        return await JsonSerializer.DeserializeAsync&amp;lt;PagedResultDtoOfInvoice&amp;gt;(stream, _json, ct).ConfigureAwait(false)
               ?? new PagedResultDtoOfInvoice();
    }

    private static string BuildUrl(
        string baseUrl, string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding)
    {
        var q = System.Web.HttpUtility.ParseQueryString(string.Empty);
        if (!string.IsNullOrWhiteSpace(filter)) q[&quot;filter&quot;] = filter;
        if (!string.IsNullOrWhiteSpace(orderBy)) q[&quot;orderBy&quot;] = orderBy;
        if (!string.IsNullOrWhiteSpace(include)) q[&quot;include&quot;] = include;
        if (pageIndex.HasValue) q[&quot;pageIndex&quot;] = pageIndex.Value.ToString();
        if (pageSize.HasValue) q[&quot;pageSize&quot;] = pageSize.Value.ToString();
        if (transactionCurrency.HasValue) q[&quot;transactionCurrency&quot;] = transactionCurrency.Value ? &quot;true&quot; : &quot;false&quot;;
        if (skipRounding.HasValue) q[&quot;skipRounding&quot;] = skipRounding.Value ? &quot;true&quot; : &quot;false&quot;;
        return $&quot;{baseUrl}/invoices?{q}&quot;;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Decorators&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Polly;
using System.Net.Sockets;

public sealed class RetryInvoicesClientDecorator : IInvoicesClient
{
    private readonly IInvoicesClient _inner;
    private readonly IAsyncPolicy _retry;

    public RetryInvoicesClientDecorator(IInvoicesClient inner)
    {
        _inner = inner;
        _retry = Policy
            .Handle&amp;lt;HttpRequestException&amp;gt;()
            .Or&amp;lt;IOException&amp;gt;()
            .Or&amp;lt;SocketException&amp;gt;()
            .Or&amp;lt;TaskCanceledException&amp;gt;(ex =&amp;gt; !ex.CancellationToken.IsCancellationRequested)
            .WaitAndRetryAsync(3,
                attempt =&amp;gt; TimeSpan.FromSeconds(Math.Pow(2, attempt)) +
                           TimeSpan.FromMilliseconds(Random.Shared.Next(0, 750)));
    }

    public Task&amp;lt;PagedResultDtoOfInvoice&amp;gt; GetInvoicesAsync(
        string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding,
        CancellationToken ct = default)
    =&amp;gt;
        _retry.ExecuteAsync(t =&amp;gt;
            _inner.GetInvoicesAsync(filter, orderBy, include, pageIndex, pageSize, transactionCurrency, skipRounding, t),
            ct);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Logging decorator&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class LoggingInvoicesClientDecorator : IInvoicesClient
{
    private readonly IInvoicesClient _inner;
    private readonly ILogger&amp;lt;LoggingInvoicesClientDecorator&amp;gt; _log;

    public LoggingInvoicesClientDecorator(IInvoicesClient inner, ILogger&amp;lt;LoggingInvoicesClientDecorator&amp;gt; log)
    { _inner = inner; _log = log; }

    public async Task&amp;lt;PagedResultDtoOfInvoice&amp;gt; GetInvoicesAsync(
        string filter, string orderBy, string include,
        int? pageIndex, int? pageSize, bool? transactionCurrency, bool? skipRounding,
        CancellationToken ct = default)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        try
        {
            _log.LogInformation(&quot;Invoices request pageIndex={PageIndex} pageSize={PageSize}&quot;, pageIndex, pageSize);
            var res = await _inner.GetInvoicesAsync(filter, orderBy, include, pageIndex, pageSize, transactionCurrency, skipRounding, ct);
            _log.LogInformation(&quot;Invoices response count={Count} in {Elapsed} ms&quot;, res?.Data?.Count ?? 0, sw.ElapsedMilliseconds);
            return res;
        }
        catch (Exception ex)
        {
            _log.LogError(ex, &quot;Invoices failed in {Elapsed} ms&quot;, sw.ElapsedMilliseconds);
            throw;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Registration with Scrutor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Scrutor;
using System.Net;

builder.Services.AddHttpClient(&quot;Invoices&quot;, c =&amp;gt;
{
    c.Timeout = Timeout.InfiniteTimeSpan;
    c.DefaultRequestHeaders.Accept.ParseAdd(&quot;application/json&quot;);
})
.ConfigurePrimaryHttpMessageHandler(() =&amp;gt; new SocketsHttpHandler
{
    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
    PooledConnectionLifetime = TimeSpan.FromMinutes(2),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
    MaxConnectionsPerServer = 8
});

builder.Services.AddTransient&amp;lt;IInvoicesClient&amp;gt;(sp =&amp;gt;
{
    var http = sp.GetRequiredService&amp;lt;IHttpClientFactory&amp;gt;().CreateClient(&quot;Invoices&quot;);
    var cfg = sp.GetRequiredService&amp;lt;IConfiguration&amp;gt;();
    var baseUrl = cfg[&quot;Erp:BaseUrl&quot;] ?? &quot;&quot;;
    var tokenProvider = sp.GetRequiredService&amp;lt;ITokenProvider&amp;gt;();
    return new InvoicesClient(http, baseUrl, () =&amp;gt; tokenProvider.GetToken());
});

builder.Services.Decorate&amp;lt;IInvoicesClient, LoggingInvoicesClientDecorator&amp;gt;();
builder.Services.Decorate&amp;lt;IInvoicesClient, RetryInvoicesClientDecorator&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Native DI without Scrutor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Services.AddHttpClient(&quot;Invoices&quot;, c =&amp;gt;
{
    c.Timeout = Timeout.InfiniteTimeSpan;
    c.DefaultRequestHeaders.Accept.ParseAdd(&quot;application/json&quot;);
});

builder.Services.AddTransient&amp;lt;IInvoicesClient&amp;gt;(sp =&amp;gt;
{
    var http = sp.GetRequiredService&amp;lt;IHttpClientFactory&amp;gt;().CreateClient(&quot;Invoices&quot;);
    var cfg = sp.GetRequiredService&amp;lt;IConfiguration&amp;gt;();
    var baseUrl = cfg[&quot;Erp:BaseUrl&quot;] ?? &quot;&quot;;
    var tokenProvider = sp.GetRequiredService&amp;lt;ITokenProvider&amp;gt;();

    IInvoicesClient svc = new InvoicesClient(http, baseUrl, () =&amp;gt; tokenProvider.GetToken());
    svc = new LoggingInvoicesClientDecorator(svc, sp.GetRequiredService&amp;lt;ILogger&amp;lt;LoggingInvoicesClientDecorator&amp;gt;&amp;gt;());
    svc = new RetryInvoicesClientDecorator(svc);
    return svc;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DelegatingHandler for transport concerns&lt;/p&gt;
&lt;p&gt;Use handlers for wire level logic, keep decorators for service level logic.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class LoggingHandler : DelegatingHandler
{
    private readonly ILogger&amp;lt;LoggingHandler&amp;gt; _log;
    public LoggingHandler(ILogger&amp;lt;LoggingHandler&amp;gt; log) =&amp;gt; _log = log;

    protected override async Task&amp;lt;HttpResponseMessage&amp;gt; SendAsync(HttpRequestMessage request, CancellationToken ct)
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();
        var response = await base.SendAsync(request, ct);
        _log.LogInformation(&quot;{Method} {Uri} -&amp;gt; {Status} in {Elapsed} ms&quot;,
            request.Method, request.RequestUri, (int)response.StatusCode, sw.ElapsedMilliseconds);
        return response;
    }
}

builder.Services.AddTransient&amp;lt;LoggingHandler&amp;gt;();
builder.Services.AddHttpClient(&quot;Invoices&quot;, c =&amp;gt; { /* defaults above */ })
    .AddHttpMessageHandler&amp;lt;LoggingHandler&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Best practices checklist&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;One retry layer only.&lt;/li&gt;
&lt;li&gt;Per call timeout with CancelAfter.&lt;/li&gt;
&lt;li&gt;ResponseHeadersRead for large payloads.&lt;/li&gt;
&lt;li&gt;Small page size for large ranges.&lt;/li&gt;
&lt;li&gt;Keep HttpClient immutable after registration.&lt;/li&gt;
&lt;li&gt;Put auth headers on the request message.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;References&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/App-vNext/Polly&quot;&gt;Polly, official docs and GitHub.&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/khellang/Scrutor&quot;&gt;Scrutor, GitHub and NuGet.&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory&quot;&gt;Microsoft docs, HttpClientFactory with resilience patterns.&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/AspNetCore.DecoratorSamples&quot;&gt;Source Code&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/7347/decorate-services-in-asp-net-core-step-by-step</guid>
<pubDate>Fri, 14 Nov 2025 11:21:56 +0000</pubDate>
</item>
<item>
<title>Global Exception Handler vs Custom Middleware in ASP.NET Core</title>
<link>https://coderlegion.com/7028/global-exception-handler-vs-custom-middleware-in-asp-net-core</link>
<description>&lt;p&gt;&lt;strong&gt;The problem we’re solving&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Stuff goes wrong: timeouts, not-found resources, validation fails. You want one consistent error response and clean logs, without sprinkling try/catch everywhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two good options&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Option A — Global Exception Handler (recommended default)&lt;/p&gt;
&lt;p&gt;Think of it as the framework’s built-in safety net. You plug in one handler and ASP.NET Core turns exceptions into ProblemDetails JSON automatically.&lt;/p&gt;
&lt;p&gt;Why people like it&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Minimal code, fewer edge cases. &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Plays nice with content negotiation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One central place to map exception → HTTP status.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use when: you want &lt;strong&gt;standard errors&lt;/strong&gt; and consistency across the app.&lt;/p&gt;
&lt;p&gt;Option B — Custom Exception Middleware&lt;/p&gt;
&lt;p&gt;Your own IMiddleware that wraps the pipeline in &lt;strong&gt;try/catch&lt;/strong&gt; and writes whatever JSON shape you want.&lt;/p&gt;
&lt;p&gt;Why people use it&lt;/p&gt;
&lt;p&gt;-Full control over the response body and headers.&lt;/p&gt;
&lt;p&gt;-Easy to keep a legacy “envelope” format.&lt;/p&gt;
&lt;p&gt;-Use when: clients require a specific, non-ProblemDetails format or you need special side effects.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decision guide&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;-Standard JSON errors and less boilerplate → Global handler&lt;/p&gt;
&lt;p&gt;-Legacy/bespoke response body → Middleware&lt;/p&gt;
&lt;p&gt;-Want a single place to map exceptions to statuses → Global handler&lt;/p&gt;
&lt;p&gt;-Heavy custom logic on errors (headers, counters) → Middleware&lt;/p&gt;
&lt;p&gt;-Using MediatR and want clean handlers → Use a MediatR behavior to log, then rethrow so the global handler shapes the response&lt;/p&gt;
&lt;p&gt;1) Global handler wiring (Program.cs)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Services.AddExceptionHandler&amp;lt;GlobalExceptionHandler&amp;gt;();

app.UseExceptionHandler(); // put this early in the pipeline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The handler itself (GlobalExceptionHandler.cs)&lt;/p&gt;
&lt;p&gt;using Microsoft.AspNetCore.Diagnostics;&lt;br&gt;
using Microsoft.AspNetCore.Mvc;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger&amp;lt;GlobalExceptionHandler&amp;gt; _logger;
    private readonly IProblemDetailsService _pds;
    private readonly IHostEnvironment _env;

    public GlobalExceptionHandler(ILogger&amp;lt;GlobalExceptionHandler&amp;gt; logger, IProblemDetailsService pds, IHostEnvironment env)
        =&amp;gt; (_logger, _pds, _env) = (logger, pds, env);

    public async ValueTask&amp;lt;bool&amp;gt; TryHandleAsync(HttpContext http, Exception ex, CancellationToken ct)
    {
        var (status, title) = ex switch
        {
            ValidationException =&amp;gt; (422, &quot;Validation failed&quot;),
            NotFoundException   =&amp;gt; (404, &quot;Not found&quot;),
            UnauthorizedAccessException =&amp;gt; (401, &quot;Unauthorized&quot;),
            TimeoutException    =&amp;gt; (503, &quot;Service unavailable&quot;),
            Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException =&amp;gt; (409, &quot;Concurrency conflict&quot;),
            TaskCanceledException =&amp;gt; (408, &quot;Request timeout&quot;),
            OperationCanceledException =&amp;gt; (StatusCodes.Status499ClientClosedRequest, &quot;Client closed request&quot;),
            HttpRequestException =&amp;gt; (502, &quot;Bad gateway&quot;),
            _ =&amp;gt; (500, &quot;Unexpected error&quot;)
        };

        if (status is 503 or 408) http.Response.Headers.TryAdd(&quot;Retry-After&quot;, &quot;5&quot;);

        http.Response.StatusCode = status;

        var pd = new ProblemDetails
        {
            Status = status,
            Title = title,
            Detail = _env.IsDevelopment() ? ex.Message : null,
            Instance = http.Request.Path
        };
        if (ex is ValidationException ve) pd.Extensions[&quot;errors&quot;] = ve.Errors;

        _logger.LogError(ex, &quot;Unhandled exception -&amp;gt; HTTP {Status}&quot;, status);

        return await _pds.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = http,
            ProblemDetails = pd,
            Exception = ex
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Custom middleware skeleton&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class ExceptionEnvelopeMiddleware : IMiddleware
{
    private readonly ILogger&amp;lt;ExceptionEnvelopeMiddleware&amp;gt; _logger;
    private readonly IHostEnvironment _env;
    public ExceptionEnvelopeMiddleware(ILogger&amp;lt;ExceptionEnvelopeMiddleware&amp;gt; logger, IHostEnvironment env)
        =&amp;gt; (_logger, _env) = (logger, env);

    public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
    {
        try { await next(ctx); }
        catch (Exception ex)
        {
            if (ctx.Response.HasStarted) throw;
            _logger.LogError(ex, &quot;Unhandled exception&quot;);

            ctx.Response.Clear();
            ctx.Response.StatusCode = ex switch
            {
                ValidationException =&amp;gt; StatusCodes.Status422UnprocessableEntity,
                NotFoundException   =&amp;gt; StatusCodes.Status404NotFound,
                TimeoutException    =&amp;gt; StatusCodes.Status503ServiceUnavailable,
                _ =&amp;gt; StatusCodes.Status500InternalServerError
            };
            ctx.Response.ContentType = &quot;application/json&quot;;

            var body = new
            {
                title = ctx.Response.StatusCode == 422 ? &quot;Validation failed&quot; :
                        ctx.Response.StatusCode == 404 ? &quot;Not found&quot; :
                        ctx.Response.StatusCode == 503 ? &quot;Service unavailable&quot; : &quot;Unexpected error&quot;,
                status = ctx.Response.StatusCode,
                traceId = ctx.TraceIdentifier,
                detail = _env.IsDevelopment() ? ex.Message : null,
                errors = ex is ValidationException ve ? ve.Errors : null
            };
            await ctx.Response.WriteAsJsonAsync(body);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Using MediatR? Add a behavior&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;using MediatR;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class UnhandledExceptionBehavior&amp;lt;TRequest,TResponse&amp;gt; : IPipelineBehavior&amp;lt;TRequest,TResponse&amp;gt;
    where TRequest : notnull
{
    private readonly ILogger&amp;lt;UnhandledExceptionBehavior&amp;lt;TRequest,TResponse&amp;gt;&amp;gt; _logger;
    public UnhandledExceptionBehavior(ILogger&amp;lt;UnhandledExceptionBehavior&amp;lt;TRequest,TResponse&amp;gt;&amp;gt; logger) =&amp;gt; _logger = logger;

    public async Task&amp;lt;TResponse&amp;gt; Handle(TRequest request, RequestHandlerDelegate&amp;lt;TResponse&amp;gt; next, CancellationToken ct)
    {
        try { return await next(); }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Unhandled exception in MediatR for {Request}&quot;, typeof(TRequest).Name);
            throw; // let the global handler shape ProblemDetails
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Common pitfalls (and easy wins)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;-Place the handler early. Put UseExceptionHandler() before routing and authorization.&lt;/p&gt;
&lt;p&gt;-Don’t leak stack traces in production. Keep Detail empty unless in Development.&lt;/p&gt;
&lt;p&gt;-Streaming/gRPC: once the response starts, you can’t switch to JSON. Log and stop.&lt;/p&gt;
&lt;p&gt;-Map the big ones: validation → 422, not found → 404, timeouts → 503, concurrency → 409.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Takeaway&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you just want clean, predictable errors, use the &lt;strong&gt;global handle&lt;/strong&gt;r and be done. If your clients expect a specific JSON envelope, use a custom middleware. With MediatR, add a behavior to keep handlers tidy and let the handler (or middleware) do the final shaping.&lt;/p&gt;
&lt;p&gt;Further Reading :&lt;/p&gt;
&lt;p&gt;Error handling &amp;amp; ProblemDetails (official docs)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-9.0&amp;amp;utm_source=chatgpt.com&quot;&gt;Handle errors in ASP.NET Core (overview). &lt;br&gt;
Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.exceptionhandlerextensions.useexceptionhandler?view=aspnetcore-9.0&quot;&gt;Handle errors in ASP.NET Core APIs (includes UseExceptionHandler patterns). &lt;br&gt;
Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Middleware&lt;/strong&gt; (how it works &amp;amp; how to write your own)&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0&quot;&gt;Middleware fundamentals (ordering, short-circuiting). &lt;br&gt;
Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;MediatR pipeline behaviors&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/LuckyPennySoftware/MediatR&quot;&gt;Jimmy Bogard (creator) — pipeline posts &amp;amp; patterns. &lt;br&gt;
Jimmy Bogard&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://fluentvalidation.net/&quot;&gt;FluentValidation in ASP.NET Core (official docs) for a validation behavior that throws a ValidationException. &lt;br&gt;
fluentvalidation.net&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Source Code &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp?tab=repositories&quot;&gt;https://github.com/stevsharp?tab=repositories&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/7028/global-exception-handler-vs-custom-middleware-in-asp-net-core</guid>
<pubDate>Tue, 04 Nov 2025 09:38:14 +0000</pubDate>
</item>
<item>
<title>Reliable Messaging in .NET: Domain Events and the Outbox Pattern with EF Core Interceptors</title>
<link>https://coderlegion.com/6363/reliable-messaging-in-net-domain-events-and-the-outbox-pattern-with-ef-core-interceptors</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Distributed systems are powerful — but they come with one big challenge: &lt;strong&gt;reliability&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What happens if your application updates a record in the database and then tries to publish a message to a message broker… but crashes in between?&lt;br&gt;
You’ve just lost an event.&lt;/p&gt;
&lt;p&gt;This problem is common in microservices, and it’s why the &lt;strong&gt;Outbox Pattern&lt;/strong&gt; exists — a simple, elegant approach that guarantees message delivery without introducing distributed transactions or external coordinators.&lt;/p&gt;
&lt;p&gt;In this article, we’ll implement a &lt;strong&gt;Domain Event + Outbox Pattern&lt;/strong&gt; in &lt;strong&gt;.NET 9&lt;/strong&gt;, using &lt;strong&gt;Entity Framework Core&lt;/strong&gt;, &lt;strong&gt;SQLite&lt;/strong&gt;, and an &lt;strong&gt;EF Core SaveChangesInterceptor&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-a-quick-look-at-entity-framework-core&quot;&gt;⚙️ A Quick Look at Entity Framework Core&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Entity Framework Core (EF Core)&lt;/strong&gt; is Microsoft’s modern &lt;strong&gt;Object-Relational Mapper (ORM)&lt;/strong&gt; for .NET.&lt;br&gt;
It allows developers to work with a database using &lt;strong&gt;C# classes&lt;/strong&gt; instead of raw SQL, automatically handling relationships, migrations, and change tracking.&lt;/p&gt;
&lt;p&gt;When you call &lt;code&gt;SaveChanges()&lt;/code&gt; or &lt;code&gt;SaveChangesAsync()&lt;/code&gt;, EF Core:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Tracks your entity changes.&lt;/li&gt;
&lt;li&gt;Generates SQL commands.&lt;/li&gt;
&lt;li&gt;Executes them in a transaction.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This makes EF Core the perfect place to integrate the &lt;strong&gt;Outbox Pattern&lt;/strong&gt;, since we can hook into its save pipeline and ensure domain events are written in the same transaction as the data itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-are-interceptors-&quot;&gt;What Are Interceptors?&lt;/h3&gt;
&lt;p&gt;In EF Core, &lt;strong&gt;interceptors&lt;/strong&gt; are powerful hooks that allow you to intercept and extend the framework’s internal operations — such as saving, querying, or connecting to the database.&lt;/p&gt;
&lt;p&gt;They work like &lt;strong&gt;middleware for EF Core&lt;/strong&gt;:&lt;br&gt;
you can observe, modify, or add logic before and after EF performs certain actions.&lt;/p&gt;
&lt;p&gt;There are several types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SaveChangesInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommandInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConnectionInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TransactionInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this article, we’ll use &lt;strong&gt;SaveChangesInterceptor&lt;/strong&gt; to detect domain events before EF saves, serialize them, and insert them into our Outbox table.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;In traditional architectures, your service might do something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;await _db.SaveChangesAsync();
await _bus.PublishAsync(new OrderPlacedEvent(orderId));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your app crashes after saving but before publishing, your event never reaches other services — resulting in inconsistent state across systems.&lt;/p&gt;
&lt;p&gt;We need a way to ensure &lt;strong&gt;atomicity&lt;/strong&gt;: either both the data and the event are persisted, or neither are.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-outbox-pattern-in-a-nutshell-&quot;&gt;The Outbox Pattern (In a Nutshell)&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Outbox Pattern&lt;/strong&gt; solves this by introducing a dedicated &lt;strong&gt;Outbox table&lt;/strong&gt; inside your database.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When your aggregate raises a domain event, it’s &lt;strong&gt;captured and saved&lt;/strong&gt; into the Outbox table &lt;strong&gt;within the same transaction&lt;/strong&gt; as your entity.&lt;/li&gt;
&lt;li&gt;A background worker (or a separate service) &lt;strong&gt;polls&lt;/strong&gt; the Outbox table, &lt;strong&gt;publishes events&lt;/strong&gt;, and marks them as processed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This way, even if your app crashes, the events are safely stored and can be retried later.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;┌──────────────────────────────┐
│         Application          │
│ ──────────────────────────── │
│  Order raises DomainEvent    │
│  → EF Interceptor writes     │
│    Outbox row                │
│  → Transaction commits       │
└──────────────┬───────────────┘
               │
               ▼
        Outbox Dispatcher
       ├─ Polls database
       ├─ Publishes messages
       └─ Marks them processed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern elegantly blends &lt;strong&gt;Domain-Driven Design&lt;/strong&gt; concepts with &lt;strong&gt;event-driven reliability&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;domain-events&quot;&gt;Domain Events&lt;/h3&gt;
&lt;p&gt;Domain events capture something meaningful that happened in your domain model.&lt;br&gt;
For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed record OrderPlacedDomainEvent(Guid OrderId, decimal Total)
    : DomainEvent(DateTime.UtcNow);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And your aggregate raises them naturally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class Order : IHasDomainEvents
{
    private readonly List&amp;lt;DomainEvent&amp;gt; _domainEvents = new();
    public Guid Id { get; private set; } = Guid.NewGuid();
    public decimal Total { get; private set; }

    public IReadOnlyCollection&amp;lt;DomainEvent&amp;gt; DomainEvents =&amp;gt; _domainEvents;

    public Order(decimal total)
    {
        Total = total;
        AddEvent(new OrderPlacedDomainEvent(Id, Total));
    }

    private void AddEvent(DomainEvent @event) =&amp;gt; _domainEvents.Add(@event);
    public void ClearDomainEvents() =&amp;gt; _domainEvents.Clear();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-capturing-events-with-an-ef-core-interceptor&quot;&gt;⚙️ Capturing Events with an EF Core Interceptor&lt;/h3&gt;
&lt;p&gt;Instead of overriding &lt;code&gt;SaveChangesAsync()&lt;/code&gt; directly inside &lt;code&gt;AppDbContext&lt;/code&gt;, we can use a &lt;strong&gt;SaveChangesInterceptor&lt;/strong&gt; — a clean, composable way to plug custom logic into EF’s lifecycle.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;
public sealed class OutboxSaveChangesInterceptor(IEventSerializer serializer) : SaveChangesInterceptor
{
    /// &amp;lt;summary&amp;gt;
    /// 
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;eventData&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;result&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;cancellationToken&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public override async ValueTask&amp;lt;InterceptionResult&amp;lt;int&amp;gt;&amp;gt; SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult&amp;lt;int&amp;gt; result,
        CancellationToken cancellationToken = default)
    {
        var context = eventData.Context;
        if (context is null) 
            return await base.SavingChangesAsync(eventData, result, cancellationToken);

        var aggregates = context.ChangeTracker
                    .Entries()
                    .Where(e =&amp;gt; e.Entity is IHasDomainEvents hasEvents &amp;amp;&amp;amp; hasEvents.DomainEvents.Count &amp;gt; 0)
                    .Select(e =&amp;gt; (IHasDomainEvents)e.Entity)
                    .ToList();

        if (aggregates.Count == 0)
            return await base.SavingChangesAsync(eventData, result, cancellationToken);

        var outbox = context.Set&amp;lt;OutboxMessage&amp;gt;();

        foreach (var aggregate in aggregates)
        {
            foreach (var @event in aggregate.DomainEvents)
            {
                outbox.Add(new OutboxMessage
                {
                    OccurredOnUtc = @event.OccurredOnUtc,
                    Type = @event.GetType().AssemblyQualifiedName!,
                    Payload = serializer.Serialize(@event)
                });
            }
        }

        return await base.SavingChangesAsync(eventData, result, cancellationToken);
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now every time you call &lt;code&gt;SaveChangesAsync()&lt;/code&gt;, domain events are written to your &lt;strong&gt;OutboxMessages&lt;/strong&gt; table automatically — in the same transaction.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-outbox-dispatcher&quot;&gt;The Outbox Dispatcher&lt;/h3&gt;
&lt;p&gt;Once the transaction is committed, a background worker can safely process these messages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;
public sealed class OutboxDispatcher(IServiceProvider sp, 
    ILogger&amp;lt;OutboxDispatcher&amp;gt; logger) : BackgroundService
{
    private readonly IServiceProvider _sp = sp;
    private readonly ILogger&amp;lt;OutboxDispatcher&amp;gt; _logger = logger;
    private readonly TimeSpan _pollInterval = TimeSpan.FromSeconds(2);
    private const int BatchSize = 50;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(&quot; Outbox Dispatcher started...&quot;);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _sp.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService&amp;lt;AppDbContext&amp;gt;();

                var jsonSerializer = scope.ServiceProvider.GetRequiredService&amp;lt;IEventSerializer&amp;gt;();

                var pending = await db.OutboxMessages
                    .Where(x =&amp;gt; x.ProcessedOnUtc == null)
                    .OrderBy(x =&amp;gt; x.Id)
                    .Take(BatchSize)
                    .ToListAsync(stoppingToken);

                foreach (var msg in pending)
                {
                    try
                    {
                        var evt = jsonSerializer.Deserialize(msg.Payload, msg.Type);
                        await SimulatedBusPublish(evt, stoppingToken);
                        msg.ProcessedOnUtc = DateTime.UtcNow;
                        msg.Error = null;
                    }
                    catch (Exception ex)
                    {
                        msg.Error = ex.Message;
                        _logger.LogError(ex, &quot;❌ Error dispatching OutboxMessage {Id}&quot;, msg.Id);
                    }
                }

                if (pending.Count &amp;gt; 0)
                    await db.SaveChangesAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, &quot;Outbox dispatcher loop error&quot;);
            }

            await Task.Delay(_pollInterval, stoppingToken);
        }
    }

    private Task SimulatedBusPublish(DomainEvent evt, CancellationToken ct)
        =&amp;gt; evt switch
        {
            OrderPlacedDomainEvent e =&amp;gt; HandleOrderPlaced(e),
            _ =&amp;gt; Task.CompletedTask
        };

    private Task HandleOrderPlaced(OrderPlacedDomainEvent e)
    {
        Console.WriteLine($&quot;[BUS] ✅ OrderPlaced published =&amp;gt; OrderId={e.OrderId}, Total={e.Total}&quot;);

        return Task.CompletedTask;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-use-an-interceptor-&quot;&gt;Why Use an Interceptor?&lt;/h3&gt;
&lt;p&gt;Using an &lt;strong&gt;interceptor&lt;/strong&gt; instead of overriding &lt;code&gt;SaveChangesAsync()&lt;/code&gt; has several advantages:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Approach                      &lt;/th&gt;&lt;th&gt;Pros                          &lt;/th&gt;&lt;th&gt;Cons&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Override SaveChangesAsync&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Simple to start               &lt;/td&gt;&lt;td&gt;Harder to reuse across contexts&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Use Interceptor&lt;/strong&gt;           &lt;/td&gt;&lt;td&gt;Decoupled, reusable, testable &lt;/td&gt;&lt;td&gt;Slightly more setup&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Interceptors follow the &lt;strong&gt;Open/Closed Principle&lt;/strong&gt; — your DbContext remains focused on persistence, while cross-cutting logic (like event logging, auditing, or outbox handling) is easily attachable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;benefits-of-the-outbox-pattern&quot;&gt;Benefits of the Outbox Pattern&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Transactional Safety&lt;/strong&gt; — data + events saved atomically&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Guaranteed Delivery&lt;/strong&gt; — retries handled by dispatcher&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;DDD Alignment&lt;/strong&gt; — aggregates raise domain events naturally&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Resilience&lt;/strong&gt; — works even if message broker is offline&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Extensibility&lt;/strong&gt; — can integrate with RabbitMQ, Kafka, or Azure Service Bus later&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references-1&quot;&gt;References&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&quot;&gt;Microsoft Docs – EF Core Interceptors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/articles/patterns-of-distributed-systems/transactional-outbox.html&quot;&gt;Martin Fowler – Transactional Outbox Pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://udidahan.com/2008/05/14/when-to-use-sagas/&quot;&gt;Udi Dahan – Reliable Messaging without Distributed Transactions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/core/extensions/workers&quot;&gt;Microsoft Docs – BackgroundService in .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.jimmybogard.com/domain-events-outbox-pattern/&quot;&gt;Jimmy Bogard – Domain Events and the Outbox Pattern in .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/OutboxPattern&quot;&gt;Source Code &lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-closing-thoughts&quot;&gt;✨ Closing Thoughts&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Domain Events + Outbox Pattern&lt;/strong&gt; is one of the most practical ways to achieve &lt;strong&gt;reliable, eventually consistent event-driven systems&lt;/strong&gt; in .NET.&lt;/p&gt;
&lt;p&gt;By combining &lt;strong&gt;Entity Framework Core&lt;/strong&gt;, &lt;strong&gt;SaveChangesInterceptor&lt;/strong&gt;, and a simple &lt;strong&gt;background dispatcher&lt;/strong&gt;, you can ensure every event is persisted and delivered — even in the face of failure.&lt;/p&gt;
&lt;p&gt;Small pattern. Big reliability. &lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;author&quot;&gt;Author&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Spyros Ponaris&lt;/strong&gt;&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/spyros-ponaris-913a6937/&quot;&gt;LinkedIn&lt;/a&gt;&lt;br&gt;
 Senior Software Engineer &amp;amp; .NET Enthusiast&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/6363/reliable-messaging-in-net-domain-events-and-the-outbox-pattern-with-ef-core-interceptors</guid>
<pubDate>Mon, 13 Oct 2025 11:27:53 +0000</pubDate>
</item>
<item>
<title>Supercharging EF Core Specifications with EF.CompileQuery</title>
<link>https://coderlegion.com/4443/supercharging-ef-core-specifications-with-ef-compilequery</link>
<description>&lt;ol&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you’re using EF Core in production, query compilation can quietly become a performance bottleneck — especially if the same query is executed thousands of times.&lt;/p&gt;
&lt;p&gt;Normally, EF Core parses your LINQ, generates SQL, and prepares the execution plan every time.&lt;br&gt;
With EF.CompileQuery, EF Core does that work once, caching the query plan for all future executions.&lt;/p&gt;
&lt;p&gt;The catch?&lt;/p&gt;
&lt;p&gt;If you’re using the Specification pattern for clean, reusable queries, your repository method probably applies a spec dynamically to DbSet — which makes EF.CompileQuery less obvious to use.&lt;/p&gt;
&lt;p&gt;In this article, we’ll solve that and show you how to bake compiled queries into your specification-driven repository.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Quick Recap: Specification Pattern&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Specification pattern encapsulates query logic into reusable, composable units.&lt;/p&gt;
&lt;p&gt;Example spec for active customers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ActiveCustomersSpec : Specification&amp;lt;Customer&amp;gt;
{
    public ActiveCustomersSpec()
    {
        Query.Where(c =&amp;gt; c.IsActive)
             .OrderBy(c =&amp;gt; c.Name)
             .Include(c =&amp;gt; c.Orders);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your repository then applies the spec:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public async Task&amp;lt;IReadOnlyList&amp;lt;T&amp;gt;&amp;gt; ListAsync(ISpecification&amp;lt;T&amp;gt; spec)
{
    var evaluator = new SpecificationEvaluator();
    var query = evaluator.GetQuery(_db.Set&amp;lt;T&amp;gt;().AsQueryable(), spec);
    return await query.ToListAsync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;The Problem&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;EF.CompileQuery needs a strongly-typed lambda starting from your DbContext:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static readonly Func&amp;lt;AppDbContext, bool, IEnumerable&amp;lt;Customer&amp;gt;&amp;gt; _activeCustomers =
    EF.CompileQuery((AppDbContext db, bool isActive) =&amp;gt;
        db.Customers.Where(c =&amp;gt; c.IsActive == isActive));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But with the Specification pattern, you usually pass in a spec, not a lambda on DbContext.&lt;br&gt;
You don’t want every caller to remember how to invoke the compiled query.&lt;br&gt;
We need a way to integrate compiled queries into the repository itself.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Solution: Encapsulate Compiled Queries in the Repository&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;4.1 Create the compiled query&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static class CompiledQueries
{
    public static readonly Func&amp;lt;AppDbContext, bool, IEnumerable&amp;lt;Customer&amp;gt;&amp;gt; ActiveCustomers =
        EF.CompileQuery((AppDbContext db, bool isActive) =&amp;gt;
            db.Customers
              .Where(c =&amp;gt; c.IsActive == isActive)
              .OrderBy(c =&amp;gt; c.Name)
              .Include(c =&amp;gt; c.Orders));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4.2 Update the repository&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class EfRepository&amp;lt;T&amp;gt; where T : class
{
    private readonly AppDbContext _db;
    public EfRepository(AppDbContext db) =&amp;gt; _db = db;

    public async Task&amp;lt;IReadOnlyList&amp;lt;T&amp;gt;&amp;gt; ListAsync(ISpecification&amp;lt;T&amp;gt; spec)
    {
        // Detect known spec → use compiled query
        if (typeof(T) == typeof(Customer) &amp;amp;&amp;amp; spec is ActiveCustomersSpec)
        {
            var result = CompiledQueries.ActiveCustomers(_db, true).ToList();
            return (IReadOnlyList&amp;lt;T&amp;gt;)result;
        }

        // Fallback: normal spec evaluation
        var evaluator = new SpecificationEvaluator();
        var query = evaluator.GetQuery(_db.Set&amp;lt;T&amp;gt;().AsQueryable(), spec);
        return await query.ToListAsync();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;4.3 Usage stays clean&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var repo = new EfRepository&amp;lt;Customer&amp;gt;(db);
var customers = await repo.ListAsync(new ActiveCustomersSpec());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The caller doesn’t know (or care) if the query was compiled&lt;br&gt;
but hot-path specs now run ~3x faster.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16149713430744441257&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ready-to-Use Libraries&lt;br&gt;
Instead of writing your own Specification pattern, you can use:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ardalis/Specification&quot;&gt;Ardalis.Specification&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Well-maintained, feature-rich.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Supports includes, projections, ordering, pagination.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/miholler/NSpecifications&quot;&gt;NSpecifications&lt;/a&gt; &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DDD-focused, composable specifications.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Great for domain-driven designs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;  Both integrate nicely with EF Core and can work with this&lt;br&gt;
   compiled-query approach.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Best Practices&lt;br&gt;
Use compiled queries for hot paths — they have a cost to set up but shine when reused often.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Avoid over-compiling — not every query benefits from precompilation.&lt;/p&gt;
&lt;p&gt;Keep specs small and focused — huge includes and joins can still dominate runtime.&lt;/p&gt;
&lt;p&gt;Combine with async wisely — EF.CompileQuery is sync; use EF.CompileAsyncQuery for async enumeration.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You don’t have to choose between clean architecture (Specification pattern) and speed.&lt;br&gt;
By encapsulating EF.CompileQuery inside your repository for frequently-used specs,&lt;br&gt;
you can keep your calling code clean while still getting huge performance wins.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCompileSpecDemo&quot;&gt;Source Code&lt;/a&gt; : &lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCompileSpecDemo&quot;&gt;https://github.com/stevsharp/EfCompileSpecDemo&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/4443/supercharging-ef-core-specifications-with-ef-compilequery</guid>
<pubDate>Tue, 05 Aug 2025 07:56:23 +0000</pubDate>
</item>
<item>
<title>⚔️ The Mighty MERGE: Using SQL Merge Statements Safely with EF Core</title>
<link>https://coderlegion.com/4268/the-mighty-merge-using-sql-merge-statements-safely-with-ef-core</link>
<description>&lt;p&gt;When you’re working with bulk data operations, upserts, or syncing external sources, MERGE is one of the most powerful tools SQL Server offers. But what happens when you need to use it alongside Entity Framework Core?&lt;/p&gt;
&lt;p&gt;In this article, we’ll cover:&lt;/p&gt;
&lt;p&gt;✅ What MERGE is and why it matters&lt;/p&gt;
&lt;p&gt; How to use it with EF Core (with and without transactions)&lt;/p&gt;
&lt;p&gt;⚠️ Common pitfalls and best practices&lt;/p&gt;
&lt;p&gt;️ Alternatives like BulkExtensions and PostgreSQL upserts&lt;/p&gt;
&lt;p&gt; When to use MERGE — and when not to&lt;/p&gt;
&lt;p&gt; What Is SQL MERGE?&lt;/p&gt;
&lt;p&gt;MERGE is a T-SQL command that allows you to INSERT, UPDATE, or DELETE in a single statement based on conditions between a source and a target.&lt;/p&gt;
&lt;p&gt;Simple Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MERGE INTO Products AS Target
USING (VALUES (1, 'Apple'), (2, 'Banana')) AS Source(Id, Name)
    ON Target.Id = Source.Id
WHEN MATCHED THEN
    UPDATE SET Name = Source.Name
WHEN NOT MATCHED THEN
    INSERT (Id, Name) VALUES (Source.Id, Source.Name);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ The power of MERGE lies in combining multiple operations in one atomic transaction — perfect for syncing external sources, staging tables, or upserting data without multiple round-trips.&lt;/p&gt;
&lt;p&gt; Using MERGE with EF Core&lt;br&gt;
EF Core does not expose MERGE as part of its LINQ API. But you can still run raw SQL safely using:&lt;/p&gt;
&lt;p&gt; Option 1: ExecuteSqlRaw&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await dbContext.Database.ExecuteSqlRawAsync(@&quot;
MERGE INTO Products AS Target
USING (VALUES (1, 'Apple'), (2, 'Banana')) AS Source(Id, Name)
ON Target.Id = Source.Id
WHEN MATCHED THEN
    UPDATE SET Name = Source.Name
WHEN NOT MATCHED THEN
    INSERT (Id, Name) VALUES (Source.Id, Source.Name);&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Option 2: Call a Stored Procedure&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE PROCEDURE MergeProducts AS
BEGIN
    MERGE INTO Products ...
END

await dbContext.Database.ExecuteSqlRawAsync(&quot;EXEC MergeProducts&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Option 3: Use EF Core Bulk Libraries&lt;/p&gt;
&lt;p&gt;Libraries like &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/borisdj/EFCore.BulkExtensions&quot;&gt;EFCore.BulkExtensions&lt;/a&gt; simulate efficient MERGE behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await dbContext.BulkInsertOrUpdateAsync(products);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Can I Use MERGE with EF Core Transactions?&lt;/p&gt;
&lt;p&gt;Yes — and you should if you’re performing other EF operations in the same workflow. EF Core supports shared transactions across both raw SQL and EF operations.&lt;/p&gt;
&lt;p&gt;✅ Full Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using var transaction = await dbContext.Database.BeginTransactionAsync();

try
{
    // 1. Execute MERGE
    await dbContext.Database.ExecuteSqlRawAsync(@&quot;
        MERGE INTO Products AS Target
        USING (VALUES (1, 'Apple'), (2, 'Banana')) AS Source(Id, Name)
        ON Target.Id = Source.Id
        WHEN MATCHED THEN
            UPDATE SET Name = Source.Name
        WHEN NOT MATCHED THEN
            INSERT (Id, Name) VALUES (Source.Id, Source.Name);&quot;);

    dbContext.SyncLogs.Add(new SyncLog { Table = &quot;Products&quot;, Timestamp = DateTime.UtcNow });
    await dbContext.SaveChangesAsync();

    await transaction.CommitAsync();
}
    catch
{
    await transaction.RollbackAsync();
    throw;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ This ensures the MERGE and the SyncLogs insert are part of the same database transaction, protecting you from partial updates.&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5036699520188165910&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;⚠️ Warnings About SQL Server MERGE&lt;/p&gt;
&lt;p&gt;Microsoft has acknowledged &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16#remarks&quot;&gt;multiple bug&lt;/a&gt;s with MERGE in SQL Server — including:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16#remarks&quot;&gt;https://learn.microsoft.com/en-us/sql/t-sql/statements/merge-transact-sql?view=sql-server-ver16#remarks&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Unexpected behavior with triggers&lt;/p&gt;
&lt;p&gt;Incorrect OUTPUT clauses&lt;/p&gt;
&lt;p&gt;Problems with concurrent updates&lt;/p&gt;
&lt;p&gt;✅ Safer Patterns&lt;/p&gt;
&lt;p&gt;Use UPDATE ... and INSERT WHERE NOT EXISTS if possible&lt;/p&gt;
&lt;p&gt;Avoid complex MERGE statements with multiple joins&lt;/p&gt;
&lt;p&gt;Always test MERGE inside a transaction in production-like data scenarios&lt;/p&gt;
&lt;p&gt; When Should I Use MERGE?&lt;/p&gt;
&lt;p&gt;✅ Use MERGE if:&lt;/p&gt;
&lt;p&gt;You need to sync two tables (e.g., staging import)&lt;/p&gt;
&lt;p&gt;You want to upsert large volumes in one atomic step&lt;/p&gt;
&lt;p&gt;You’re doing data warehousing or ETL pipelines&lt;/p&gt;
&lt;p&gt;❌ Avoid MERGE if:&lt;/p&gt;
&lt;p&gt;You're only updating/inserting a few rows&lt;/p&gt;
&lt;p&gt;You rely on entity validation logic in your app&lt;/p&gt;
&lt;p&gt;You need maximum concurrency safety without side effects&lt;/p&gt;
&lt;p&gt;✨ Bonus: Reusable MERGE Helper&lt;br&gt;
You can build a helper that accepts any raw SQL and executes it within a transaction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static async Task ExecuteMergeAsync(this DbContext db, string sql)
{
    using var tx = await db.Database.BeginTransactionAsync();
    try
    {
        await db.Database.ExecuteSqlRawAsync(sql);
        await tx.CommitAsync();
    }
    catch
    {
        await tx.RollbackAsync();
        throw;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Conclusion&lt;/p&gt;
&lt;p&gt;SQL MERGE is a mighty tool for any developer working with bulk updates, external data, or data sync pipelines. EF Core doesn’t have built-in support  but raw SQL, stored procs, and bulk extensions let you use it safely and effectively.&lt;/p&gt;
&lt;p&gt;Wrap it in transactions, know when it shines, and watch out for its quirks  and you’ll get the best of both SQL Server and EF Core worlds.&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/4268/the-mighty-merge-using-sql-merge-statements-safely-with-ef-core</guid>
<pubDate>Fri, 25 Jul 2025 12:16:46 +0000</pubDate>
</item>
<item>
<title> Why Records in C# Are Great (and So Nice to Work With)</title>
<link>https://coderlegion.com/2949/why-records-in-c-are-great-and-so-nice-to-work-with</link>
<description>&lt;p&gt;&lt;strong&gt; Why Records in C# Are Great&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you’ve been working with C# lately, you’ve probably encountered records , a feature introduced in C# 9 that many developers have come to love. &lt;/p&gt;
&lt;p&gt;They’re elegant, powerful, and remove a lot of boilerplate when modeling data.&lt;/p&gt;
&lt;p&gt;But what actually makes them great? Here’s why records aren’t just nice , they’re game-changing.&lt;/p&gt;
&lt;p&gt; What Are Records?&lt;/p&gt;
&lt;p&gt;At their core, records are &lt;strong&gt;reference types&lt;/strong&gt; designed for &lt;strong&gt;immutable&lt;/strong&gt; data modeling. Think of them as classes optimized for holding data  with extra features built in.&lt;/p&gt;
&lt;p&gt;You can define a record in two main ways:&lt;/p&gt;
&lt;p&gt;✅ 1. Positional Syntax (Concise and Clean)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public record Person(string FirstName, string LastName);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This one-liner gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Constructor&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Equals(), GetHashCode(), and ToString() overrides&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;init-only properties (immutable)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deconstruction support&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ouaaa...&lt;/p&gt;
&lt;p&gt;In order to implement all that in a regular class, you'd need to write a lot of code  constructors, equality logic, ToString() overrides, and possibly even a deconstructor. &lt;br&gt;
&lt;strong&gt;Records do it all for you, automatically and cleanly.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✅ 2. Classic Syntax (More Flexible)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Need mutability? You can use set instead of init, but that sacrifices immutability and &lt;strong&gt;thread safety.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt; What is init?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;init&lt;/strong&gt; is a special accessor introduced in C# 9 that allows properties to be set only during initialization making them immutable afterward.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var person = new Person { FirstName = &quot;Alice&quot;, LastName = &quot;Green&quot; };
// person.FirstName = &quot;Bob&quot;; ❌ Not allowed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Cleaner than readonly fields&lt;br&gt;
✅ Safer than public setters&lt;br&gt;
✅ Perfect for records, DTOs, and configuration objects&lt;/p&gt;
&lt;p&gt; Reference:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/init&quot;&gt;Microsoft Docs – Init-only Setters&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;✅ 1. Value-Based Equality — Finally Done Right&lt;/p&gt;
&lt;p&gt;With classes, equality checks compare references. With records, it's based on content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var a = new Person(&quot;Alice&quot;, &quot;Smith&quot;);
var b = new Person(&quot;Alice&quot;, &quot;Smith&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Console.WriteLine(a == b); // True ✅&lt;/p&gt;
&lt;p&gt; Reference:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record&quot;&gt;Microsoft Docs – Records&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;✅ 2. Immutability by Default — and Thread-Safe by Nature&lt;/p&gt;
&lt;p&gt;Records are immutable by default, which leads to safer, more predictable code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var p1 = new Person(&quot;John&quot;, &quot;Doe&quot;);
var p2 = p1 with { FirstName = &quot;Jane&quot; }; // Creates a copy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And since their state can't be changed after creation, records are thread-safe for read operations — perfect for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Parallel tasks&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Background services&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blazor state containers&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Event sourcing&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Reference:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/immutability&quot;&gt;Immutability in C#&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;✅ 3. Concise and Readable Code&lt;br&gt;
Records eliminate boilerplate. You define what matters — and the compiler handles the rest.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public record Invoice(string Id, DateTime Date, decimal Amount);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clean. Lightweight. Clear.&lt;/p&gt;
&lt;p&gt;✅ 4. Deconstruction and Pattern Matching&lt;/p&gt;
&lt;p&gt;Records play perfectly with modern C# features:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var (first, last) = new Person(&quot;Ana&quot;, &quot;Lopez&quot;);

if (p1 is { FirstName: &quot;John&quot; })
{
    Console.WriteLine(&quot;Hello John!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Reference:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching&quot;&gt;Pattern Matching in C#&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;✅ 5. Functional Programming Friendly&lt;/p&gt;
&lt;p&gt;Records align with functional principles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No side effects&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy transformations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Clear data flow&lt;/p&gt;
&lt;p&gt; var updatedInvoice = invoice with { Amount = invoice.Amount + 10 };&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Reference:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/#records&quot;&gt;Welcome to C# 9 – Records&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;✅ 6. Supports Inheritance, Structs, and More&lt;/p&gt;
&lt;p&gt;You’re not locked into one pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;record struct — value-type records&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;readonly record struct — fully immutable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Abstract and sealed record inheritance supported&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Reference:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-10&quot;&gt;What's New in C# 10 – Record Structs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Records and the Value Object Pattern&lt;br&gt;
In Domain-Driven Design (DDD), Value Objects are small objects that:&lt;/p&gt;
&lt;p&gt;Represent a concept (e.g., Money, Email)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Are immutable&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Have no identity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Compare by value&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;✅ Records Are Ideal for Value Objects&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public record Email(string Address)
{
    public Email
    {
        if (!Address.Contains(&quot;@&quot;))
            throw new ArgumentException(&quot;Invalid email&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get a clean, reusable object with built-in validation and equality , no fuss.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public record Money(decimal Amount, string Currency);

public record Product
{
    public Guid Id { get; init; }
    public string Name { get; init; }
    public Money Price { get; init; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Reference:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/bliki/ValueObject.html&quot;&gt;Martin Fowler – Value Object&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; When Not to Use Records&lt;/p&gt;
&lt;p&gt;Avoid records if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You need full mutable entities (e.g., for EF Core)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You rely on reference identity&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your inheritance hierarchy is too complex&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Real-World Use Cases&lt;/p&gt;
&lt;p&gt;✅ DTOs for APIs&lt;/p&gt;
&lt;p&gt;✅ Immutable configs&lt;/p&gt;
&lt;p&gt;✅ Logging and audit trails&lt;/p&gt;
&lt;p&gt;✅ Domain Value Objects&lt;/p&gt;
&lt;p&gt;✅ State models in Blazor, Redux, Fluxor&lt;/p&gt;
&lt;p&gt;✅ Event-driven architecture&lt;/p&gt;
&lt;p&gt; Final Thoughts&lt;br&gt;
C# records are more than just syntactic sugar — they promote a mindset of immutability, clarity, and value-based design.&lt;/p&gt;
&lt;p&gt;They:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Reduce bugs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simplify data models&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improve thread safety&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Save a lot of time&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whether you're building APIs, desktop apps, or microservices, records make your C# experience cleaner and smarter.&lt;/p&gt;
&lt;p&gt;Once you go record, you'll never want to class again. &lt;/p&gt;
&lt;p&gt; References:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record&quot;&gt;Microsoft Docs – Records&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/init&quot;&gt;Init-only Setters in C# 9&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.codeguru.com/csharp/c-sharp-immutability/&quot;&gt;Immutability&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching&quot;&gt;Pattern Matching&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/#records&quot;&gt;Dev Blog – Welcome to C# 9&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/bliki/ValueObject.html&quot;&gt;Martin Fowler – Value Objects&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/2949/why-records-in-c-are-great-and-so-nice-to-work-with</guid>
<pubDate>Sat, 10 May 2025 20:37:38 +0000</pubDate>
</item>
<item>
<title>How to Bind a MudSelect from an External Source in Blazor Using MudBlazor</title>
<link>https://coderlegion.com/1832/how-to-bind-a-mudselect-from-an-external-source-in-blazor-using-mudblazor</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor&quot;&gt;Blazor&lt;/a&gt; is a modern framework from Microsoft that allows you to build interactive web applications using C# and .NET, both on the server and the client, without needing JavaScript. It brings the full power of .NET to the browser via WebAssembly or server-side rendering.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mudblazor.com/&quot;&gt;MudBlazor&lt;/a&gt; is a popular UI component library built specifically for Blazor. It offers clean, responsive components following Google's Material Design system, making it incredibly easy to create modern, beautiful, and interactive web UIs entirely in C#.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Use Case&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s say you need to:&lt;/p&gt;
&lt;p&gt;Display a multi-select dropdown&lt;/p&gt;
&lt;p&gt;Load the options from an external service&lt;/p&gt;
&lt;p&gt;Sync selected values with the parent component&lt;/p&gt;
&lt;p&gt;Reuse this logic throughout your application&lt;/p&gt;
&lt;p&gt;We'll walk through a real-world pattern using MudSelect, bound to a list of brands (dtoBrands) provided by a service.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A Blazor project with MudBlazor installed&lt;/p&gt;
&lt;p&gt;A service that returns the brand data&lt;br&gt;
A DTO like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BrandDto
{
    public string Text { get; set; }
    public string Value { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Sample Service Interface&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface IBrandService
{
    Task&amp;lt;IEnumerable&amp;lt;BrandDto&amp;gt;&amp;gt; GetBrandsAsync();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; The MudSelect Markup&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;MudSelect T=&quot;BrandDto&quot;
           Label=&quot;@Label&quot;
           MultiSelection=&quot;true&quot;
           Clearable=&quot;true&quot;
           @bind-SelectedValues=&quot;InternalSelectedValues&quot;
           ToStringFunc=&quot;item =&amp;gt; item.Text&quot;&amp;gt;
    @if (dtoBrands.Any())
    {
        @foreach (var item in dtoBrands)
        {
            &amp;lt;MudSelectItem T=&quot;BrandDto&quot; Value=&quot;item&quot;&amp;gt;
                @item.Text
            &amp;lt;/MudSelectItem&amp;gt;
        }
    }
&amp;lt;/MudSelect&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚙️ Component Logic&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@inject IBrandService BrandService

@code {
    [Parameter]
    public string Label { get; set; } = &quot;Select Brands&quot;;

    [Parameter]
    public IEnumerable&amp;lt;BrandDto&amp;gt; SelectedValues { get; set; } = new HashSet&amp;lt;BrandDto&amp;gt;();

    [Parameter]
    public EventCallback&amp;lt;IEnumerable&amp;lt;BrandDto&amp;gt;&amp;gt; SelectedValuesChanged { get; set; }

    protected IEnumerable&amp;lt;BrandDto&amp;gt; dtoBrands = new List&amp;lt;BrandDto&amp;gt;();

    private IEnumerable&amp;lt;BrandDto&amp;gt; _internalSelectedValues = new HashSet&amp;lt;BrandDto&amp;gt;();

    private IEnumerable&amp;lt;BrandDto&amp;gt; InternalSelectedValues
    {
        get =&amp;gt; _internalSelectedValues;
        set
        {
            if (_internalSelectedValues != value)
            {
                _internalSelectedValues = value;
                SelectedValuesChanged.InvokeAsync(_internalSelectedValues);
            }
        }
    }

    protected override async Task OnInitializedAsync()
    {
        dtoBrands = await BrandService.GetBrandsAsync();

        _internalSelectedValues = new HashSet&amp;lt;BrandDto&amp;gt;(
            dtoBrands.Where(x =&amp;gt; SelectedValues.Any(s =&amp;gt; s.Value == x.Value))
        );
    }

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        await base.SetParametersAsync(parameters);

        _internalSelectedValues = new HashSet&amp;lt;BrandDto&amp;gt;(
            dtoBrands.Where(x =&amp;gt; SelectedValues.Any(s =&amp;gt; s.Value == x.Value))
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; How It Works&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1170082142962582969&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;♻️ Example Usage&lt;/p&gt;
&lt;p&gt;Imagine turning this into a reusable component, like BrandSelector.razor. You’d use it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;BrandSelector Label=&quot;Choose Brands&quot;
               SelectedValues=&quot;@selectedBrands&quot;
               SelectedValuesChanged=&quot;@OnSelectedBrandsChanged&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Why This Pattern Rocks&lt;/p&gt;
&lt;p&gt;By combining Blazor's powerful component model with MudBlazor's modern UI toolkit, you get:&lt;/p&gt;
&lt;p&gt;✅ Fully data-driven dropdowns&lt;/p&gt;
&lt;p&gt;✅ Type-safe C# components&lt;/p&gt;
&lt;p&gt;✅ No need for JavaScript&lt;/p&gt;
&lt;p&gt;✅ A consistent look and feel&lt;/p&gt;
&lt;p&gt;And all of it is maintainable and testable across large-scale apps.&lt;/p&gt;
&lt;p&gt;✅ Final Thoughts&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blazor&lt;/strong&gt; and &lt;strong&gt;MudBlazor&lt;/strong&gt; are a dream combo for building modern web apps in C#. By binding a MudSelect to an external service, you're creating a highly flexible and reusable pattern that fits beautifully in a clean architecture.&lt;/p&gt;
&lt;p&gt; Useful Links&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-9.0&quot;&gt;Blazor documentation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mudblazor.com/&quot;&gt;MudBlazor official site&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://mudblazor.com/api/mudselect#pages&quot;&gt;MudSelect documentation&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/1832/how-to-bind-a-mudselect-from-an-external-source-in-blazor-using-mudblazor</guid>
<pubDate>Thu, 10 Apr 2025 09:15:42 +0000</pubDate>
</item>
<item>
<title>EF Core Bulk Updates and Deletes: Performance vs Change Tracking</title>
<link>https://coderlegion.com/1640/ef-core-bulk-updates-and-deletes-performance-vs-change-tracking</link>
<description>&lt;p&gt;Entity Framework Core 7 introduced two powerful methods for bulk operations: &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ExecuteUpdateAsync&lt;/strong&gt; and &lt;strong&gt;ExecuteDeleteAsync&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;These methods are designed for high-performance scenarios where you need to update or delete multiple records efficiently without loading them into memory. They work great, but there's one caveat: EF Core's Change Tracker is not aware of these changes.&lt;/p&gt;
&lt;p&gt;In this article, we explore how these methods work, why they bypass the &lt;strong&gt;Change Tracker&lt;/strong&gt;, and most importantly—how you can audit or track changes manually when using them.&lt;/p&gt;
&lt;p&gt;If you're not familiar with EF Core's Change Tracker, check out this &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/change-tracking/&quot;&gt;introductory guide on Change Tracking&lt;/a&gt; to understand how EF Core monitors entity states.&lt;/p&gt;
&lt;p&gt;Why Use ExecuteUpdateAsync and ExecuteDeleteAsync?&lt;/p&gt;
&lt;p&gt;These methods:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate direct SQL UPDATE and DELETE statements.&lt;/li&gt;
&lt;li&gt;Do not load entities into memory, improving performance.&lt;/li&gt;
&lt;li&gt;Bypass change tracking, which avoids unnecessary overhead.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example Usage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Bulk update
await context.Users
    .Where(u =&amp;gt; u.LastLogin &amp;lt; DateTime.UtcNow.AddYears(-1))
    .ExecuteUpdateAsync(setters =&amp;gt; setters.SetProperty(u =&amp;gt; u.IsInactive, true));

// Bulk delete
await context.Users
    .Where(u =&amp;gt; u.IsInactive)
    .ExecuteDeleteAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Problem: No Change Tracking&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Because EF Core does not load entities into memory during these operations, the Change Tracker never sees them. This means:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;No automatic change detection.&lt;/li&gt;
&lt;li&gt;No SaveChangesAsync() involvement.&lt;/li&gt;
&lt;li&gt;No triggers for audit or domain events.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, how do you track what was changed or deleted?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.Auditing Options for Bulk Operations&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Manual Pre-Query Before Execution&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Fetch affected entities before performing the batch operation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var affectedUsers = await context.Users
    .Where(u =&amp;gt; u.LastLogin &amp;lt; DateTime.UtcNow.AddYears(-1))
    .Select(u =&amp;gt; new { u.Id, u.Name, OldValue = u.IsInactive })
    .ToListAsync();

await context.Users
    .Where(u =&amp;gt; u.LastLogin &amp;lt; DateTime.UtcNow.AddYears(-1))
    .ExecuteUpdateAsync(setters =&amp;gt; setters.SetProperty(u =&amp;gt; u.IsInactive, true));

// Log changes manually
foreach (var user in affectedUsers)
{
    Console.WriteLine($&quot;User {user.Name} updated: IsInactive {user.OldValue} → true&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Precise control over what you log.&lt;/p&gt;
&lt;p&gt;Access to old values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Requires an extra query.&lt;/p&gt;
&lt;p&gt;May impact performance for large datasets.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Audit Table Logging&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Save the affected data into an audit table before executing the bulk operation.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await context.AuditLogs.AddRangeAsync(affectedUsers.Select(user =&amp;gt; new AuditLog
{
    EntityName = &quot;User&quot;,
    EntityId = user.Id,
    ChangeType = &quot;Updated&quot;,
    ChangedAt = DateTime.UtcNow,
    Description = $&quot;User {user.Name} was updated: IsInactive {user.OldValue} → true&quot;
}));

await context.SaveChangesAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Database Triggers&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Define SQL triggers to capture changes directly in the database.&lt;/p&gt;
&lt;p&gt;Example :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TRIGGER trg_User_Delete
ON Users
AFTER DELETE
AS
BEGIN
    INSERT INTO AuditLogs (EntityName, EntityId, ChangeType, ChangedAt, Description)
    SELECT 'User', Id, 'Deleted', GETUTCDATE(), CONCAT('User ', Name, ' was deleted') FROM deleted;
END;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pros:&lt;/p&gt;
&lt;p&gt;Fully automated.&lt;/p&gt;
&lt;p&gt;Ensures auditing even if EF Core is bypassed.&lt;/p&gt;
&lt;p&gt;Cons:&lt;/p&gt;
&lt;p&gt;Less flexible.&lt;/p&gt;
&lt;p&gt;Harder to manage and version control.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;EF Core Interceptors (Advanced)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Capture executed SQL commands using DbCommandInterceptor.&lt;/p&gt;
&lt;p&gt;Limitation:&lt;/p&gt;
&lt;p&gt;You can log the SQL query but not the previous values of affected entities.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AuditInterceptor : DbCommandInterceptor
{
    public override InterceptionResult&amp;lt;DbDataReader&amp;gt; ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult&amp;lt;DbDataReader&amp;gt; result)
    {
        Console.WriteLine($&quot;SQL Executed: {command.CommandText}&quot;);
        return base.ReaderExecuting(command, eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Registering the Interceptor : &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ApplicationDbContext : DbContext
{
    public DbSet&amp;lt;User&amp;gt; Users { get; set; }
    public DbSet&amp;lt;AuditLog&amp;gt; AuditLogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(&quot;YourConnectionStringHere&quot;);
        optionsBuilder.AddInterceptors(new AuditInterceptor());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EF Core's ExecuteUpdateAsync and ExecuteDeleteAsync offer incredible performance for bulk data operations but come at the cost of losing automatic change tracking.&lt;/p&gt;
&lt;p&gt;If you need to audit or track changes, the best approach is to manually query the affected data before execution or use a dedicated audit system such as audit tables or SQL triggers.&lt;/p&gt;
&lt;p&gt;Choose the method that balances performance and auditing needs for your application.&lt;/p&gt;
&lt;p&gt;What do you think? Do you prefer performance with manual tracking, or do you rely on EF Core's change tracking? Let us know your approach!&lt;/p&gt;
&lt;p&gt;You can find the full code example at: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCoreExamples/tree/BulkUpdateAndDeletes&quot;&gt;https://github.com/stevsharp/EfCoreExamples/tree/BulkUpdateAndDeletes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;References&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#bulk-updates-and-deletes&quot;&gt;Microsoft Docs - EF Core 7: ExecuteUpdate and ExecuteDelete&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/change-tracking/&quot;&gt;Microsoft Docs - Change Tracking in EF Core&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&quot;&gt;Microsoft Docs - Interceptors in EF Core&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/dotnet/efcore&quot;&gt;Community Discussion - EF Core GitHub Issue on Change Tracking and Bulk Operations&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1640/ef-core-bulk-updates-and-deletes-performance-vs-change-tracking</guid>
<pubDate>Mon, 24 Mar 2025 20:25:26 +0000</pubDate>
</item>
<item>
<title>EF Core Interceptors: Supercharge Your Data Access with the Decorator Pattern</title>
<link>https://coderlegion.com/1602/ef-core-interceptors-supercharge-your-data-access-with-the-decorator-pattern</link>
<description>&lt;p&gt;Learn how to hook into EF Core operations using interceptors, leverage the &lt;strong&gt;Decorator Pattern&lt;/strong&gt; for clean cross-cutting concerns, and see real-world examples like SQL logging and auditing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A Brief History of EF Core Interceptors&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What Are EF Core Interceptors?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EF Core Interceptors and the Decorator Pattern&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Top Use Cases for EF Core Interceptors&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example 1: Logging SQL Queries&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example 2: Auditing SaveChanges&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final Thoughts&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-brief-history-of-ef-core-interceptors&quot;&gt;A Brief History of EF Core Interceptors&lt;/h3&gt;
&lt;p&gt;EF Core interceptors were first introduced in EF Core 3.0 (released in September 2019). This version marked a significant step forward in EF Core's extensibility, allowing developers to intercept low-level database operations like commands, connections, and transactions.&lt;/p&gt;
&lt;p&gt;Since then, Microsoft has expanded interceptor capabilities in newer versions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core 5.0&lt;/strong&gt;: Introduced SaveChanges interceptors for tracking changes at the context level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core 6.0+&lt;/strong&gt;: Continued performance improvements and better support for asynchronous interception.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Are EF Core Interceptors?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In Entity Framework Core (EF Core), interceptors allow developers to hook into the execution pipeline of EF Core's operations. Think of them as middleware for your database interactions. You can intercept events such as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SQL command execution&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connection opening/closing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transactions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SaveChanges lifecycle&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This lets you inject custom logic like &lt;strong&gt;logging&lt;/strong&gt;, &lt;strong&gt;auditing&lt;/strong&gt;, &lt;strong&gt;validation&lt;/strong&gt;, or performance metrics without modifying your application’s core logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core Interceptors and the Decorator Pattern&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EF Core interceptors are a real-world application of the Decorator Pattern, a classic design pattern where functionality is wrapped around existing behavior to extend or alter it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The core EF operation is the base object.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Your interceptor is the decorator.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;You add behavior before or after EF Core’s operation — without altering EF’s core.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This approach promotes clean code, separation of concerns, and makes your custom behavior reusable and testable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Top Use Cases for EF Core Interceptors&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;– SQL Query Logging &lt;/p&gt;
&lt;p&gt;– Track database queries for performance or debugging. &lt;/p&gt;
&lt;p&gt;– Auditing – Automatically log user actions and timestamps. &lt;/p&gt;
&lt;p&gt;– Soft Deletes – Intercept delete commands and convert them to updates. &lt;/p&gt;
&lt;p&gt;– Command Modification – Change or enrich raw SQL before execution. &lt;/p&gt;
&lt;p&gt;– Multi-Tenant Filtering – Inject filters dynamically for multi-tenant scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 1: Logging SQL Queries&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s log every SQL command executed by EF Core.&lt;/p&gt;
&lt;p&gt;Step 1: Create a SQL Logging Interceptor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Diagnostics;

public class SqlLoggingInterceptor : DbCommandInterceptor
{
    public override InterceptionResult&amp;lt;DbDataReader&amp;gt; ReaderExecuting(
        DbCommand command, 
        CommandEventData eventData, 
        InterceptionResult&amp;lt;DbDataReader&amp;gt; result)
    {
        Debug.WriteLine($&quot;SQL Executed: {command.CommandText}&quot;);
        return base.ReaderExecuting(command, eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SQL Executed: SELECT * FROM [Users] WHERE [IsActive] = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Register the Interceptor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(&quot;YourConnectionString&quot;)
            .AddInterceptors(new SqlLoggingInterceptor());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example 2: Auditing SaveChanges&lt;/p&gt;
&lt;p&gt;Let’s implement automatic auditing of entity modifications.&lt;/p&gt;
&lt;p&gt;Step 1: Define an Auditable Entity Base Class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class AuditableEntity
{
    public DateTime ModifiedAt { get; set; }
    public string ModifiedBy { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 2: Implement a SaveChangesInterceptor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AuditInterceptor : SaveChangesInterceptor
{
    private readonly string _currentUser;

    public AuditInterceptor(string currentUser)
    {
        _currentUser = currentUser;
    }

    public override int SavingChanges(DbContextEventData eventData, int result)
    {
        var context = eventData.Context;
        if (context == null) return base.SavingChanges(eventData, result);

        var entries = context.ChangeTracker
            .Entries&amp;lt;AuditableEntity&amp;gt;()
            .Where(e =&amp;gt; e.State == EntityState.Modified);

        foreach (var entry in entries)
        {
            entry.Entity.ModifiedAt = DateTime.UtcNow;
            entry.Entity.ModifiedBy = _currentUser;
        }

        return base.SavingChanges(eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Final Thoughts&lt;/p&gt;
&lt;p&gt;EF Core interceptors are an underutilized gem in the .NET ecosystem. They let you cleanly implement cross-cutting concerns without muddying your core application code. And because they follow the Decorator Pattern, you get flexibility, maintainability, and testability.&lt;/p&gt;
&lt;p&gt;Whether you need to log queries, audit changes, enforce soft deletes, or handle custom behavior, interceptors offer a powerful and elegant solution.&lt;/p&gt;
&lt;p&gt;References&lt;br&gt;
EF Core Documentation – Interceptors&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&quot;&gt;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core 3.0 Release Notes (Interceptors introduced)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/whatsnew&quot;&gt;https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/whatsnew&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core SaveChanges Interceptor Docs&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#savechanges-interception&quot;&gt;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#savechanges-interception&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Decorator Pattern - Refactoring Guru (for pattern explanation)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://refactoring.guru/design-patterns/decorator&quot;&gt;https://refactoring.guru/design-patterns/decorator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core GitHub Repository (for latest updates and issues)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/dotnet/efcore&quot;&gt;https://github.com/dotnet/efcore&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1602/ef-core-interceptors-supercharge-your-data-access-with-the-decorator-pattern</guid>
<pubDate>Thu, 20 Mar 2025 08:25:11 +0000</pubDate>
</item>
<item>
<title>EF Core: Lazy Loading, Eager Loading, and Loading Data on Demand</title>
<link>https://coderlegion.com/1401/ef-core-lazy-loading-eager-loading-and-loading-data-on-demand</link>
<description>&lt;p&gt;When working with &lt;strong&gt;Entity Framework Core (EF Core),&lt;/strong&gt; there are different strategies for loading related data from the database.&lt;/p&gt;
&lt;p&gt;So, how do you load a collection or other &lt;strong&gt;related objects?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here are the main options:&lt;/p&gt;
&lt;p&gt;1️⃣ Eager Loading (Include)&lt;/p&gt;
&lt;p&gt;Loads related data immediately with the main entity using .Include().&lt;/p&gt;
&lt;p&gt;Example: Load an Order with its Customer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var order = context.Orders
    .Include(o =&amp;gt; o.Customer) // Load related Customer
    .First();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example: Load a Customer with Their Orders&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var customer = context.Customers
    .Include(c =&amp;gt; c.Orders) // Load related Orders
    .First();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅Pros:&lt;/p&gt;
&lt;p&gt;✔ Avoids multiple database queries (N+1 problem)&lt;/p&gt;
&lt;p&gt;✔ Simple and easy to use&lt;/p&gt;
&lt;p&gt;❌ Cons:&lt;/p&gt;
&lt;p&gt;❌ Can load unnecessary data, increasing query size&lt;/p&gt;
&lt;p&gt;❌ Slower if you load too many relationships&lt;/p&gt;
&lt;p&gt;2️⃣ Explicit Loading (Entry().Reference() / Entry().Collection())&lt;/p&gt;
&lt;p&gt;Loads related data on demand (manually) using .Entry().&lt;/p&gt;
&lt;p&gt;Example: Load a Single Related Entity&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var order = context.Orders.First();

// Load related Customer manually
context.Entry(order).Reference(o =&amp;gt; o.Customer).Load();

Console.WriteLine(order.Customer.Name);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example: Load a Collection of Related Entities&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var customer = context.Customers.First();

// Load related Orders manually
context.Entry(customer).Collection(c =&amp;gt; c.Orders).Load();

Console.WriteLine($&quot;Orders count: {customer.Orders.Count}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Pros:&lt;/p&gt;
&lt;p&gt;✔ More control over when data is loaded&lt;/p&gt;
&lt;p&gt;✔ Avoids unnecessary queries&lt;/p&gt;
&lt;p&gt;❌ Cons:&lt;/p&gt;
&lt;p&gt;❌ Requires additional queries&lt;/p&gt;
&lt;p&gt;❌ Must remember to load data explicitly&lt;/p&gt;
&lt;p&gt;3️⃣ Lazy Loading (Requires Proxies)&lt;/p&gt;
&lt;p&gt;Loads related data only when accessed.&lt;/p&gt;
&lt;p&gt;Install :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package Microsoft.EntityFrameworkCore.Proxies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Steps to Enable Lazy Loading&lt;/p&gt;
&lt;p&gt;Enable in DbContext:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.UseLazyLoadingProxies().UseSqlite(&quot;Data Source=mydb.db&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make navigation properties virtual&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public virtual Customer Customer { get; set; } = default!;
public virtual List&amp;lt;Order&amp;gt; Orders { get; set; } = new();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example Usage : &lt;/p&gt;
&lt;p&gt;var order = context.Orders.First();&lt;/p&gt;
&lt;p&gt;// Lazy loaded automatically when accessed&lt;/p&gt;
&lt;p&gt;Console.WriteLine(order.Customer.Name);&lt;/p&gt;
&lt;p&gt;✅ Pros:&lt;/p&gt;
&lt;p&gt;✔ Simplifies code (data is loaded automatically)&lt;/p&gt;
&lt;p&gt;✔ Works without modifying queries&lt;/p&gt;
&lt;p&gt;❌ Cons:&lt;/p&gt;
&lt;p&gt;❌ Requires proxies (extra package)&lt;/p&gt;
&lt;p&gt;❌ Can lead to multiple database hits (N+1 problem)&lt;/p&gt;
&lt;p&gt;4️⃣ Projection (Select with Anonymous Types)&lt;/p&gt;
&lt;p&gt;Loads only specific data instead of full entities.&lt;/p&gt;
&lt;p&gt;Example: Load Only Needed Fields&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var orders = context.Orders
    .Select(o =&amp;gt; new 
    { 
        o.OrderNumber, 
        CustomerName = o.Customer.Name 
    })
    .ToList();

foreach (var order in orders)
{
    Console.WriteLine($&quot;{order.OrderNumber} - {order.CustomerName}&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Pros:&lt;/p&gt;
&lt;p&gt;✔ Best for performance (loads only required fields)&lt;/p&gt;
&lt;p&gt;✔ Reduces unnecessary data transfer&lt;/p&gt;
&lt;p&gt;❌ Cons:&lt;/p&gt;
&lt;p&gt;❌ Cannot track entities (only works for read-only queries)&lt;/p&gt;
&lt;p&gt;Summary Table&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=18412293215726215640&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt; Best Practices&lt;br&gt;
Use Eager Loading (Include()) when related data is always needed.&lt;/p&gt;
&lt;p&gt;Use Explicit Loading if you want manual control.&lt;/p&gt;
&lt;p&gt;Use Lazy Loading only if necessary (it can cause performance issues).&lt;/p&gt;
&lt;p&gt;Use Projection (Select) when you need only some fields.&lt;/p&gt;
&lt;p&gt;Source Code&lt;/p&gt;
&lt;p&gt;You can find the complete source code for this tutorial at:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCoreExamples&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here are some useful references to learn more about EF Core : &lt;/p&gt;
&lt;p&gt; Official Microsoft Documentation&lt;br&gt;
Entity Framework Core Loading Related Data&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/related-data&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/related-data&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Covers Eager Loading, Explicit Loading, and Lazy Loading with examples.&lt;br&gt;
EF Core: Configuring Lazy Loading&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/related-data/lazy&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/related-data/lazy&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Explains how to enable Lazy Loading with proxies.&lt;br&gt;
EF Core: Using Include() for Eager Loading&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Details how to use .Include() and .ThenInclude().&lt;br&gt;
EF Core Performance Considerations&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/performance/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/performance/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Best practices for optimizing queries and avoiding performance issues like N+1 queries.&lt;/p&gt;
&lt;p&gt; Community &amp;amp; Blog Articles&lt;br&gt;
EF Core Lazy Loading vs. Eager Loading - Which One Should You Use?&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.thinktecture.com/en/entity-framework-core/lazy-loading-vs-eager-loading/&quot;&gt;https://www.thinktecture.com/en/entity-framework-core/lazy-loading-vs-eager-loading/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A detailed comparison of when to use each strategy.&lt;br&gt;
EF Core Best Practices: Avoiding Performance Pitfalls&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.michalbialecki.com/2020/11/29/entity-framework-core-performance-best-practices/&quot;&gt;https://www.michalbialecki.com/2020/11/29/entity-framework-core-performance-best-practices/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Covers when to use .Include(), lazy loading, and projection.&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1401/ef-core-lazy-loading-eager-loading-and-loading-data-on-demand</guid>
<pubDate>Thu, 06 Mar 2025 16:31:00 +0000</pubDate>
</item>
<item>
<title>EF Core Global Query Filters: A Complete Guide</title>
<link>https://coderlegion.com/1323/ef-core-global-query-filters-a-complete-guide</link>
<description>&lt;p&gt;Entity Framework Core (EF Core) Global Query Filters allow you to apply filters to queries at the model level so that they automatically apply to all queries involving a specific entity. &lt;/p&gt;
&lt;p&gt;This is particularly useful for scenarios like soft deletes, multi-tenancy, and user-based data filtering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is a Global Query Filter?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A global filter is a LINQ predicate that applies automatically to all queries on a specific entity type. &lt;/p&gt;
&lt;p&gt;This means that any time you query the database, EF Core applies the filter implicitly, reducing the need for adding Where clauses manually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use Cases&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Soft Deletes – Automatically filter out entities where IsDeleted =&lt;br&gt;
true.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-Tenancy – Ensure that a tenant only accesses their own data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User-Based Filtering – Restrict users to see only their own records.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;How to Define a Global Query Filter&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Global filters are defined in the OnModelCreating method inside the &lt;strong&gt;DbContext&lt;/strong&gt; class using the HasQueryFilter method.&lt;/p&gt;
&lt;p&gt;Example : Soft Delete Implementation  ..&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class BaseEntity
{
    public bool IsDeleted { get; set; }
}

public class Product : BaseEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet&amp;lt;Product&amp;gt; Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&amp;lt;Product&amp;gt;()
            .HasQueryFilter(p =&amp;gt; !p.IsDeleted);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; Now, every query on Products automatically filters out records where &lt;strong&gt;IsDeleted&lt;/strong&gt; = true.&lt;/p&gt;
&lt;p&gt;Querying Products&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var products = context.Products.ToList(); // Implicitly applies &quot;WHERE IsDeleted = 0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bypassing the Global Filter&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can explicitly disable the filter using .IgnoreQueryFilters().&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var allProducts = context.Products.IgnoreQueryFilters().ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Applying Filters Dynamically (Multi-Tenancy Example)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppDbContext : DbContext
{
    private readonly int _tenantId;

    public AppDbContext(DbContextOptions&amp;lt;AppDbContext&amp;gt; options, ITenantProvider tenantProvider)
        : base(options)
    {
        _tenantId = tenantProvider.GetTenantId();
    }

    public DbSet&amp;lt;Order&amp;gt; Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&amp;lt;Order&amp;gt;()
            .HasQueryFilter(o =&amp;gt; o.TenantId == _tenantId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;strong&gt;Now, EF Core automatically filters records based on _tenantId.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limitations and Considerations&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Filters do not apply to raw SQL queries (FromSqlRaw).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigation properties respect global filters (e.g., if Order has OrderDetails, the filter applies to them too).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure filters do not cause unexpected performance issues on large datasets.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can define multiple query filters per entity.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Combining Multiple Filters&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;modelBuilder.Entity&amp;lt;Product&amp;gt;()
    .HasQueryFilter(p =&amp;gt; !p.IsDeleted)
    .HasQueryFilter(p =&amp;gt; p.Status == &quot;Active&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This results in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM Products WHERE IsDeleted = 0 AND Status = 'Active'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Disabling All Global Filters&lt;/p&gt;
&lt;p&gt;You can disable all filters for a specific context query using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var allOrders = context.Orders.IgnoreQueryFilters().ToList();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Conclusion&lt;br&gt;
EF Core’s Global Query Filters help enforce consistent rules across queries without adding redundant Where clauses. They are a powerful feature for soft deletes, multi-tenancy, and user-based access control. However, they should be used carefully, considering potential performance impacts.&lt;/p&gt;
&lt;p&gt;Source Code&lt;/p&gt;
&lt;p&gt;You can find the complete source code for this tutorial at:&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/EfCoreAndGlobalFilter&quot;&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here are some useful references to learn more about EF Core Global Query Filters and Soft Delete:&lt;/p&gt;
&lt;p&gt; Official EF Core Documentation&lt;br&gt;
Global Query Filters&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/filters&quot;&gt;https://learn.microsoft.com/en-us/ef/core/querying/filters&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Entity Framework Core SQLite Provider&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/providers/sqlite/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/providers/sqlite/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Managing Data with EF Core&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/&quot;&gt;https://learn.microsoft.com/en-us/ef/core/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Community Articles &amp;amp; Tutorials&lt;br&gt;
EF Core: Global Query Filters for Soft Deletes&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.thinktecture.com/en/entity-framework-core/global-query-filters/&quot;&gt;https://www.thinktecture.com/en/entity-framework-core/global-query-filters/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Implementing Soft Delete in EF Core&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/swlh/soft-delete-in-entity-framework-core-6c6b7a9784c7&quot;&gt;https://medium.com/swlh/soft-delete-in-entity-framework-core-6c6b7a9784c7&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Using SQLite with EF Core in Console Applications&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://code-maze.com/using-sqlite-with-entity-framework-core/&quot;&gt;https://code-maze.com/using-sqlite-with-entity-framework-core/&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1323/ef-core-global-query-filters-a-complete-guide</guid>
<pubDate>Sun, 02 Mar 2025 09:42:51 +0000</pubDate>
</item>
<item>
<title>When to Use AsSplitQuery() in EF.core</title>
<link>https://coderlegion.com/1255/when-to-use-assplitquery-in-ef-core</link>
<description>&lt;p&gt;When working with Entity Framework Core (EF Core), queries that involve multiple Include() statements can result in a single large query with multiple joins. This can cause performance issues due to data duplication and increased memory usage.&lt;/p&gt;
&lt;h2 id=&quot;what-is-assplitquery-&quot;&gt;What is AsSplitQuery()?&lt;/h2&gt;
&lt;p&gt;AsSplitQuery() tells EF Core to fetch related data using separate SQL queries instead of one large join query. This helps optimize performance for complex queries.&lt;/p&gt;
&lt;h3 id=&quot;step-1-setup-your-model&quot;&gt;&lt;strong&gt;Step 1: Setup Your Model&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Let’s create a simple e-commerce scenario with Order, Customer, and OrderItem entities.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Order
{
    public int Id { get; set; }
    public string OrderNumber { get; set; }
    public Customer Customer { get; set; }
    public List&amp;lt;OrderItem&amp;gt; OrderItems { get; set; } = new();
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderItem
{
    public int Id { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-2-configure-the-database-context&quot;&gt;&lt;strong&gt;Step 2: Configure the Database Context&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We define our DbContext to include the necessary DbSet properties.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppDbContext : DbContext
{
    public DbSet&amp;lt;Order&amp;gt; Orders { get; set; }
    public DbSet&amp;lt;Customer&amp;gt; Customers { get; set; }
    public DbSet&amp;lt;OrderItem&amp;gt; OrderItems { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite(&quot;Data Source=app.db&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step-3-query-without-assplitquery-&quot;&gt;&lt;strong&gt;Step 3: Query Without AsSplitQuery()&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Let's say we want to fetch orders with their customers and order items.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**var orders = await _context.Orders
    .Include(o =&amp;gt; o.Customer)
    .Include(o =&amp;gt; o.OrderItems)
    .ToListAsync();**
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What Happens Here?&lt;/strong&gt;&lt;br&gt;
EF Core generates a single SQL query with multiple JOIN operations.&lt;br&gt;
If there are many orders and items, this can cause performance issues due to data duplication.&lt;/p&gt;
&lt;h3 id=&quot;step-4-optimize-with-assplitquery-&quot;&gt;&lt;strong&gt;Step 4: Optimize with AsSplitQuery()&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Now, let's improve performance by using AsSplitQuery().&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var orders = await _context.Orders
    .Include(o =&amp;gt; o.Customer)
    .Include(o =&amp;gt; o.OrderItems)
    .AsSplitQuery() // Fetches data using multiple queries instead of joins
    .ToListAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What Happens Now?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EF Core executes multiple separate SQL queries instead of a single large join.&lt;br&gt;
Less memory is used since duplicate data is avoided.&lt;br&gt;
Performance is improved when dealing with large datasets.&lt;/p&gt;
&lt;h3 id=&quot;step-5-understanding-the-sql-queries&quot;&gt;&lt;strong&gt;Step 5: Understanding the SQL Queries&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Without AsSplitQuery()&lt;/strong&gt;&lt;br&gt;
EF Core runs one big SQL query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT o.*, c.*, oi.*
FROM Orders o
LEFT JOIN Customers c ON o.CustomerId = c.Id
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This may lead to repetitive customer data for each order item.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With AsSplitQuery()&lt;/strong&gt;&lt;br&gt;
EF Core runs separate queries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM Orders;
SELECT * FROM Customers WHERE Id IN (...);
SELECT * FROM OrderItems WHERE OrderId IN (...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reduces duplication and improves performance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When to Use AsSplitQuery()&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✅ Use AsSplitQuery() when:&lt;/p&gt;
&lt;p&gt;You include multiple navigation properties (Include()).&lt;br&gt;
You experience performance issues with large datasets.&lt;br&gt;
You want to avoid data duplication.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;❌ Avoid AsSplitQuery() when:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Your queries are already small and fast.&lt;br&gt;
Your database round trips increase due to multiple queries.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Using AsSplitQuery() in EF Core can boost performance and reduce memory usage when working with complex queries. It’s a simple yet powerful optimization that can make your applications more efficient.&lt;/p&gt;
</description>
<category>ASP.NET</category>
<guid isPermaLink="true">https://coderlegion.com/1255/when-to-use-assplitquery-in-ef-core</guid>
<pubDate>Wed, 26 Feb 2025 08:10:09 +0000</pubDate>
</item>
</channel>
</rss>