<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Wed, 14 Oct 2026 18:41:24 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=oguzhan.agir" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/blazor-.net-11-whats-new-in-virtualize-anchormode-and-csp-support-hdpestha</guid>
      <link>https://abp.io/community/posts/blazor-.net-11-whats-new-in-virtualize-anchormode-and-csp-support-hdpestha</link>
      <a10:author>
        <a10:name>oguzhan.agir</a10:name>
        <a10:uri>https://abp.io/community/members/oguzhan.agir</a10:uri>
      </a10:author>
      <category>blazor</category>
      <category>asp.net-core</category>
      <category>new-features</category>
      <category>performance</category>
      <category>.net</category>
      <title>Blazor .NET 11: What's New in Virtualize, AnchorMode, and CSP Support</title>
      <description>Explore Blazor .NET 11's Virtualize improvements, including variable-height items, scroll anchoring, AnchorMode, and strict Content Security Policy support. Build efficient feeds, chats, and data grids with practical code examples, performance benchmarks, cross-browser tests, and production considerations based on .NET 11 RC1.</description>
      <pubDate>Wed, 23 Sep 2026 06:40:57 Z</pubDate>
      <a10:updated>2026-10-14T16:12:59Z</a10:updated>
      <content:encoded><![CDATA[<h1>Building High-Performance Blazor Lists in .NET 11: Virtualize, AnchorMode, and Strict CSP</h1>
<blockquote>
<p><strong>Release status.</strong> Everything in this article was built and tested on <strong>.NET 11 RC1</strong> (SDK <code>11.0.100-rc.1.26425.128</code>, runtime <code>11.0.0-rc.1.26425.128</code>, released September 8, 2026). RC1 has a go-live license, but it isn't the final release. APIs, generated markup, defaults, and browser behavior can still change before .NET 11 GA, which is planned for .NET Conf in November 2026. Rerun the sample's tests against each new SDK before you ship.</p>
</blockquote>
<p>.NET 11 makes <code>Virtualize&lt;TItem&gt;</code> a viable choice for dynamic feeds, chats, logs, and variable-height collections. Before, it was mostly a component for predictable, uniform lists. It also works under a strict Content Security Policy for the first time. This article covers the changes behind that. It shows them in a runnable sample app, and it reports what I measured in Chrome, Firefox, and WebKit, including the rough edges that RC1 still has.</p>
<p><strong>TL;DR</strong></p>
<ul>
<li><p><strong>Variable heights:</strong> <code>Virtualize</code> now measures rendered items and keeps a running average item height. <code>ItemSize</code> is only the starting estimate.</p>
</li>
<li><p><strong>Scroll anchoring:</strong> When something above the viewport changes height, what you're reading now normally stays put. Lists built from <code>div</code>s use the browser's native CSS scroll anchoring. <code>&lt;table&gt;</code> layouts, and browsers without <code>overflow-anchor</code>, use a <code>ResizeObserver</code>-based fallback.</p>
</li>
<li><p><strong><code>AnchorMode</code>:</strong> A new parameter of the <code>[Flags]</code> enum <code>VirtualizeAnchorMode</code>. <code>Start</code>, the default, shows new items when you're at the top. <code>End</code> follows new items when you're at the bottom. <code>None</code> does neither. Away from the edges, every mode keeps the content you're reading in place when items are inserted.</p>
</li>
<li><p><strong>Scroll APIs:</strong> <code>InitialItemIndex</code> and <code>ScrollToItemAsync</code> finally let you open a list at a given item and jump to any item.</p>
</li>
<li><p><strong>Overscan:</strong> The <code>OverscanCount</code> default went from <strong>3 to 15</strong>. That means more DOM, and bigger <code>ItemsProvider</code> requests.</p>
</li>
<li><p><strong>CSP:</strong> Spacer heights now travel in <code>data-blazor-virtualize-*</code> attributes and are applied through CSSOM, so <code>style-src 'self'</code> no longer breaks virtualization. On .NET 10, the same policy collapsed a 100,000-item list to a 1,227 px scroll range.</p>
</li>
<li><p><strong>RC1 caveats I measured:</strong></p>
<ul>
<li><code>&lt;table&gt;</code> rows that grow right after scrolling are only partly compensated.</li>
<li>The first render after <code>InitialItemIndex</code> can move the content by 53–82 px.</li>
<li><code>AnchorMode.End</code> sometimes stops 29–77 px short of the bottom after an append. In Playwright's WebKit build that happened in 10 of 30 checks; in Chrome and Firefox, in 1–2 of 30.</li>
<li>In Playwright's WebKit build, a <code>Start</code> list jumps when items are appended at the very end.</li>
<li>The docs' <code>AnchorMode=&quot;Start&quot;</code> shorthand doesn't compile.</li>
</ul>
<p>Details and workarounds are below.</p>
</li>
</ul>
<h2>Large lists are not just a rendering problem</h2>
<p>Picture an operations dashboard that shows 100,000 events. New events arrive every second and descriptions vary in length. Users have to be able to inspect older entries without being yanked back to the newest one every time something happens.</p>
<p>A naive <code>@foreach</code> over those events runs into trouble on several fronts at once:</p>
<ul>
<li><strong>DOM size.</strong> In the sample, 100,000 simple three-cell rows produced 400,078 DOM elements. Layout, style recalculation, and memory all grow with that number, and richer rows are worse.</li>
<li><strong>Render cost.</strong> Blazor has to diff and describe every row. On <strong>Blazor Server</strong>, that description is a render batch sent over SignalR. On <strong>Blazor WebAssembly</strong>, it runs on the browser's main thread.</li>
<li><strong>Prerendered HTML.</strong> With prerendering, which the Blazor Web App template has on by default, every row is also rendered into the initial HTML document. The <code>foreach</code> version of the sample's 100,000-item list produced a <strong>28.6 MB</strong> HTML response, compared with <strong>11.7 KB</strong> for the virtualized version. The <code>foreach</code> page also took almost two minutes to become interactive (<a href="#performance-what-i-measured">section 12</a>).</li>
<li><strong>Constant change.</strong> Feeds, chats, and log viewers change while people read them. Prepending an item or expanding a card above the viewport moves the content under the reader's eyes.</li>
</ul>
<p>Three separate ideas are at play here. Real apps usually need all three, but they solve different problems:</p>
<p>| Concept | What it solves | Blazor feature |
|---|---|---|
| <strong>Virtualization</strong> | Only render what's (nearly) visible | <code>Virtualize&lt;TItem&gt;</code> |
| <strong>Incremental loading</strong> | Only <em>fetch</em> what's (nearly) visible | <code>ItemsProvider</code> + <code>ItemsProviderRequest</code> |
| <strong>Scroll anchoring</strong> | Keep what the user is reading still while data changes | .NET 11 anchoring + <code>AnchorMode</code> |</p>
<h2>What <code>Virtualize&lt;TItem&gt;</code> actually does</h2>
<p><code>Virtualize&lt;TItem&gt;</code> renders a <em>window</em> of items inside a scroll container. It stands in for everything outside that window with two spacer elements.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/diagrams/virtualize-anatomy.png" alt="Anatomy of a virtualized list: a top spacer, overscan items, the visible items, more overscan, and a bottom spacer inside a fixed-height scroll container" /></p>
<p><em>Figure 1. The two spacers reserve the height of the items that aren't rendered. An <code>IntersectionObserver</code> watches the spacers and tells .NET when one of them scrolls into view.</em></p>
<p>Here's how it works:</p>
<ol>
<li>It finds the closest scrollable ancestor. In RC1, that's the first ancestor whose <code>overflow-y</code> is not <code>visible</code>, <code>hidden</code>, or <code>clip</code>. If there's none, it uses the document itself.</li>
<li>It renders the visible items plus <code>OverscanCount</code> extra items before and after them.</li>
<li>It renders a spacer before and after the window. Each spacer's height is <em>number of items it replaces</em> × <em>average item height</em>.</li>
<li>An <code>IntersectionObserver</code> (50 px root margin) watches both spacers. When one becomes visible, JavaScript reports the sizes to .NET, which computes a new window and renders it.</li>
</ol>
<p>This <code>foreach</code>:</p>
<pre><code class="language-razor">&lt;div class=&quot;stream-viewport&quot;&gt;
    @foreach (var item in items)
    {
        &lt;ActivityCard @key=&quot;item.Id&quot; Item=&quot;item&quot; /&gt;
    }
&lt;/div&gt;
</code></pre>
<p>becomes this:</p>
<pre><code class="language-razor">&lt;div class=&quot;stream-viewport&quot; tabindex=&quot;0&quot;&gt;
    &lt;Virtualize Items=&quot;items&quot; Context=&quot;item&quot;&gt;
        &lt;ActivityCard @key=&quot;item.Id&quot; Item=&quot;item&quot; /&gt;
    &lt;/Virtualize&gt;
&lt;/div&gt;
</code></pre>
<p><code>Virtualize</code> doesn't require the whole collection to be in memory:</p>
<ul>
<li><strong><code>Items</code></strong> takes an in-memory <code>ICollection&lt;TItem&gt;</code>. The component slices it directly.</li>
<li><strong><code>ItemsProvider</code></strong> takes a delegate. It receives an <code>ItemsProviderRequest</code> (<code>StartIndex</code>, <code>Count</code>, <code>CancellationToken</code>) and returns an <code>ItemsProviderResult&lt;TItem&gt;</code> (the slice plus <code>TotalItemCount</code>). Only the requested range needs to be loaded from the backend.</li>
</ul>
<p><strong>Prerendering emits (almost) no items.</strong> During static prerendering, <code>Virtualize</code> normally emits only its spacers. Real items appear once the component becomes interactive, which is why the virtualized HTML above was only 11.7 KB.</p>
<ul>
<li>With <code>Items</code>, the prerendered trailing spacer is item count × <code>ItemSize</code>.</li>
<li>With an <code>ItemsProvider</code>, the provider isn't called during prerendering, so both spacers are 0 px. The sample's <code>/feed</code> and <code>/grid</code> pages prerender <code>data-blazor-virtualize-reserved-height=&quot;0&quot;</code> twice.</li>
<li>One exception in RC1: with in-memory <code>Items</code> and <code>InitialItemIndex</code> greater than 0, the window moves to the target before the first render. Up to <code>2 × OverscanCount + 1</code> items around it are then prerendered. The sample's <code>/chat</code> page prerenders messages #99,985–#100,000.</li>
</ul>
<p>If you need all items in the initial HTML, for SEO or for no-JS clients, virtualization isn't the right tool. See <a href="#when-not-to-use-virtualize">section 20</a>.</p>
<h2>What changed in .NET 11</h2>
<p>| | .NET 10 | .NET 11 RC1 | Introduced in |
|---|---|---|---|
| Variable-height items | Assumed uniform height (<code>ItemSize</code>) | Running average of measured heights | Preview 3 |
| Default <code>OverscanCount</code> | 3 | <strong>15</strong> (QuickGrid stays at 3) | Preview 3 |
| Content stability when items above resize | Native scroll anchoring disabled, so content jumped | Native CSS anchoring or <code>ResizeObserver</code> compensation | Preview 4 |
| Edge pinning | Hand-written JS | <code>AnchorMode</code> (<code>None</code>, <code>Start</code>, <code>End</code>, combinable) | Preview 4 (renamed in Preview 7) |
| Prepend/append detection for providers | — | <code>ItemComparer</code> (defaults to <code>EqualityComparer&lt;T&gt;.Default</code>) | Preview 4 / Preview 7 |
| Open at / jump to an item | Hand-written JS | <code>InitialItemIndex</code>, <code>ScrollToItemAsync</code> | Preview 6 (renamed in Preview 7) |
| Strict <code>style-src 'self'</code> | Spacers render <code>style=&quot;height:…&quot;</code>, which the policy blocks | Spacer sizes rendered as <code>data-*</code> attributes and applied via CSSOM | Preview 6 |</p>
<p>To avoid relying only on the docs, I dumped the public surface from the RC1 shared framework with reflection, using a small file-based C# app. Abridged output, with <code>TItem</code> shown generically:</p>
<pre><code class="language-text">Assembly: Microsoft.AspNetCore.Components.Web 11.0.0-rc.1.26425.128
[Parameter] VirtualizeAnchorMode AnchorMode = Start
[Parameter] Int32 InitialItemIndex = 0
[Parameter] IEqualityComparer`1 ItemComparer = (EqualityComparer&lt;TItem&gt;.Default)
[Parameter] Single ItemSize = 50
[Parameter] Int32 MaxItemCount = 100
[Parameter] Int32 OverscanCount = 15
[Parameter] String SpacerElement = div
method Task ScrollToItemAsync(Int32 itemIndex, CancellationToken cancellationToken = default)
enum VirtualizeAnchorMode [Flags]  None = 0, Start = 1, End = 2
</code></pre>
<blockquote>
<p><strong>Reading older preview blog posts?</strong> Preview 7 renamed three APIs: <code>VirtualizeAnchorMode.Beginning</code> → <code>Start</code>, <code>InitialIndex</code> → <code>InitialItemIndex</code>, and <code>ScrollToIndexAsync</code> → <code>ScrollToItemAsync</code>. Code from Preview 4–6 posts won't compile on RC1 until you apply those renames.</p>
</blockquote>
<h3>Variable-height items</h3>
<p>Earlier versions assumed that every item had the height given by <code>ItemSize</code>. In .NET 11, <code>ItemSize</code> is only the <strong>initial estimate</strong>. Once items render, the JavaScript side measures the total height of everything rendered between the two spacers, using a DOM <code>Range</code>. It sends that total to .NET along with the spacer and container sizes. .NET adds it to a running total and uses <code>totalMeasuredHeight / measuredItemCount</code> for every spacer calculation after that. Measurements are only taken when no placeholders are on screen, so placeholder heights don't distort the average.</p>
<p>You can see this in the sample. The activity feed starts with <code>ItemSize=&quot;96&quot;</code> and 100,000 items, so the first scroll range is 100,000 × 96 ≈ <strong>9.6 million px</strong>. After the first measurements it settles at about <strong>8.4 million px</strong>, an average of about 84 px per card.</p>
<p>Two consequences:</p>
<ul>
<li><code>Virtualize</code> doesn't know the exact height of each item. It estimates the unrendered ones from the average. When you jump by dragging the scrollbar far into an unmeasured region, the first position is an estimate and may be corrected slightly.</li>
<li>A better <code>ItemSize</code> still helps, because it makes the first render and the first scroll range closer to reality.</li>
</ul>
<h3>3.2 Hybrid scroll anchoring</h3>
<p>In .NET 10, <code>Virtualize</code> turned off the browser's native scroll anchoring, because spacer resizes could otherwise start a feedback loop. As a result, content jumped whenever an item above the viewport changed height. .NET 11 uses one of two paths instead:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/diagrams/scroll-anchoring-paths.png" alt="Decision flow: tables and browsers without overflow-anchor use manual ResizeObserver compensation; other lists use native CSS scroll anchoring" /></p>
<p><em>Figure 2. The path is chosen by feature detection (<code>CSS.supports('overflow-anchor', 'auto')</code>) and by whether the spacers' parent is a table section. RC1 does no user-agent sniffing.</em></p>
<p>You can see the choice in the DOM:</p>
<ul>
<li>In the sample's <code>div</code>-based feed, the scroll container keeps <code>overflow-anchor: auto</code> and only the spacers get <code>overflow-anchor: none</code>.</li>
<li>In the <code>&lt;table&gt;</code> grid, the container itself gets <code>overflow-anchor: none</code> and the manual path takes over.</li>
</ul>
<p>Chrome, Firefox, and Playwright's WebKit build all behaved this way, and all three report <code>overflow-anchor</code> support. Real Safari is a different story: according to MDN's compatibility data it supports <code>overflow-anchor</code> only from <strong>Safari 27</strong>. On older Safari and iOS versions, even <code>div</code> lists take the manual path. Playwright's WebKit build doesn't exercise that path.</p>
<h3>The <code>OverscanCount</code> default changed from 3 to 15</h3>
<p>More overscan means fewer blank moments during fast scrolling and more items to average heights over. It also means:</p>
<ul>
<li><strong>More DOM.</strong> The sample's feed renders 37–38 cards at the top of a 100,000-item list, compared with about 7 visible ones.</li>
<li><strong>Bigger <code>ItemsProvider</code> requests.</strong> With 15 items before and after the visible range, a request asks for roughly <code>visible + 30</code> items instead of <code>visible + 6</code>.</li>
<li><strong>No breaking-change notice.</strong> The change isn't listed on the .NET 11 or ASP.NET Core 11 breaking-changes pages, so check for it yourself when you upgrade.</li>
</ul>
<p><code>QuickGrid</code> deliberately keeps <code>OverscanCount = 3</code>, because grid rows are heavier. See <a href="#choosing-itemsize-and-overscancount">section 13</a> for how to choose.</p>
<h2>The sample application: LiveStream</h2>
<p>The sample is a Blazor Web App (Interactive Server) that renders a deterministic dataset of 100,000 synthetic events. Every scenario in this article is a page in it. The sample isn't published with this article. The code blocks below are excerpts from it, and the layout shows where each excerpt lives.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/01-home.png" alt="LiveStream home page showing scenario cards, the runtime version 11.0.0-rc.1, and the strict Content-Security-Policy" /></p>
<pre><code class="language-text">LiveStream/
├── global.json                      # pins the .NET 11 RC1 SDK
├── src/LiveStream/
│   ├── Components/
│   │   ├── Pages/ActivityFeed.razor  # ItemsProvider + AnchorMode.Start
│   │   ├── Pages/LiveChat.razor      # Items + AnchorMode.End + InitialItemIndex
│   │   ├── Pages/DataGrid.razor      # &lt;table&gt; + ScrollToItemAsync
│   │   ├── Pages/Labs/AnchorLab.razor
│   │   ├── Pages/Labs/PerfLab.razor
│   │   └── Stream/                   # ActivityCard, ChatMessage, StatsBar, ReadyMarker
│   ├── Models/StreamItem.cs
│   ├── Services/SyntheticDataset.cs  # shared, immutable 100k items
│   ├── Services/ActivityFeedStore.cs # per-circuit view + ItemsProvider
│   ├── Security/ContentSecurityPolicy.cs
│   └── wwwroot/js/                   # csp-monitor.js, stream-interop.js
├── baseline/CspBaseline.Net10/       # same CSP on .NET 10, for comparison
├── tests/LiveStream.Tests/           # xUnit + WebApplicationFactory
├── tools/ApiSurface.cs              # dumps the Virtualize API (section 3)
└── e2e/                              # Playwright tests, measurement scripts, results/
</code></pre>
<p>Technical choices:</p>
<ul>
<li><strong>Interactive Server</strong> keeps the sample easy to run. The virtualization code is the same on WebAssembly. <a href="#browser-and-layout-caveats">Section 17</a> covers the differences.</li>
<li><strong><code>net11.0</code></strong>, with the SDK pinned in <code>global.json</code>.</li>
<li><strong>100,000 records</strong>, with messages ranging from one to six lines. About 18% have an expandable diagnostics block.</li>
<li><strong>150–400 ms simulated latency</strong> in the feed's provider, with cancellation.</li>
<li><strong>A strict CSP header on every page:</strong> <code>style-src 'self'</code> and a per-request nonce for the one inline script (the import map). A badge in the corner counts violations live.</li>
</ul>
<p>To try the ideas yourself, start from the Blazor Web App template on the .NET 11 SDK:</p>
<pre><code class="language-bash">dotnet new blazor --interactivity Server --all-interactive -o LiveStream
cd LiveStream
dotnet run
</code></pre>
<p>Then add the pieces from the excerpts: the model and provider (<a href="#the-data-model-and-the-itemsprovider">next section</a>), the feed and chat pages, and the CSP middleware (<a href="#content-security-policy-why-virtualization-used-to-break">Content Security Policy</a>).</p>
<h2>The data model and the <code>ItemsProvider</code></h2>
<p>The model is a record:</p>
<pre><code class="language-csharp">public sealed record StreamItem(
    long Id,
    string Author,
    string Message,
    string? Details,
    DateTimeOffset CreatedAt,
    StreamItemKind Kind)
{
    /// &lt;summary&gt;
    /// Identity by key. Virtualize uses ItemComparer to tell whether items were prepended
    /// or appended between ItemsProvider calls. The default (record value equality) compares
    /// every field, so an edited item would look like a different item; the Id doesn't change.
    /// &lt;/summary&gt;
    public static IEqualityComparer&lt;StreamItem&gt; ById { get; } =
        EqualityComparer&lt;StreamItem&gt;.Create((a, b) =&gt; a?.Id == b?.Id, item =&gt; item.Id.GetHashCode());
}
</code></pre>
<p>Why a comparer, when records already have value equality? When data comes from an <code>ItemsProvider</code> and the total count grows, <code>Virtualize</code> compares the first item it rendered before with the first item of the new result. A mismatch means &quot;items were inserted above&quot;. Record equality works until that first rendered item is <em>edited</em> in a refresh where the count also grew, which happens constantly in a live feed. The old and new versions then compare unequal, and an edit looks like a prepend. A key-based comparer avoids that.</p>
<p>The RC1 SDK agrees. Building the sample without a comparer produced a new analyzer warning:</p>
<pre><code class="language-text">warning BL0011: Virtualize uses 'ItemsProvider' without 'ItemComparer'.
Set ItemComparer to an IEqualityComparer that identifies items by a unique key.
</code></pre>
<p>(With in-memory <code>Items</code>, <code>Virtualize</code> ignores <code>ItemComparer</code> and uses <code>EqualityComparer&lt;TItem&gt;.Default</code>.)</p>
<p>The provider is the store method that <code>Virtualize</code> calls:</p>
<pre><code class="language-csharp">private async ValueTask&lt;ItemsProviderResult&lt;StreamItem&gt;&gt; GetAsync(ItemsProviderRequest request, bool newestFirst)
{
    Interlocked.Increment(ref _providerCalls);
    LastRequest = (request.StartIndex, request.Count);

    try
    {
        var (min, max) = Latency;
        if (max &gt; 0)
        {
            // Simulated network + database time. Virtualize cancels this token
            // when the user scrolls on before the response arrives.
            await Task.Delay(Random.Shared.Next(min, max + 1), request.CancellationToken);
        }
    }
    catch (OperationCanceledException)
    {
        Interlocked.Increment(ref _canceledCalls);
        throw;
    }

    return GetRange(request.StartIndex, request.Count, newestFirst);
}
</code></pre>
<ul>
<li><strong><code>request.StartIndex</code></strong> is the index of the first item to return.</li>
<li><strong><code>request.Count</code></strong> covers the visible range plus overscan on both sides. It's larger in .NET 11 because of the new default.</li>
<li><strong><code>request.CancellationToken</code></strong> is canceled when a newer request replaces this one. In the browser test, scrolling quickly through the feed produced canceled provider calls in all three engines. Pass the token all the way down to your database or HTTP call.</li>
<li><strong><code>TotalItemCount</code></strong> (the second constructor argument of the result) sets the spacer sizes. It has to be accurate and cheap.</li>
</ul>
<blockquote>
<p><strong>Offset pagination gets expensive.</strong> <code>Skip(n).Take(m)</code> is fine for a demo. Deep offsets can be slow in relational databases, and running <code>COUNT(*)</code> on every request adds up. For production feeds, consider keyset (cursor) pagination behind a layer that maps virtual indexes to stable cursors, and cache the total count.</p>
</blockquote>
<p>One design detail in the sample is worth copying. The 100,000 base records are a <strong>shared, immutable singleton</strong>. Each circuit keeps only its own new events in a small list, so adding an item costs O(1) and a new circuit costs a few hundred bytes instead of a copy of the dataset.</p>
<h2>Scenario 1: an activity feed with <code>AnchorMode.Start</code></h2>
<p>New activity arrives at the <strong>top</strong> of the feed. It's newest first, and index 0 is the newest event.</p>
<pre><code class="language-razor">&lt;div class=&quot;stream-viewport&quot; tabindex=&quot;0&quot; @ref=&quot;viewport&quot; aria-label=&quot;Activity feed, newest first&quot;&gt;
    &lt;Virtualize @ref=&quot;virtualize&quot; TItem=&quot;StreamItem&quot; ItemsProvider=&quot;provider&quot; ItemComparer=&quot;StreamItem.ById&quot;
                Context=&quot;item&quot; ItemSize=&quot;96&quot; AnchorMode=&quot;VirtualizeAnchorMode.Start&quot;&gt;
        &lt;ItemContent&gt;
            &lt;ActivityCard @key=&quot;item.Id&quot; Item=&quot;item&quot; Expanded=&quot;expanded.Contains(item.Id)&quot; OnToggle=&quot;ToggleDetails&quot; /&gt;
        &lt;/ItemContent&gt;
        &lt;Placeholder&gt;
            &lt;div class=&quot;activity-card activity-card--placeholder&quot; aria-hidden=&quot;true&quot;&gt;…&lt;/div&gt;
        &lt;/Placeholder&gt;
        &lt;EmptyContent&gt;
            &lt;p class=&quot;stream-empty&quot;&gt;No activity has been recorded yet.&lt;/p&gt;
        &lt;/EmptyContent&gt;
    &lt;/Virtualize&gt;
&lt;/div&gt;
</code></pre>
<p><code>provider</code> is assigned once in <code>OnInitialized</code> (<code>provider = Store.GetNewestFirstAsync;</code>), so every render passes the same delegate instance. New events, whether from the simulated live stream or the &quot;+5 events&quot; button, aren't pushed to the UI one at a time. They go into a queue, and a 250 ms <code>PeriodicTimer</code> flushes them in batches:</p>
<pre><code class="language-csharp">private async Task FlushAsync()
{
    List&lt;StreamItem&gt; batch = [];
    while (pending.TryDequeue(out var item))
    {
        batch.Add(item);
    }

    if (batch.Count == 0 || virtualize is null)
    {
        return;
    }

    Store.Add(batch);

    if (!atTop)
    {
        unseen += batch.Count;
        announcement = $&quot;{unseen} new events above.&quot;;
    }

    // ItemsProvider data changed outside of Virtualize: ask it to re-request the visible range.
    await virtualize.RefreshDataAsync();
    StateHasChanged();
}
</code></pre>
<p><code>atTop</code> comes from a small JS helper, shown in the next section, that reports when the user reaches or leaves the top edge. It uses the same definition as <code>AnchorMode.Start</code> in RC1: <code>scrollTop &lt; 1</code>. A looser threshold would create a gap. The page would think the user was at the top and skip the pill, while <code>Virtualize</code> would already be keeping the new events out of view.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/02-activity-feed.png" alt="Activity feed after five live events: newest first, variable-height cards, a stats bar with 100,005 total events and 38 cards in the DOM, and the green CSP badge" /></p>
<h3>Expected UI behavior, and what I measured</h3>
<p>| Situation | Expected | Measured (Chrome, Firefox, WebKit) |
|---|---|---|
| At the top, 5 events arrive | The view stays at the top and shows the new events | <code>scrollTop</code> stays 0; the first visible card is the newest |
| Reading older events, 5 events arrive | What you're reading doesn't move; an &quot;↑ 5 new events&quot; pill appears | Same first visible card, offset changed by ≤ 1 px |
| A card <em>above</em> the viewport expands | Visible content stays put | 0 px shift (wheel scrolling, 6/6 runs per engine) |
| DOM size | Only a window of the 100,000 events | 37–38 cards rendered |
| Fast scrolling with 150–400 ms latency | Stale requests are canceled | Canceled provider calls &gt; 0 in every run of the test |</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/03-activity-feed-new-events.png" alt="Activity feed while scrolled down: an &quot;↑ 5 new events&quot; pill floats above the list while the visible cards stay in place" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/feed-anchor-start.gif" alt="Animated: new events appear at the top; while reading older events, new events don't move the view; clicking the pill jumps to the newest" /></p>
<p><em>The pill calls <code>ScrollToItemAsync(0)</code>. The GIF is a screen recording of the sample in Chrome.</em></p>
<h2>Scenario 2: a chat or log viewer with <code>AnchorMode.End</code></h2>
<p>In a chat or log viewer, new items arrive at the <strong>bottom</strong>, and the list should open there too:</p>
<pre><code class="language-razor">&lt;div class=&quot;stream-viewport stream-viewport--chat&quot; tabindex=&quot;0&quot; @ref=&quot;viewport&quot; aria-label=&quot;Chat messages, oldest first&quot;&gt;
    &lt;Virtualize @ref=&quot;virtualize&quot; Items=&quot;messages&quot; Context=&quot;message&quot;
                ItemSize=&quot;72&quot; AnchorMode=&quot;VirtualizeAnchorMode.End&quot;
                InitialItemIndex=&quot;@(messages.Count - 1)&quot;&gt;
        &lt;ChatMessage @key=&quot;message.Id&quot; Message=&quot;message&quot; Mine=&quot;@(message.Author == Me)&quot; /&gt;
    &lt;/Virtualize&gt;
&lt;/div&gt;
</code></pre>
<ul>
<li><code>Items</code> is a <code>List&lt;StreamItem&gt;</code>. With in-memory <code>Items</code>, <code>Virtualize</code> re-reads the collection on every render, so appending is just <code>messages.Add(...)</code> followed by <code>StateHasChanged()</code>. You don't need <code>RefreshDataAsync</code>.</li>
<li><code>InitialItemIndex=&quot;@(messages.Count - 1)&quot;</code> opens the list at the latest message. It's applied once, on the first interactive render.</li>
<li><code>AnchorMode.End</code> follows new messages <strong>only while the user is at the bottom</strong>. In RC1, &quot;at the bottom&quot; means less than 2 px away.</li>
</ul>
<p>The core UX rule for chat UIs is to follow new messages only while the user is already following the live edge. <code>AnchorMode.End</code> implements that rule. The sample adds the other half: an unread counter and a way back to the bottom. A tiny JS module reports when the user leaves or reaches an edge, using the same thresholds as <code>Virtualize</code> (1 px for the top, 2 px for the bottom). It calls .NET only when that state flips, not on every scroll event:</p>
<pre><code class="language-js">export function watchEdge(element, dotNetRef, edge, threshold) {
    let atEdge = null;
    let frame = 0;

    const check = () =&gt; {
        frame = 0;
        const distance = edge === 'end'
            ? element.scrollHeight - element.clientHeight - element.scrollTop
            : element.scrollTop;
        const now = distance &lt; threshold;
        if (now !== atEdge) {
            atEdge = now;
            dotNetRef.invokeMethodAsync('OnEdgeChanged', now);
        }
    };
    // … a scroll listener and a ResizeObserver schedule check() once per animation frame
}
</code></pre>
<pre><code class="language-razor">@if (unread &gt; 0)
{
    &lt;button type=&quot;button&quot; class=&quot;new-items-pill new-items-pill--bottom&quot; @onclick=&quot;JumpToLatestAsync&quot;&gt;
        ↓ @unread new @(unread == 1 ? &quot;message&quot; : &quot;messages&quot;)
    &lt;/button&gt;
}
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/04-live-chat-unread.png" alt="Live chat after scrolling up: the view stays on older messages and a &quot;↓ 20 new messages&quot; pill is shown at the bottom" /></p>
<h3>Expected UI behavior, and what I measured</h3>
<p>| Situation | Expected | Measured |
|---|---|---|
| Page opens | At the bottom, message #100,000 visible | ✔ all engines (<code>InitialItemIndex</code>) |
| At the bottom, a message arrives | Scrolls to show it | With the sample's guard: ✔ 30/30 follow checks per engine. <code>AnchorMode.End</code> alone: Chrome 29/30, Firefox 28/30, <strong>WebKit 20/30</strong> (see below) |
| Scrolled up, 20 messages arrive | View stays put; &quot;↓ 20 new messages&quot; pill | ✔ all engines, <code>scrollTop</code> unchanged |
| User returns to the bottom | Following resumes | ✔ all engines |</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/chat-anchor-end.gif" alt="Animated: at the bottom new messages are followed; after scrolling up, new messages don't pull the view down; clicking the pill returns to the live edge and following resumes" /></p>
<h3>An RC1 caveat: End-follow sometimes stops short</h3>
<p>I ran the chat tests 10 times per engine with the sample's safety net turned off. Each run contains three checks that depend on <code>AnchorMode.End</code> following an append: a new message, a message sent from the composer, and a new message after returning to the bottom. They passed <strong>29/30 in Chrome, 28/30 in Firefox, and 20/30 in Playwright's WebKit</strong>. In every failure, the list ended <strong>29–77 px short</strong> of the bottom 600 ms after the append (Chrome 72 px, Firefox 61 px, WebKit 29–77 px), so the new message was only partly visible. Outside the test suite the miss was rare: in isolated WebKit runs.</p>
<p>The sample therefore adds a small, explicit safety net. It only acts if the page's edge watcher considered the user to be at the live edge (less than 2 px, the same as <code>AnchorMode.End</code>) when the message arrived, <em>and</em> the list then ended up between 1 and 120 px short of the bottom:</p>
<pre><code class="language-js">export function stickToBottom(element, tolerance) {
    requestAnimationFrame(() =&gt; {
        const distance = element.scrollHeight - element.clientHeight - element.scrollTop;
        if (distance &gt; 1 &amp;&amp; distance &lt;= tolerance) {
            element.scrollTop = element.scrollHeight;
        }
    });
}
</code></pre>
<p>With the guard on (the sample's default), the same 10× run passed <strong>all 40 chat tests, including 30/30 follow checks, in every engine</strong>. The guard-off numbers above were measured with the guard disabled, to observe <code>AnchorMode.End</code> on its own. An End-anchoring fix is already merged for RC2 (<a href="https://github.com/dotnet/aspnetcore/pull/69388">dotnet/aspnetcore#69388</a>). It changes exactly the condition that decides when to pin, so retest before you keep or remove a workaround like this. Playwright's WebKit on Windows is also not Safari on macOS or iOS, so test on real devices too.</p>
<h2>Understanding every <code>AnchorMode</code>, measured</h2>
<p>The docs describe the modes in terms of scroll position. To find out what they actually do, the sample's <strong>AnchorMode lab</strong> (<code>/lab/anchor</code>) prepends or appends 5 items at the top, middle, and bottom of a 2,000-item variable-height list. It runs every mode with both <code>Items</code> and <code>ItemsProvider</code>, in Chrome, Firefox, and WebKit.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/diagrams/anchor-modes.png" alt="A 3×3 grid showing None, Start, and End for three situations: at the top with items prepended, reading the middle, and at the bottom with items appended" /></p>
<p>| Where the user is | What arrives | <code>None</code> | <code>Start</code> (default) | <code>End</code> | <code>Start \| End</code> |
|---|---|---|---|---|---|
| At the top | 5 items prepended | Keeps place | <strong>Shows the new items</strong> | Keeps place | <strong>Shows the new items</strong> |
| At the top | 5 items appended | Keeps place | Keeps place | Keeps place | Keeps place |
| In the middle | 5 items prepended | Keeps place | Keeps place | Keeps place | Keeps place |
| In the middle | 5 items appended | Keeps place | Keeps place | Keeps place | Keeps place |
| At the bottom | 5 items prepended | Keeps place | Keeps place | Keeps place | Keeps place |
| At the bottom | 5 items appended | Keeps place | Keeps place ⚠️ | <strong>Follows the new items</strong> | <strong>Follows the new items</strong> |</p>
<p><em>&quot;Keeps place&quot; means the item that was first visible before the change was still the first visible item afterward. The matrix records which item that was, not its pixel offset; sections 6 and 10 report pixel shifts. <code>Items</code> and <code>ItemsProvider</code> fell into the same category in every cell. 46 of the 48 scenarios gave the same result in Chrome, Firefox, and WebKit (144 measurements in total). The ⚠️ cell covers both exceptions, which were WebKit with <code>Start</code>, for <code>Items</code> and for <code>ItemsProvider</code>.</em></p>
<p>Three conclusions:</p>
<ol>
<li><strong>Keeping your place is the default everywhere.</strong> <code>AnchorMode</code> doesn't turn anchoring on or off. It decides what happens <strong>at the edges</strong>: <code>Start</code> shows new items when you're at the very top (<code>scrollTop &lt; 1</code>), and <code>End</code> follows new items when you're at the very bottom (within 2 px).</li>
<li><strong><code>None</code> keeps the <em>content</em> still, not the <code>scrollTop</code> number.</strong> The docs say <code>None</code> keeps &quot;the current scroll position&quot;. With <code>None</code>, though, prepending 5 items at the top <em>increased</em> <code>scrollTop</code> by about 400 px, so that the same item stayed in view. If you want new items at the top to become visible, that's <code>Start</code>.</li>
<li><strong>Between the edges, all four modes behave the same.</strong></li>
</ol>
<p><strong>⚠️ WebKit and infinite lists.</strong> In Playwright's WebKit build, appending items while the user was at the very end of a <code>Start</code> list (the default) moved the visible content. It moved <strong>214 px</strong> with <code>Items</code> and <strong>366–486 px</strong> with <code>ItemsProvider</code>, in 3 of 3 runs. With <code>AnchorMode.None</code> it moved <strong>0 px</strong>, and Chrome and Firefox moved 0 px in both modes. That's exactly the classic infinite-scroll pattern: reach the end, and more items load. If a list only ever grows at the end and doesn't need top pinning, <strong>set <code>AnchorMode=&quot;VirtualizeAnchorMode.None&quot;</code></strong> and test on real Safari.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/08-anchor-lab.png" alt="The AnchorMode lab page with the four mode buttons and prepend/append buttons" /></p>
<h3>Gotcha: the docs' Razor shorthand doesn't compile</h3>
<p>The release notes and the virtualization docs show this:</p>
<pre><code class="language-razor">&lt;Virtualize AnchorMode=&quot;Start&quot; ...&gt;   @* from the docs *@
</code></pre>
<p>On RC1, that fails with <code>CS0103: The name 'Start' does not exist in the current context</code>. Razor treats non-string component parameters as C# expressions, so write the enum out in full:</p>
<pre><code class="language-razor">&lt;Virtualize AnchorMode=&quot;VirtualizeAnchorMode.Start&quot; ...&gt;
&lt;Virtualize AnchorMode=&quot;VirtualizeAnchorMode.Start | VirtualizeAnchorMode.End&quot; ...&gt;
</code></pre>
<h3>Other details worth knowing</h3>
<ul>
<li><strong>Upgraded lists get <code>Start</code> automatically.</strong> It's the default because it matches how lists behaved before. If a list shouldn't pin to anything, such as a sortable grid or an infinite list that grows at the end, set <code>None</code> explicitly.</li>
<li><strong><code>AnchorMode</code> can change at runtime.</strong> <code>Virtualize</code> sends the new value to JavaScript after the next render.</li>
<li><strong>Keyboard:</strong> Pressing <kbd>End</kbd> in the scroll container turns on bottom-following even when <code>End</code> isn't set, and <kbd>Home</kbd> clears it. <code>ScrollToItemAsync</code> also clears it.</li>
<li><strong>Deletions above the viewport are not anchored.</strong> Inserts and height changes are, but removing items above the reader still moves content. That's an open backlog request (<a href="https://github.com/dotnet/aspnetcore/issues/66509">dotnet/aspnetcore#66509</a>).</li>
<li><strong><code>Start | End</code> is allowed,</strong> but the design discussion admits there's no known real use case for it.</li>
</ul>
<h2>Refreshing virtualized data without thrashing</h2>
<p>With an <code>ItemsProvider</code>, <code>Virtualize</code> can't know that your data changed. Call <code>RefreshDataAsync()</code>. When the change comes from outside Blazor's event pipeline, such as a timer, a SignalR client, or a message-bus handler, marshal it onto the renderer first. The handler below is illustrative. The sample does the same thing with its <code>PumpAsync</code> loop, which calls <code>InvokeAsync(FlushAsync)</code> (section 6):</p>
<pre><code class="language-csharp">// Called from a timer, a SignalR client, or a message-bus handler, not from a Blazor event.
private Task OnEventsArrivedAsync(IReadOnlyList&lt;StreamItem&gt; batch) =&gt; InvokeAsync(async () =&gt;
{
    Store.Add(batch);

    if (virtualize is not null)
    {
        await virtualize.RefreshDataAsync(); // re-requests the current window
    }

    StateHasChanged(); // RefreshDataAsync doesn't render by itself
});
</code></pre>
<p>Rules that matter in practice:</p>
<ul>
<li><strong>Get back onto the renderer's synchronization context</strong> with <code>InvokeAsync</code>, as above. <code>RefreshDataAsync</code> must run there too.</li>
<li><strong>Call <code>StateHasChanged()</code> afterwards</strong> when the refresh doesn't come from a Blazor event handler. <code>RefreshDataAsync</code> fetches, but it doesn't render.</li>
<li><strong>Batch.</strong> In RC1, <code>RefreshDataAsync()</code> also <strong>resets the running average item height</strong>, so every refresh starts measuring from scratch. Refreshing on every message of a busy stream means more provider calls, more renders, <em>and</em> less accurate spacer estimates. The sample drains its queue with a 250 ms <code>PeriodicTimer</code>, so the list refreshes at most four times per second, however fast events arrive.</li>
<li><strong>Clean up.</strong> Implement <code>IAsyncDisposable</code>: cancel timers and loops, dispose <code>DotNetObjectReference</code>s and JS module references, and catch <code>JSDisconnectedException</code> when the circuit is already gone. The RC1 SDK nudges you here too, with a new analyzer warning: <em>BL0016: JS interop call 'InvokeAsync' is not guarded with a try/catch block</em>.</li>
<li><strong>Pass the cancellation token through.</strong> When a newer request supersedes an older one, the older token is canceled. Ignoring it just wastes backend work.</li>
</ul>
<h2>Variable heights and expansion</h2>
<p>Every card in the sample's feed has a different height, and about 18% can expand a diagnostics block. Three rules make that work well:</p>
<ol>
<li><strong>Keep per-item UI state in the parent, not in the item component.</strong> <code>Virtualize</code> disposes item components that scroll out of the window. An <code>Expanded</code> field inside <code>ActivityCard</code> would be lost as soon as the card scrolled away. The sample keeps a <code>HashSet&lt;long&gt;</code> of expanded IDs in the page and passes <code>Expanded</code> down as a parameter.</li>
<li><strong>Give each item one root element, and no vertical margins.</strong> Heights are measured from the rendered content between the spacers, and collapsing margins between siblings make that fragile. Use padding and borders inside the item instead.</li>
<li><strong>Reserve space for content that loads later.</strong> An image without <code>width</code> and <code>height</code> (or <code>aspect-ratio</code>) changes the row height after it loads. Anchoring compensates, but it's better not to cause the shift in the first place.</li>
</ol>
<h3>How stable is it? Measurements</h3>
<p>In this test I scrolled with real mouse-wheel input, then expanded an item rendered just <em>above</em> the viewport, and recorded how far the first visible item moved. Six runs per cell:</p>
<p>| Layout (anchoring path) | Chrome | Firefox | WebKit |
|---|---|---|---|
| <code>div</code> feed (native CSS anchoring) | <strong>0 px</strong> ×6 | <strong>0 px</strong> ×6 | <strong>0 px</strong> ×6 |
| <code>&lt;table&gt;</code> grid (manual <code>ResizeObserver</code>) | 41–42 px ×4, 222 px ×1, 0 px ×1 | 41–42 px ×4, 0 px ×2 | 0–1 px ×5, 42 px ×1 |
| <code>&lt;table&gt;</code> grid, after ordinary renders first | ≤ 1 px ×6 | 0 px ×6 | ≤ 1 px ×6 |</p>
<p>What's behind the table numbers:</p>
<ul>
<li><strong>Tables have a gap in RC1.</strong> On the manual path, rows are added to the <code>ResizeObserver</code> after ordinary renders but <em>not</em> after scroll-triggered renders. A row that changes height right after a scroll can be observed for the first time with its new height already applied, so there's nothing to compensate against. When I triggered ordinary renders first (expanding and collapsing another row), the shift was <strong>≤ 1 px in 6/6 runs in every engine</strong>. Right after scrolling, Chrome and Firefox usually shifted by one row line (41–42 px; 16 of 24 runs), and Chrome occasionally by about 220 px (2 of 12 runs). WebKit shifted 42 px in 1 of 12 runs and ≤ 1 px otherwise.</li>
<li><strong>Programmatic jumps are harder than wheel scrolling.</strong> I also set <code>scrollTop</code> directly, let the list settle, and then expanded an item above the viewport. In Chrome and WebKit, the <code>div</code> feed didn't move in any run. Firefox moved it 168 px in 1 of 7 runs, and 132–186 px in 3 of 7 runs in an earlier session. The grid moved 41–132 px in Chrome, 0–42 px in Firefox, and 0 px in WebKit. If your app scrolls programmatically and then changes content above the viewport, test that combination.</li>
</ul>
<h2>Data grids: tables, jumping to rows, and QuickGrid</h2>
<p>The sample's grid virtualizes <code>&lt;tr&gt;</code> elements inside <code>&lt;tbody&gt;</code>. The visible column header lives in a separate table <em>outside</em> the scroll container:</p>
<pre><code class="language-razor">@* The visible header sits outside the scroll container. A sticky &lt;thead&gt; inside it would cover the
   row that InitialItemIndex / ScrollToItemAsync align to the container's top edge. *@
&lt;div class=&quot;grid-frame&quot;&gt;
    &lt;div class=&quot;grid-header&quot; aria-hidden=&quot;true&quot;&gt;
        &lt;table class=&quot;event-grid&quot;&gt;&lt;colgroup&gt;…&lt;/colgroup&gt;&lt;thead&gt;…&lt;/thead&gt;&lt;/table&gt;
    &lt;/div&gt;

    &lt;div class=&quot;stream-viewport grid-viewport&quot; tabindex=&quot;0&quot; aria-label=&quot;Event log&quot;&gt;
        &lt;table class=&quot;event-grid&quot;&gt;
            &lt;colgroup&gt;…&lt;/colgroup&gt;
            &lt;thead class=&quot;visually-hidden&quot;&gt;…&lt;/thead&gt; @* column headers for assistive technology *@
            &lt;tbody&gt;
                &lt;Virtualize @ref=&quot;virtualize&quot; TItem=&quot;StreamItem&quot; ItemsProvider=&quot;provider&quot; ItemComparer=&quot;StreamItem.ById&quot;
                            Context=&quot;row&quot; SpacerElement=&quot;tr&quot; ItemSize=&quot;62&quot; AnchorMode=&quot;VirtualizeAnchorMode.None&quot;
                            InitialItemIndex=&quot;@(StartRow is int start ? start - 1 : 0)&quot;&gt;
                    &lt;ItemContent&gt;
                        &lt;tr @key=&quot;row.Id&quot; data-id=&quot;@row.Id&quot; @onclick=&quot;() =&gt; Toggle(row.Id)&quot;&gt;…&lt;/tr&gt;
                    &lt;/ItemContent&gt;
                &lt;/Virtualize&gt;
            &lt;/tbody&gt;
        &lt;/table&gt;
    &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="language-css">/* Same &lt;colgroup&gt; and table-layout: fixed in both tables; scrollbar-gutter keeps the columns aligned. */
.grid-header { flex: none; overflow-y: hidden; scrollbar-gutter: stable; }
.grid-frame .grid-viewport { flex: 1 1 0; scrollbar-gutter: stable; }
.event-grid { width: 100%; table-layout: fixed; }
</code></pre>
<pre><code class="language-csharp">private async Task JumpAsync()
{
    if (virtualize is not null &amp;&amp; jumpTo is int row)
    {
        await virtualize.ScrollToItemAsync(row - 1);
    }
}
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/05-data-grid.png" alt="Event log grid after &quot;Go to row 75000&quot;: row 75,000 at the top, directly below the column header, and row 75,001 expanded with its diagnostics" /></p>
<ul>
<li><strong><code>SpacerElement=&quot;tr&quot;</code></strong> keeps the markup valid. <code>Virtualize</code> gives the spacer rows <code>display: table-row</code> itself.</li>
<li><strong><code>AnchorMode.None</code></strong> is set explicitly, because a grid that users sort and filter shouldn't pin to an edge.</li>
<li><strong><code>ScrollToItemAsync</code></strong> (&quot;Go to row&quot;) and <strong><code>InitialItemIndex</code></strong> (<code>/grid?row=500</code>) put the requested row at the top of the viewport in every engine. Keep in mind:
<ul>
<li><code>ScrollToItemAsync</code> scrolls instantly (no smooth scrolling). It aligns the item with the <strong>top edge of the scroll container</strong> and cancels any earlier call that's still running (the last call wins).</li>
<li>A sticky <code>&lt;thead&gt;</code> <em>inside</em> the scroll container would cover exactly that row. That's why the sample keeps the header outside the container, as shown above.</li>
<li>It throws <code>InvalidOperationException</code> if called before the first interactive render. Use <code>InitialItemIndex</code> for the starting position.</li>
<li>The index is a <strong>position in the list</strong>, not a database identity. If your provider's ordering isn't stable, the jump can land on a different record.</li>
</ul>
</li>
<li><strong>An RC1 issue with <code>InitialItemIndex</code>:</strong> When the grid opened with <code>?row=75000</code>, the <em>next</em> ordinary render (expanding a visible row) moved the rows by <strong>82 px in Chrome, −53 px in Firefox, and 63 px in WebKit</strong>, in 3/3 runs each, while <code>scrollTop</code> stayed the same. The top spacer is recomputed once positioning ends. After <code>ScrollToItemAsync</code> the same action moved them 0 px. RC2 freezes the spacer item size during initial positioning (<a href="https://github.com/dotnet/aspnetcore/pull/69170">dotnet/aspnetcore#69170</a>).</li>
<li><strong>Size <code>ItemSize</code> for the real rows.</strong> The grid's rows average about 62 px (38.6–122 px), because messages wrap. The sample keeps the short columns on one line (<code>white-space: nowrap</code>) so that only the message column wraps.</li>
<li><strong><code>&lt;table&gt;</code> layouts use the manual compensation path.</strong> See the measurements in <a href="#variable-heights-and-expansion">section 10</a>.</li>
</ul>
<p><strong>QuickGrid</strong> forwards <code>InitialItemIndex</code> and <code>ScrollToItemAsync</code> in .NET 11. Its <code>AnchorMode</code> and <code>ItemComparer</code> parameters are marked <code>[Experimental(&quot;ASP0030&quot;)]</code> in RC1, and that attribute has already been removed for RC2. It keeps <code>OverscanCount = 3</code>. Two QuickGrid issues filed against RC1 are still open: Start anchoring drifts after prepends in Server render modes, and the grid sends duplicate provider requests (<a href="https://github.com/dotnet/aspnetcore/issues/69380">#69380</a>, <a href="https://github.com/dotnet/aspnetcore/issues/69381">#69381</a>). If you rely on QuickGrid anchoring, test it specifically.</p>
<h2>Performance: what I measured</h2>
<p>I didn't want illustrative numbers here, so the sample's <strong>Perf lab</strong> (<code>/lab/perf</code>) renders the same row markup in three ways: a plain <code>foreach</code>, <code>Virtualize</code> with <code>Items</code>, and <code>Virtualize</code> with an <code>ItemsProvider</code>. A Playwright script measured each variant three times in fresh Chrome contexts and took the median.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/perf-foreach-vs-virtualize.png" alt="Dot plots on log scales: foreach with 100,000 items takes 112 s to become interactive, creates 400,078 DOM elements and sends 112 MB over the WebSocket; Virtualize takes about 125 ms, 256 elements and 34 KB" /></p>
<p>| Variant (100,000 items unless noted) | HTML | First row painted | Interactive | DOM elements | Rows rendered | JS heap | WebSocket until interactive |
|---|---|---|---|---|---|---|---|
| <code>foreach</code>, 10,000 items | 2.9 MB | 77 ms | 2.8 s | 40,078 | 10,000 | 21.2 MB | 5.6 MB |
| <code>foreach</code> | 28.6 MB | 248 ms | <strong>112.4 s</strong> | 400,078 | 100,000 | 189.5 MB | 111.9 MB |
| <code>Virtualize</code> <code>Items</code>, fixed heights, overscan 15 | 11.7 KB | 106 ms | <strong>125 ms</strong> | 288 | 52 | 2.7 MB | 39 KB |
| <code>Virtualize</code> <code>Items</code>, variable heights, overscan 15 | 11.7 KB | 109 ms | <strong>128 ms</strong> | 256 | 44 | 2.7 MB | 34 KB |
| <code>Virtualize</code> <code>ItemsProvider</code>, variable heights, overscan 15 | 11.7 KB | 108 ms | <strong>125 ms</strong> | 256 | 44 | 2.7 MB | 34 KB |</p>
<p><em>Published Release build, Production environment, Blazor Server on localhost. Chrome 153 on an AMD Ryzen 9 5900HX. Median of 3 fresh browser contexts per variant. &quot;Interactive&quot; is the time from navigation until the first interactive render batch was applied. JS heap is in MiB; HTML and WebSocket sizes use decimal units (1 MB = 1,000,000 bytes).</em></p>
<p>What the numbers say:</p>
<ul>
<li><strong>Prerendering hides the problem for a moment.</strong> The <code>foreach</code> page painted its first row after 248 ms, because the rows are in the HTML. It then took <strong>112 seconds</strong> to become interactive. The circuit had to send 112 MB of render batches for 100,000 rows, and the browser recorded 92 seconds of long tasks on the main thread. Even at 10,000 items it took 2.8 s.</li>
<li><strong>With <code>Virtualize</code>, the rendered window sets the cost, not the list length.</strong> For 100,000 items it took about 125 ms to become interactive, with 256–288 DOM elements, 2.7 MB of JS heap, and 34–39 KB over the WebSocket.</li>
<li><strong><code>Items</code> and <code>ItemsProvider</code> cost the same on the UI side.</strong> The difference is on the server: <code>Items</code> needs the collection in memory, while a provider fetches only the window.</li>
</ul>
<h3>The overscan trade-off</h3>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/perf-overscan.png" alt="Bar charts: overscan 3 renders 20–28 rows but leaves the viewport blank in 52–55 of 60 samples during a fast fling; overscan 15 renders 44–52 rows, with 3 blank samples for fixed heights and 39 for variable heights; overscan 30 renders 74 rows and 32 blank samples" /></p>
<p>To stress each configuration, the harness &quot;flings&quot; the list: 60 mouse-wheel events of 600 px each in about 2.4 seconds (≈ 15,000 px/s). After each event it checks whether the center of the viewport shows a row or an empty spacer.</p>
<ul>
<li><strong>Fixed heights:</strong> Moving from overscan 3 to 15 cut blank samples from <strong>55 to 3 out of 60</strong>, at the cost of 52 instead of 28 rendered rows.</li>
<li><strong>Variable heights:</strong> Overscan helps less here: 52, 39, and 32 blank samples for overscan 3, 15, and 30. Every window change must be measured and rendered, and the running average is still converging while you fling through unmeasured regions.</li>
<li><strong>Server traffic while scrolling</strong> was 0.6–1.2 MB per fling on Blazor Server. That's the render batches for each new window, and it grows with overscan and row complexity.</li>
</ul>
<p>Treat these numbers as a <em>relative</em> comparison, taken on one machine with the server on localhost. Over a real network, Blazor Server adds a round trip to every window change, which makes overscan more valuable. On WebAssembly there's no round trip, but rendering competes with everything else on the main thread.</p>
<p>The main message: <strong><code>Virtualize</code> limits how much UI you render. It doesn't make your data access faster.</strong> If your provider runs an expensive <code>COUNT(*)</code> on every request, slows down at deep offsets, returns far more columns than the row needs, or ignores the cancellation token, virtualization won't hide that.</p>
<h2>Choosing <code>ItemSize</code> and <code>OverscanCount</code></h2>
<p><strong><code>ItemSize</code></strong></p>
<ul>
<li>It's the starting estimate. <code>Virtualize</code> uses it before any measurements exist and for the first scroll range.</li>
<li>Set it close to your real average item height. Measure it in the browser, as the sample did for its grid (about 62 px). A value that's far off makes the first layout and scrollbar noticeably wrong until measurements arrive.</li>
<li>Variable-height support doesn't make it irrelevant. Changing <code>ItemSize</code> at runtime resets the measured average.</li>
</ul>
<p><strong><code>OverscanCount</code></strong></p>
<p>| Lower (e.g. 3) | Higher (e.g. 15–30) |
|---|---|
| Fewer DOM nodes and components | Fewer blank moments during fast scrolling |
| Smaller render batches and provider requests | More items to average variable heights over |
| More risk of placeholders or empty space when scrolling fast | More DOM, more component instances, bigger batches |</p>
<p>Start with the .NET 11 default of 15, then profile with your real row template on the slowest devices you support. Heavy rows, such as grids with many cells or rich cards, may justify going back down. <code>QuickGrid</code> stays at 3 for exactly that reason. <code>MaxItemCount</code> (default 100) still caps how many items are rendered, plus 2 × <code>OverscanCount</code>. Don't set it lower than what a tall window can show.</p>
<h2>Content Security Policy: why virtualization used to break</h2>
<p>A strict policy like the following blocks inline <code>style</code> attributes:</p>
<pre><code class="language-http">Content-Security-Policy: default-src 'self'; style-src 'self'
</code></pre>
<p>That applies both to attributes in the server-rendered markup and to attributes set with <code>element.setAttribute('style', …)</code>. Browsers report them as <strong><code>style-src-attr</code></strong> violations. Nonces don't help, because they never apply to attributes, and <code>'unsafe-hashes'</code> doesn't scale to values that change on every scroll.</p>
<p>Up to .NET 10, <code>Virtualize</code> rendered its spacers like this (captured from the sample's .NET 10.0.10 baseline):</p>
<pre><code class="language-html">&lt;div style=&quot;height: 0px; flex-shrink: 0;&quot; aria-hidden=&quot;true&quot;&gt;&lt;/div&gt;
&lt;div aria-hidden=&quot;true&quot; style=&quot;height: 9598560px; flex-shrink: 0;&quot;&gt;&lt;/div&gt;
</code></pre>
<p>With the same strict policy as the .NET 11 app, the baseline logged <strong>5 <code>style-src-attr</code> violations</strong>, with samples such as <code>height: 9600000px; flex-shrink: 0;</code>. They came from both the prerendered HTML and <code>blazor.web.js</code>. Worse, the list <strong>collapsed</strong>. The spacers had no height, so a 100,000-item list had a scroll range of <strong>1,227 px</strong>, and about 15 rows were all anyone could reach. Chrome, Firefox, and WebKit gave identical results.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/09-csp-side-by-side.png" alt="Side by side: the .NET 10 baseline with a red &quot;5 violations&quot; badge, and the .NET 11 feed with a green &quot;0 violations&quot; badge under the same policy" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/csp-net10-vs-net11.gif" alt="Animated: trying to scroll the .NET 10 list goes nowhere; under the same policy, the .NET 11 list keeps scrolling with 0 violations" /></p>
<h3>What .NET 11 does instead</h3>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-22-blazor-net11-virtualize-anchormode-csp/images/diagrams/csp-before-after.png" alt=".NET 10 renders a style attribute that CSP blocks; .NET 11 renders numbers in data attributes that a MutationObserver applies through CSSOM" /></p>
<p>In .NET 11, the C# side renders <strong>numbers</strong>, not styles:</p>
<pre><code class="language-html">&lt;!-- prerendered by .NET 11 RC1 (/lab/perf?mode=items: 100,000 items × ItemSize 64) --&gt;
&lt;div data-blazor-virtualize-reserved-height=&quot;0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/div&gt;
&lt;div aria-hidden=&quot;true&quot; data-blazor-virtualize-reserved-height=&quot;6400000&quot;&gt;&lt;/div&gt;
</code></pre>
<ul>
<li><code>data-blazor-virtualize-reserved-height</code> holds the height as a unitless number, for both spacers and default placeholders.</li>
<li><code>data-blazor-virtualize-loop-breaker-transform</code> holds a vertical offset for the trailing spacer, when one is needed.</li>
<li>A <code>MutationObserver</code> in <code>blazor.web.js</code> watches those two attributes on the spacers. It parses each value, accepts only finite numbers, and applies it through the CSS Object Model, with <code>element.style.setProperty('height', …)</code> or <code>translateY(…)</code>.</li>
</ul>
<p>Browsers don't apply <code>style-src</code> to properties set through an element's <code>style</code> object. MDN documents this, and the web-platform-tests confirm it for current Chrome, Firefox, and Safari. If you explain this to your security team, be precise: <strong>the CSP specification doesn't formally exempt CSSOM</strong>. It even describes gating some CSSOM setters on <code>'unsafe-eval'</code>. The guarantee is how browsers actually behave, and that's what the framework relies on.</p>
<blockquote>
<p><strong>Don't test for &quot;no <code>style</code> attribute in the DOM&quot;.</strong> After hydration, the .NET 11 spacers <em>do</em> show a <code>style</code> attribute in DevTools:</p>
<pre><code class="language-html">&lt;div aria-hidden=&quot;true&quot; data-blazor-virtualize-reserved-height=&quot;9596352&quot;
     style=&quot;flex-shrink: 0; height: 9.59635e+06px; overflow-anchor: none;&quot;&gt;&lt;/div&gt;
</code></pre>
<p>That's the serialization of the properties set through CSSOM, and it's allowed. The .NET 10 spacers show a <code>style</code> attribute too, but there it's inert, because the browser refused it. Instead, check for <strong>violations</strong>, and check that the <strong>computed height equals the reserved height</strong>.</p>
</blockquote>
<h3>Sending a strict CSP from a Blazor Web App</h3>
<p>Send the policy as an <strong>HTTP response header</strong>, not a <code>&lt;meta&gt;</code> tag. Only a header supports <code>frame-ancestors</code>, <code>report-to</code>, and <code>Content-Security-Policy-Report-Only</code>. The sample generates a nonce per request, because the Blazor Web App's <code>&lt;ImportMap /&gt;</code> is an inline <code>&lt;script&gt;</code>:</p>
<pre><code class="language-csharp">public static string Build(string nonce) =&gt; string.Join(&quot;; &quot;,
    &quot;default-src 'self'&quot;,
    &quot;base-uri 'self'&quot;,
    &quot;object-src 'none'&quot;,
    &quot;frame-ancestors 'none'&quot;,
    &quot;form-action 'self'&quot;,
    &quot;img-src 'self' data:&quot;,
    &quot;font-src 'self'&quot;,
    &quot;style-src 'self' 'report-sample'&quot;,
    $&quot;script-src 'self' 'nonce-{nonce}' 'report-sample'&quot;,
    &quot;connect-src 'self'&quot;);

public static IApplicationBuilder UseStrictContentSecurityPolicy(this IApplicationBuilder app, CspMode mode)
{
    // …
    return app.Use(async (context, next) =&gt;
    {
        // UseStatusCodePagesWithReExecute and UseExceptionHandler run the rest of the pipeline
        // again for the same HttpContext. Create the nonce and register the header callback once,
        // or the header and the re-rendered &lt;ImportMap /&gt; end up with different nonces.
        if (!context.Items.ContainsKey(NonceKey))
        {
            var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
            context.Items[NonceKey] = nonce;

            context.Response.OnStarting(() =&gt;
            {
                if (context.Response.ContentType?.StartsWith(&quot;text/html&quot;, StringComparison.OrdinalIgnoreCase) == true)
                {
                    context.Response.Headers[headerName] = Build(nonce);
                }

                return Task.CompletedTask;
            });
        }

        await next();
    });
}
</code></pre>
<pre><code class="language-razor">@* App.razor (abridged) *@
&lt;ImportMap nonce=&quot;@nonce&quot; /&gt;

@code {
    private string? nonce;

    [CascadingParameter]
    private HttpContext? HttpContext { get; set; }

    protected override void OnInitialized() =&gt; nonce = HttpContext.GetCspNonce();
}
</code></pre>
<p>The <code>ContainsKey</code> guard fixes a real bug that the article review caught in the first version of this middleware. Without it, a 404 page re-executed by <code>UseStatusCodePagesWithReExecute</code> got one nonce in the header and a different one in the import map, so the import map was blocked on error pages. The sample now has an integration test for exactly that case.</p>
<p>Notes on the directives:</p>
<ul>
<li><strong><code>'report-sample'</code></strong> doesn't relax anything. It only adds the first 40 characters of a blocked inline style or script to violation reports. That's how the .NET 10 samples above were captured.</li>
<li><strong><code>img-src 'self' data:</code></strong> is needed because the template's Bootstrap and navigation icons use <code>data:</code> SVGs in CSS.</li>
<li><strong><code>connect-src 'self'</code></strong> covers the Blazor Server WebSocket. Under CSP Level 3, <code>'self'</code> matches same-host <code>ws:</code>/<code>wss:</code>, and current browsers implement that. Very old browsers needed explicit <code>ws:</code>/<code>wss:</code> sources.</li>
<li><strong>The template's <code>&lt;ReconnectModal /&gt;</code></strong> is CSP-friendly. Apps created before .NET 10 that still use the built-in JS reconnect UI get an injected <code>&lt;style&gt;</code> element, which violates <code>style-src 'self'</code>.</li>
<li><strong>The .NET 11 template no longer puts an inline <code>onclick</code> on <code>NavMenu</code>.</strong> It uses a collocated <code>NavMenu.razor.js</code> module instead. (The .NET 10 template still has the inline handler; the sample's baseline removes it so that only <code>Virtualize</code> is compared.) In RC1, that module binds to the prerendered menu element, which interactive render modes replace, so the mobile menu stays open after navigation. RC2 fixes this (<a href="https://github.com/dotnet/aspnetcore/pull/68951">dotnet/aspnetcore#68951</a>, a backport of #68768). Because the sample is fully interactive, it keeps the menu's open/closed state in the component instead.</li>
<li><strong>Blazor WebAssembly</strong> also needs <code>'wasm-unsafe-eval'</code> in <code>script-src</code>. Microsoft's Blazor CSP docs give a starting policy for each render mode.</li>
<li><strong>Nonces must be unique per response.</strong> Don't let a CDN or output cache serve one nonce to many users.</li>
<li><strong>Roll out with <code>Content-Security-Policy-Report-Only</code> first.</strong> Add <code>report-to csp-endpoint</code> to the policy and declare the endpoint in a <code>Reporting-Endpoints: csp-endpoint=&quot;https://…/csp-reports&quot;</code> response header. Also add <code>report-uri https://…/csp-reports</code> for browsers that don't support <code>report-to</code> yet. Switch to enforcing once the reports are clean.</li>
</ul>
<h3>Security framing</h3>
<p>.NET 11 removes an important source of CSP incompatibility from <code>Virtualize</code>, but it doesn't make an application &quot;CSP-secure&quot;. Your complete policy still depends on your render mode, scripts, styles, third-party components, and hosting. CSP reduces the impact of XSS and clickjacking. It doesn't replace output encoding, CSRF protection, or authorization. <code>Virtualize</code> decides which records are <em>rendered</em>; your backend still has to decide which records a user may <em>see</em>.</p>
<h2>Testing strict CSP compatibility</h2>
<p>A test plan that catches real problems:</p>
<ol>
<li>Enable <code>style-src 'self'</code> (and a nonce-based <code>script-src</code>) as an HTTP header, in every environment you test in.</li>
<li>Don't add <code>'unsafe-inline'</code>.</li>
<li>Listen for <code>securitypolicyviolation</code> events from the very start of the page. The sample loads a tiny <code>csp-monitor.js</code> from <code>'self'</code> as the first script in <code>&lt;head&gt;</code>, so it also sees violations raised while the prerendered HTML is parsed.</li>
<li>Load every virtualized page, then scroll to the start, middle, and end.</li>
<li>Prepend and append items, and expand variable-height items.</li>
<li>Assert <strong>zero violations</strong>, and assert that each spacer's computed height matches its <code>data-blazor-virtualize-reserved-height</code>.</li>
<li>Include error pages (404, 500), which render through a re-executed pipeline.</li>
<li>Run the tests in Chromium, Firefox, and WebKit, and check real Safari/iOS manually.</li>
</ol>
<p>The sample's Playwright test is short:</p>
<pre><code class="language-js">test(`${path}: no CSP violations while loading and scrolling`, async ({ page }) =&gt; {
    await page.goto(APP + path);
    await waitForList(page, row);
    for (const f of [0.25, 0.75, 1, 0]) {
        await scrollViewportTo(page, f);
        await settle(page, 300);
    }

    expect(await cspViolations(page)).toEqual([]);
});
</code></pre>
<p>The integration tests also check the prerendered HTML without a browser, using <code>WebApplicationFactory</code>. They verify that the HTML of every page contains no <code>style=&quot;</code>, that the import map carries the response's nonce, and that a re-executed 404 page uses the same nonce in its header and its markup.</p>
<h2>Accessibility and UX</h2>
<ul>
<li><strong>Make the scroll container focusable.</strong> Since Chrome 132, Chromium makes a scroll container keyboard-focusable automatically only when it has <em>no</em> focusable children. Lists whose items contain buttons or links, like the feed's &quot;Show details&quot;, don't qualify. Give the container <code>tabindex=&quot;0&quot;</code> and an <code>aria-label</code>, as the sample does. If there's no <code>tabindex</code>, RC1 adds <code>tabindex=&quot;-1&quot;</code> so the container can receive <kbd>Home</kbd>/<kbd>End</kbd>, but that doesn't make it reachable with <kbd>Tab</kbd>.</li>
<li><strong>Don't force-scroll readers.</strong> <code>AnchorMode.End</code> already follows new items only at the live edge. Pair it with a visible &quot;N new messages&quot; control.</li>
<li><strong>Announce summaries, not every item.</strong> Use a single polite live region with an updating count (&quot;12 new messages below.&quot;) rather than announcing each message. A busy log would flood a screen reader.</li>
<li><strong>Keep placeholders close to real item heights,</strong> so loading states don't shift the layout.</li>
<li><strong>Use stable <code>@key</code>s</strong> (the item ID, never the loop index).</li>
<li><strong>Watch focus.</strong> If a focused row scrolls out of the window, it's removed from the DOM and focus is lost. Keep focus on the container, or scroll the focused item into view before changing data.</li>
<li><strong>Keep table semantics when you split the header.</strong> The sample's visible grid header is <code>aria-hidden</code>, and the body table keeps a visually hidden <code>&lt;thead&gt;</code>, so screen readers still announce the column names.</li>
<li><strong>Respect <code>prefers-reduced-motion</code></strong> in skeleton animations. The sample turns its shimmer off.</li>
</ul>
<h2>Browser and layout caveats</h2>
<ul>
<li><strong>The scroll container needs a real height</strong> and <code>overflow-y: auto</code> or <code>scroll</code>. In a flex layout, give the list <code>flex: 1; min-height: 0</code>. If a sibling (like a header) sits in the same flex column, give that sibling <code>flex: none</code>. The list's flex basis is its virtual height, which can be millions of pixels, so flexbox would otherwise shrink the sibling to nothing. That happened to the sample's grid header during development.</li>
<li><strong>Single column only.</strong> Variable-height support assumes a vertical stack. Items that share a row (<code>flex-wrap</code>, CSS grid with several columns, horizontal lists) aren't supported.</li>
<li><strong>Don't style the spacers.</strong> No borders, margins, or <code>::before</code>/<code>::after</code> content on them.</li>
<li><strong>Tables and <code>div</code>s behave differently.</strong> They use different anchoring paths (<a href="#32-hybrid-scroll-anchoring">section 3.2</a>), so test the layout you actually ship.</li>
<li><strong>Safari and iOS:</strong> Safari 26 and earlier don't support <code>overflow-anchor</code>, so they take the manual path even for <code>div</code> lists. Playwright's WebKit build doesn't exercise that path. Test on devices.</li>
<li><strong>Lazy-loaded media</strong> changes row heights after render. Reserve its space.</li>
<li><strong>Very uneven height distributions</strong> (a few rows 100× taller than the rest) make the running average less representative, so expect the scrollbar to be approximate.</li>
<li><strong>Nested scroll containers and document-level scrolling</strong> both work, but the scroll container is detected as the first scrollable ancestor. Check that it's the one you expect.</li>
<li><strong>WebAssembly.</strong> The same component and markup apply. Spacer styles are applied by a <code>MutationObserver</code> callback, which runs asynchronously in every render mode. On WebAssembly, though, the .NET side can refresh synchronously before that callback has run. That's why the RC2 End-anchoring fix (<a href="https://github.com/dotnet/aspnetcore/pull/69388">#69388</a>, a backport of #69310) also flushes pending spacer styles before reading <code>scrollHeight</code>. Your CSP also needs <code>'wasm-unsafe-eval'</code>.</li>
</ul>
<h2>Preview/RC status and production readiness</h2>
<blockquote>
<p><strong>Tested against:</strong> .NET SDK <code>11.0.100-rc.1.26425.128</code> · ASP.NET Core <code>11.0.0-rc.1.26425.128</code> · Microsoft.AspNetCore.Components.Web <code>11.0.0-rc.1.26425.128</code></p>
</blockquote>
<p><strong>Support facts</strong></p>
<ul>
<li>RC1 ships with a <strong>go-live license</strong>, which means Microsoft supports it in production. The go-live support window for RC1 ends <strong>October 13, 2026</strong>, and you're expected to move to RC2 and then GA.</li>
<li><strong>.NET 11 is an STS release</strong> with two years of support. The dotnet/core release notes currently give November 10, 2026 – November 9, 2028. So far, the support-policy page lists only the RC1 go-live release, not .NET 11's GA support dates.</li>
<li><strong>.NET 10 is LTS</strong> and is supported until <strong>November 14, 2028</strong>. That's the same month .NET 11 support ends, because STS releases are now supported for 24 months. Upgrading doesn't meaningfully change your support window (the published end dates are five days apart), so decide based on features and risk.</li>
</ul>
<p><strong>Known RC1 issues relevant to this article</strong> (as of September 21, 2026)</p>
<p>| Issue | Status |
|---|---|
| <code>AnchorMode.End</code> stays at the top after the initial <code>ItemsProvider</code> load (<a href="https://github.com/dotnet/aspnetcore/issues/69302">#69302</a>) | Fixed for RC2 (<a href="https://github.com/dotnet/aspnetcore/pull/69388">#69388</a>). The RC1 workaround is a small initial fetch followed by the regular one |
| <code>InitialItemIndex</code> positioning can oscillate or finish at the wrong item; in the sample, the next render moved rows by 53–82 px | Fixed for RC2 (<a href="https://github.com/dotnet/aspnetcore/pull/69170">#69170</a>). Observed in section 11 |
| False-positive prepend detection causes backward scroll jumps with <code>ItemsProvider</code> and ongoing updates (<a href="https://github.com/dotnet/aspnetcore/pull/69362">#69362</a>) | Open PR, RC2 milestone |
| User scrolling doesn't reliably cancel an in-flight <code>ScrollToItemAsync</code> (<a href="https://github.com/dotnet/aspnetcore/pull/69288">#69288</a>) | Open PR |
| QuickGrid: Start anchoring in Server modes, and duplicate provider requests (<a href="https://github.com/dotnet/aspnetcore/issues/69380">#69380</a>, <a href="https://github.com/dotnet/aspnetcore/issues/69381">#69381</a>) | Open |
| Deleting items above the viewport isn't anchored (<a href="https://github.com/dotnet/aspnetcore/issues/66509">#66509</a>) | Backlog |
| The template's mobile <code>NavMenu</code> stays open after navigation in interactive render modes (<a href="https://github.com/dotnet/aspnetcore/pull/68951">#68951</a>, a backport of #68768) | Fixed for RC2 |
| <code>&lt;table&gt;</code> rows that grow right after scrolling are only partly compensated: usually 41–42 px in Chrome and Firefox, sometimes ~220 px in Chrome | Observed in this sample (section 10) |
| <code>AnchorMode.End</code> sometimes stops 29–77 px short of the bottom after an append. WebKit: 20/30 checks passed; Chrome and Firefox: 1–2 misses in 30 | Observed in this sample (section 7). Playwright WebKit 26.6 on Windows, not verified in Safari |
| A <code>Start</code> list moves 214–486 px when items are appended at the very end; <code>None</code> doesn't | Observed in this sample (section 8). Playwright WebKit 26.6 on Windows only, not verified in Safari |</p>
<p><strong>Docs inconsistencies to be aware of in RC1</strong></p>
<ul>
<li>The docs and release notes use <code>AnchorMode=&quot;Start&quot;</code>, which doesn't compile in Razor. Use <code>VirtualizeAnchorMode.Start</code>.</li>
<li>The &quot;Advanced styles&quot; section of the virtualization docs still says all items must have identical height. That text predates variable-height support.</li>
<li>The .NET 10 version of the virtualization docs suggests using <code>data-blazor-virtualize-reserved-height</code> for CSP compliance. .NET 10 doesn't read that attribute: the sample's .NET 10.0.10 baseline renders and needs inline styles.</li>
</ul>
<h2>Build and verification results</h2>
<p><strong>Test environment</strong></p>
<p>| | |
|---|---|
| .NET SDK | <code>11.0.100-rc.1.26425.128</code> (installed side by side with <code>dotnet-install.ps1</code> in <code>~/.dotnet11</code>) |
| Runtime | .NET <code>11.0.0-rc.1.26425.128</code>; baseline: .NET <code>10.0.10</code> (SDK <code>10.0.301</code>) |
| App model | Blazor Web App, Interactive Server, prerendering on |
| Builds | Debug (<code>dotnet run</code>) for tests and probes; published Release build in the Production environment for the perf numbers |
| OS / hardware | Windows 11 Pro (build 26200), AMD Ryzen 9 5900HX (8 cores / 16 threads), 32 GB RAM |
| Browsers | Google Chrome 153.0.8010.50, Playwright Firefox, Playwright WebKit 26.6 (Playwright 1.63.0) |
| Dataset | 100,000 generated records (deterministic seed) |</p>
<p><strong>How it was verified</strong></p>
<p>The sample was checked at three levels:</p>
<ul>
<li><code>dotnet build</code> for the app and the test project.</li>
<li>xUnit tests (<code>dotnet test</code>), including <code>WebApplicationFactory</code> integration tests that inspect the prerendered HTML and the CSP headers.</li>
<li>Playwright browser tests in Chrome, Firefox, and WebKit against the running .NET 11 app and the .NET 10 baseline. The baseline ran on the .NET 10 SDK.</li>
</ul>
<p><strong>Results</strong></p>
<p>| Check | Result |
|---|---|
| <code>dotnet build</code> (Debug and Release) | 0 warnings, 0 errors |
| <code>dotnet test</code> | <strong>17/17 passed</strong>, 3 consecutive runs |
| <code>npx playwright test</code> (Chrome, Firefox, WebKit) | <strong>72 tests: 65 passed, 0 failed, 0 flaky, 7 skipped.</strong> The skipped tests are <code>fixme</code> markers for the RC1 issues below: the table gap and the <code>InitialItemIndex</code> shift in each engine, plus <code>AnchorMode.End</code> without the guard in WebKit |</p>
<p>| Test | Expected result | Actual result |
|---|---|---|
| Initial activity feed render | Only viewport items are rendered | <strong>Pass</strong>: 37–38 of 100,000 cards in the DOM (all engines) |
| Prepend at the top | The viewport stays pinned to the start and shows the new items | <strong>Pass</strong> (all engines) |
| Prepend while reading older items | Visible content doesn't move; a pill appears | <strong>Pass</strong>: ≤ 1 px (all engines) |
| Append at the chat bottom | The new message is followed | <strong>Pass</strong> with the sample's guard (30/30 follow checks per engine). <code>AnchorMode.End</code> alone: Chrome 29/30, Firefox 28/30, WebKit 20/30 |
| Append while reading history | The user isn't pulled to the bottom | <strong>Pass</strong> (all engines) |
| Expand a variable-height item above the viewport (<code>div</code> list) | No disruptive jump | <strong>Pass</strong>: 0 px (18/18 wheel runs) |
| Expand a row above the viewport (<code>&lt;table&gt;</code>) | No disruptive jump | <strong>Partial</strong>: ≤ 1 px after an ordinary render. Right after scrolling: usually 41–42 px in Chrome and Firefox (occasionally ~220 px in Chrome), and ≤ 1 px in 11 of 12 WebKit runs |
| Open at a row / jump to a row | The target row is at the top | <strong>Pass</strong> for both. RC1: after <code>InitialItemIndex</code>, the next render moves the rows by 53–82 px |
| Append at the end of a <code>Start</code> list (infinite scroll) | No movement | <strong>Pass</strong> in Chrome and Firefox; <strong>fail</strong> in Playwright WebKit (214–486 px). With <code>None</code>: pass everywhere |
| Strict CSP (<code>style-src 'self'</code>) | No virtualization style violations | <strong>Pass</strong>: 0 violations, 5 pages × 3 engines; error pages keep the header and markup nonces in sync |
| Same CSP on .NET 10.0.10 | (baseline for comparison) | 5 <code>style-src-attr</code> violations; the list collapsed to 1,227 px (all engines) |
| Fast scrolling with latency | Stale provider calls are canceled | <strong>Pass</strong>: canceled calls &gt; 0 (all engines) |
| Performance, 100,000 items | <code>Virtualize</code> ≪ <code>foreach</code> | 125 ms vs 112.4 s to interactive; 256 vs 400,078 DOM elements (<a href="#performance-what-i-measured">section 12</a>) |</p>
<h2>When not to use <code>Virtualize</code></h2>
<p>Virtualization is a trade-off. It's not the right choice when:</p>
<ul>
<li>The list has only 20–50 simple rows. The overhead isn't worth it.</li>
<li>The full content must be in the initial HTML for SEO or no-JS clients. <code>Virtualize</code> prerenders at most a small window of items, and usually none.</li>
<li>Users need the browser's find-in-page (<kbd>Ctrl</kbd>+<kbd>F</kbd>) across all rows. Unrendered rows can't be found.</li>
<li>All rows must print or export from the page. Do that on the server instead.</li>
<li>The layout isn't a single vertical stack (masonry, multi-column wraps, horizontal carousels).</li>
<li>Classic pagination gives users a clearer mental model, as with &quot;page 3 of the invoices&quot;.</li>
</ul>
<p>Virtualization and pagination can also work together. Large data grids often combine server-side paging, filtering, and sorting with viewport virtualization inside the current result set.</p>
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11">What's new in ASP.NET Core in .NET 11</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/virtualization?view=aspnetcore-11.0">ASP.NET Core Razor component virtualization</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/security/content-security-policy?view=aspnetcore-11.0">Enforce a Content Security Policy for ASP.NET Core Blazor</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/security/">ASP.NET Core Blazor security overview</a></li>
<li><a href="https://dotnet.microsoft.com/en-us/download/dotnet/11.0">.NET 11 downloads</a> and the <a href="https://dotnet.microsoft.com/platform/support/policy/dotnet-core">.NET support policy</a></li>
<li>dotnet/aspnetcore pull requests: <a href="https://github.com/dotnet/aspnetcore/pull/64964">#64964</a> (variable heights), <a href="https://github.com/dotnet/aspnetcore/pull/65951">#65951</a> (hybrid anchoring), <a href="https://github.com/dotnet/aspnetcore/pull/66262">#66262</a> (AnchorMode), <a href="https://github.com/dotnet/aspnetcore/pull/66680">#66680</a> (CSP), <a href="https://github.com/dotnet/aspnetcore/pull/66753">#66753</a> (scroll to item), <a href="https://github.com/dotnet/aspnetcore/pull/67934">#67934</a> (anchoring regression fix)</li>
<li><a href="https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/style-src">MDN: CSP <code>style-src</code></a>, <a href="https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/style-src-attr"><code>style-src-attr</code></a>, and <a href="https://developer.mozilla.org/docs/Web/CSS/overflow-anchor"><code>overflow-anchor</code></a></li>
<li><a href="https://www.w3.org/TR/CSP3/">W3C Content Security Policy Level 3</a></li>
<li><a href="https://developer.chrome.com/blog/keyboard-focusable-scrollers">Chrome for Developers: Keyboard-focusable scrollers</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a23df2b-d1e0-7b78-c0a2-8ffc63d5dcf0" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a23df2b-d1e0-7b78-c0a2-8ffc63d5dcf0" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/the-asp.net-core-dependency-injection-system-3vbsdhq8</guid>
      <link>https://abp.io/community/posts/the-asp.net-core-dependency-injection-system-3vbsdhq8</link>
      <a10:author>
        <a10:name>oguzhan.agir</a10:name>
        <a10:uri>https://abp.io/community/members/oguzhan.agir</a10:uri>
      </a10:author>
      <category>dependency-injection</category>
      <category>best-practices</category>
      <category>aspnet-core</category>
      <category>aot</category>
      <category>dotnet-9.0</category>
      <title>The ASP.NET Core Dependency Injection System</title>
      <description>This article explores Dependency Injection (DI) in ASP.NET Core and its built-in IoC container. It explains the differences between transient, scoped, and singleton service lifecycles.

It covers best practices like manual scope management with `IServiceScopeFactory`, resource cleanup using `IAsyncDisposable`, and avoiding captive dependencies. The guide also touches on topics like keyed services and .NET 9's source generators for AOT support.</description>
      <pubDate>Tue, 21 Oct 2025 13:24:23 Z</pubDate>
      <a10:updated>2026-10-14T12:43:02Z</a10:updated>
      <content:encoded><![CDATA[<h1>The ASP.NET Core Dependency Injection System</h1>
<h2>Article Overview</h2>
<p>This article provides a guide to <strong>ASP.NET Core Dependency Injection</strong>, a fundamental element of .NET development. We'll examine the built in Inversion of Control (IoC) container, examining the critical differences between service lifecycles (scoped, singleton, or transient), and comparing constructor injection to property injection.</p>
<p>You'll learn how to effectively register your services with patterns like <code>TryAdd</code> methods and the <strong>Options Pattern</strong>, how to adhere to established best practices like avoiding captive dependencies and asynchronous constructor logic, how to understand patterns like decorators and explicit generics, how to leverage manual scope management with <code>IServiceScopeFactory</code>, how to implement proper asynchronous disposal with <code>IAsyncDisposable</code>, and how to analyze the performance impact of your DI strategy, including the new compile time source generation features that enable native AOT support in .NET 9. Ultimately, you'll have the knowledge to create loosely coupled, maintainable, and testable applications using the advanced dependency injection patterns in .NET 9. Of course, the explanations here are general, if you'd like to delve deeper, you can check out the references section.</p>
<h2>Introduction to Dependency Injection</h2>
<p>Dependency Injection (DI) is a design pattern used to implement Inversion of Control (IoC), in which control of object creation and binding is transferred from the object itself to another container or framework. In the context of ASP.NET Core, DI is a tool integrated into the framework for managing the lifecycle and creation of application components.</p>
<h3>The Evolution from .NET Framework to .NET Core and .NET 9</h3>
<p>The journey of dependency injection in the .NET ecosystem represents a sweeping architectural shift. .NET Framework applications (prior to 2016) typically relied on third party IoC containers such as</p>
<ul>
<li><strong>Unity</strong> (Microsoft's own container, often used in enterprise applications)</li>
<li><strong>Autofac</strong> (popular for its advanced features and fluent API)</li>
<li><strong>Ninject</strong> (known for its simplicity)</li>
<li><strong>StructureMap</strong> (one of the earliest .NET IoC containers)</li>
<li><strong>Castle Windsor</strong> (powerful but complex)</li>
</ul>
<p>Many legacy applications have fallen back to anti patterns like the <strong>Service Locator pattern</strong>, which hides dependencies and makes testing difficult.</p>
<p>When ASP.NET Core was released in 2016, Microsoft made a decision to integrate dependency injection directly into the runtime. This meant:</p>
<ul>
<li><strong>Standardization:</strong> Creating a consistent DI approach across all .NET Core applications.</li>
<li><strong>Performance:</strong> Creating a lightweight, optimized container designed for workloads.</li>
<li><strong>Simplicity:</strong> No need to choose third party containers for basic scenarios.</li>
<li><strong>Cloud Native Ready:</strong> Designed for microservices, containers, and serverless architectures.</li>
</ul>
<p>Now, with <strong>.NET 9</strong>, the DI container has become even more advanced as follows:</p>
<ul>
<li><strong>Source generated DI</strong> for faster startup and AOT compatibility.</li>
<li><strong>Keyed services</strong> for advanced solution scenarios.</li>
<li><strong>Lifetime validation</strong> to catch common errors during development.</li>
</ul>
<p>Unlike .NET Framework applications, which required installing these containers as third-party packages and often present issues with consistency, in ASP.NET Core, and now in .NET 9, dependency injection is a fundamental part of the architecture.</p>
<h3>Why is Dependency Injection important?</h3>
<p>The key benefits of adopting a DI strategy would be.</p>
<ul>
<li><strong>Loose Coupling:</strong> Components don't create their dependencies directly. Instead, they get them from the DI container. This means you can change the implementation of a dependency without changing the component that uses it.</li>
<li><strong>Testability:</strong> Once dependencies are added, you can easily replace them with mocks or mock implementations in your unit tests. This will allow you to test components in isolation.</li>
<li><strong>Maintenance and Scalability:</strong> A loosely coupled architecture is easier to manage, refactor, and extend. New features can be added with minimal changes to existing code.</li>
</ul>
<p>This article provides a guide for developers covering the basic mechanisms of Dependency Injection in .NET and the performance models available in .NET 9.</p>
<h2>The Built in IoC Container in ASP.NET Core</h2>
<p>ASP.NET Core ships with the lightweight yet comprehensive <strong>ASP.NET Core IoC container</strong>. It's not designed to have all the features of third party tools, but it provides the basic functionality needed for most applications.</p>
<p>The two basic interfaces representing the structure are as follows:</p>
<ul>
<li><code>IServiceCollection</code>: This is the &quot;registration&quot; side of the structure. When the application starts up, you add your services or dependencies to this collection.</li>
<li><code>IServiceProvider</code>: This is the &quot;resolving&quot; side of the structure. After the application is created, <code>IServiceProvider</code> is used to retrieve instances of registered services.</li>
</ul>
<h3>Comparison with Third Party Tools</h3>
<p>While the internal structure is sufficient for many scenarios, you can easily modify it if you need more features, such as:</p>
<ul>
<li><strong>Automatic registration / Assembly Scan:</strong> Automatically register types based on contracts.</li>
<li><strong>Interception / Decorators:</strong> Providing more support for packaging services with cross cutting concerns.</li>
<li><strong>Child Containers:</strong> Some tools, such as Autofac, allow you to create nested sub containers with their own lifetimes, which can be useful for isolating components in complex applications. The built in framework uses a simpler scoping mechanism.</li>
</ul>
<h2>Service Lifetimes in ASP.NET Core</h2>
<p>When registering a service, you must specify its lifetime. The lifetime determines how long a service instance will be valid. Understanding the difference between <strong>scoped, singleton, and transient</strong> is important for building stable and optimized applications.</p>
<h3>Transient</h3>
<p>A new instance of a transient service is created <strong>each time</strong> it is requested from the container.</p>
<ul>
<li><strong>When to use:</strong> For lightweight, stateless services.</li>
<li><code>builder.Services.AddTransient&lt;IMessageSender, EmailSender&gt;();</code></li>
</ul>
<h3>Scoped</h3>
<p>A single instance of a scoped service is created once per client request (or per scope). The same instance is shared within that single request.</p>
<ul>
<li><strong>When to use:</strong> It is more appropriate to use it for services that need to maintain state within a single request, such as <code>DbContext</code> or Unit of Work.</li>
<li><code>builder.Services.AddScoped&lt;IUserRepository, UserRepository&gt;();</code></li>
</ul>
<h3>Singleton</h3>
<p>A single instance of the service is created once during the entire application lifetime.</p>
<ul>
<li><strong>When to use:</strong> Commonly used for stateless services that are source intensive to create or need to share their state extensively, such as application configuration or caching services.</li>
<li><code>builder.Services.AddSingleton&lt;IAppCache, MemoryCacheService&gt;();</code></li>
</ul>
<blockquote>
<p><strong>Considered Best Practice: Avoid Captive Dependencies</strong>
A common mistake is injecting a deep, scoped service (for example, <code>MyDbContext</code>) into a singleton service. Because the singleton service lives forever, it will keep the scoped service in the container structure for the lifetime of the application, converting it to a singleton service. This can lead to memory leaks and erratic behavior across requests. ASP.NET Core throws an exception at runtime to help you detect this during development.</p>
</blockquote>
<h3>Manual Scope Management with <code>IServiceScopeFactory</code></h3>
<p>In scenarios where you need to manually create and manage scopes (background workers, singleton services, or long running tasks), you can use <code>IServiceScopeFactory</code> to create scopes.</p>
<p>This would be particularly useful when properly controlling when a singleton service needs to use scoped dependencies without causing captive dependency problems.</p>
<p>Within continuously running services, you must not directly inject objects that require short lifespans, such as database connections. This code example solves this problem by creating a temporary workspace for each task using a &quot;throw away&quot; approach. The environment and necessary services are created when the process starts, and all are automatically cleaned up when the task ends. This method utilizes sources efficiently and prevents memory leaks.</p>
<pre><code class="language-csharp">public class DataProcessingService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public DataProcessingService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task ProcessDataAsync()
    {
        // Create a new scope for this unit of work
        await using (var scope = _scopeFactory.CreateAsyncScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService&lt;AppDbContext&gt;();
            var repository = scope.ServiceProvider.GetRequiredService&lt;IDataRepository&gt;();
            
            // Perform scoped work
            var data = await repository.GetPendingDataAsync();
            await dbContext.SaveChangesAsync();
        }
        // Scope is disposed here, releasing all scoped services
    }
}
</code></pre>
<p><strong>Key Points:</strong></p>
<ul>
<li>We should use <code>CreateAsyncScope()</code> when working with asynchronous disposal.</li>
<li>It would be logical to use <code>CreateScope()</code> for synchronous scenarios.</li>
<li>Always destroying scopes appropriately using <code>using</code> or <code>await using</code> statements is important for scope management and optimization.</li>
</ul>
<p>Here is the detailed explanation, incorporating your text and adding the requested details for property injection.</p>
<h2>Constructor Injection vs. Property Injection</h2>
<p>There are several ways a class can receive its dependencies. The two most common patterns are <strong>constructor</strong> and <strong>property</strong> injection.</p>
<h3>Constructor Injection</h3>
<p>With constructor injection, a class retrieves its dependencies from the container via constructor parameters. With dependency injection (DI), the container will be responsible for creating instances of these dependencies and fetching them when the class is generated. This is one of the most common and recommended approaches to <strong>ASP.NET Core Dependency Injection</strong>.</p>
<pre><code class="language-csharp">// Primary Constructors
public class OrderService(IOrderRepository orderRepository, ILogger&lt;OrderService&gt; logger)
{
    private readonly IOrderRepository _orderRepository = orderRepository;

    private readonly ILogger&lt;OrderService&gt; _logger = logger;
    public async Task&lt;Order&gt; GetOrderAsync(int orderId)
    {
        _logger.LogInformation(&quot;Fetching order {OrderId}&quot;, orderId);
        return await _orderRepository.GetByIdAsync(orderId);
    }
}

// Traditional Class Constructor
public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly ILogger&lt;OrderService&gt; _logger;

    public OrderService(IOrderRepository orderRepository, ILogger&lt;OrderService&gt; logger)
    {
        _orderRepository = orderRepository;
        _logger = logger;
    }

    public async Task&lt;Order&gt; GetOrderAsync(int orderId)
    {
        _logger.LogInformation(&quot;Fetching order {OrderId}&quot;, orderId);
        return await _orderRepository.GetByIdAsync(orderId);
    }
}
</code></pre>
<h4>Pros:</h4>
<ul>
<li><strong>Explicit Dependencies:</strong> The constructor's signature explicitly states all <strong>required</strong> dependencies. A developer will immediately see what the class needs to function when calling it.</li>
<li><strong>Immutability:</strong> Dependencies can be assigned to <code>readonly</code> fields so that they cannot be changed after the object is created. This will lead to more stable and predictable class behavior.</li>
<li><strong>Availability:</strong> The class is guaranteed to have the required dependencies when created. It will not need to perform null checks on required services.</li>
<li><strong>Startup Validation:</strong> If a required dependency is not registered in the DI container, the application will fail <em>on startup</em> (at runtime), making errors easy to detect early.</li>
</ul>
<blockquote>
<p><strong>Best Practice: Avoid Asynchronous Operations in Constructors</strong>
A constructor is expected to be simple and fast. We should not perform asynchronous operations (<code>await</code>) or long running tasks within a constructor. This can lead to deadlocks and unpredictable application startup behavior. Using asynchronous factory patterns or <code>IHostedService</code> for asynchronous startup logic will fix this issue in most scenarios.</p>
</blockquote>
<h3>Property Injection</h3>
<p>With property injection (also known as &quot;setter injection&quot;), dependencies are provided through publicly settable properties on the class. The dependency is injected after the class is created.</p>
<p>This pattern is less common in ASP.NET Core because the built in DI container will not support it out of the box (other third party containers like Autofac or Ninject do support it).</p>
<p>Property injection is almost exclusively used for <strong>optional dependencies</strong>, which would be services that the class could use but doesn't need to perform its core functionality.</p>
<pre><code class="language-csharp">public class ProductService
{
    private readonly IProductRepository _productRepository;

    // Injected via a public property
    public ILogger&lt;ProductService&gt;? Logger { get; set; }

    // Still injected via the constructor
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository ?? throw new ArgumentNullException(nameof(productRepository));
    }

    public async Task&lt;Product&gt; GetProductAsync(int productId)
    {
        // Must check if the optional dependency was injected before using it
        Logger?.LogInformation(&quot;Fetching product {ProductId}&quot;, productId);
        
        return await _productRepository.GetByIdAsync(productId);
    }
}
</code></pre>
<p>Since the built in container doesn't automatically set the <code>Logger</code> property, you would either have to use a different container or set it manually (which partially defeats the purpose of DI). This is why it's strongly discouraged for <em>required</em> dependencies.</p>
<h4>Pros:</h4>
<ul>
<li><strong>Optional Dependencies:</strong> Can be used to provide optional services. The class can function without the dependency, but if a dependency is provided, its behavior needs to be improved.</li>
<li><strong>Decoupling:</strong> It can help to break up large classes or prevent over injection in the constructor (constructors with too many parameters), but this usually indicates that the class is doing too much (Single Responsibility Principle).</li>
</ul>
<h4>Cons:</h4>
<ul>
<li><strong>Hidden Dependencies:</strong> It won't be immediately obvious from the constructor what the class might depend on. Because of the way it's implemented, this means a developer will need to examine the class's properties.</li>
<li><strong>Mutability:</strong> This means that the dependency will not be readonly and can be changed at any time, which may lead to unforeseen situations and changes.</li>
<li><strong>Null Check:</strong> The class should always check if the optional dependency is <code>null</code> before using it.</li>
<li><strong>No Container Support:</strong> The default ASP.NET Core container will not inject properties. This makes this pattern unusable unless you use a different container or manually add dependencies.</li>
</ul>
<h2>Registering Services in ASP.NET Core</h2>
<p>Services are registered in the DI container in <code>Program.cs</code>. This means adding the services to the <code>IServiceCollection</code>.</p>
<h3>Basic and Factory based Registrations</h3>
<p>You can add an interface to a concrete class or use a factory based registration style for complex initialization.</p>
<p><strong>In Program.cs</strong></p>
<pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

// Simple registration
builder.Services.AddScoped&lt;ISimpleService, SimpleService&gt;();

// Factory based registration
builder.Services.AddScoped&lt;ISomeComplexService&gt;(provider =&gt;
{
    // Resolve other service
    var logger = provider.GetRequiredService&lt;ILogger&lt;SomeComplexService&gt;&gt;();
    var someValue = &quot;CalculatedOrRetrievedValue&quot;;
    
    // Manually build with dependencies
    return new SomeComplexService(logger, someValue);
});
</code></pre>
<h3>Conditional <code>TryAdd</code> Registrations</h3>
<p>When developing reusable libraries or building dependent applications, you may want to register a service only if another application has not already registered it, and you may want to check for it. The <code>TryAdd</code> method is used for this scenario.</p>
<p><strong>Available Methods:</strong></p>
<ul>
<li><code>TryAddSingleton&lt;TService, TImplementation&gt;()</code> Adds the singleton if it is not already registered</li>
<li><code>TryAddScoped&lt;TService, TImplementation&gt;()</code> Adds scoped if not already registered.</li>
<li><code>TryAddTransient&lt;TService, TImplementation&gt;()</code> Adds a transient if not already registered.</li>
<li><code>TryAddEnumerable()</code> Adds to a service collection (for <code>IEnumerable&lt;T&gt;</code> resolution).</li>
</ul>
<pre><code class="language-csharp">builder.Services.AddSingleton&lt;ILogger, CustomLogger&gt;();

builder.Services.TryAddSingleton&lt;ILogger, DefaultLogger&gt;();

// CustomLogger is used because it was registered first
// TryAdd* only adds if the service type isn't already registered
</code></pre>
<h3>The Options Pattern (<code>IOptions&lt;T&gt;</code>, <code>IOptionsSnapshot&lt;T&gt;</code>, <code>IOptionsMonitor&lt;T&gt;</code>)</h3>
<p>The <strong>Options Pattern</strong> is the recommended way to add configuration to your services. It provides type safe access to configuration sections and integrates with DI.</p>
<p><strong>Three different options:</strong></p>
<ol>
<li><strong><code>IOptions&lt;T&gt;</code></strong> Singleton, will be loaded once at startup.</li>
<li><strong><code>IOptionsSnapshot&lt;T&gt;</code></strong> Scoped, will be reloaded per request. (useful for multi tenant scenarios.)</li>
<li><strong><code>IOptionsMonitor&lt;T&gt;</code></strong> Individually triggered changes will be reloaded when the configuration changes.</li>
</ol>
<pre><code class="language-csharp">public class ApiSettings
{
    public string BaseUrl { get; set; } = string.Empty;
    public string ApiKey { get; set; } = string.Empty;
    public int TimeoutSeconds { get; set; } = 30;
}
</code></pre>
<p><strong>In appsettings.json</strong></p>
<pre><code class="language-json">{
  &quot;ExternalApi&quot;: {
    &quot;BaseUrl&quot;: &quot;https://api.example.com&quot;,
    &quot;ApiKey&quot;: &quot;your-api-key&quot;,
    &quot;TimeoutSeconds&quot;: 60
  }
}
</code></pre>
<p><strong>Inject and use in a service</strong></p>
<pre><code class="language-csharp">public class ExternalApiClient
{
    private readonly ApiSettings _settings;
    private readonly ILogger&lt;ExternalApiClient&gt; _logger;
    private readonly HttpClient _httpClient;

    // Use IOptions&lt;T&gt; for singleton services
    public ExternalApiClient(
        IOptions&lt;ApiSettings&gt; options, 
        ILogger&lt;ExternalApiClient&gt; logger,
        HttpClient httpClient)
    {
        _settings = options.Value;
        _logger = logger;
        _httpClient = httpClient;

        _httpClient.BaseAddress = new Uri(_settings.BaseUrl);
        _httpClient.DefaultRequestHeaders.Add(&quot;X-API-Key&quot;, _settings.ApiKey);
        _httpClient.Timeout = TimeSpan.FromSeconds(_settings.TimeoutSeconds);
    }

    public async Task&lt;string&gt; FetchDataAsync()
    {
        return await _httpClient.GetStringAsync(&quot;/data&quot;);
    }
}

public class DynamicConfigService
{
    private readonly IOptionsMonitor&lt;ApiSettings&gt; _optionsMonitor;

    public DynamicConfigService(IOptionsMonitor&lt;ApiSettings&gt; optionsMonitor)
    {
        _optionsMonitor = optionsMonitor;
        
        _optionsMonitor.OnChange(settings =&gt;
        {
            Console.WriteLine($&quot;Configuration changed! New URL: {settings.BaseUrl}&quot;);
        });
    }

    public ApiSettings GetCurrentSettings() =&gt; _optionsMonitor.CurrentValue;
}
</code></pre>
<p><strong>In Program.cs</strong></p>
<pre><code class="language-csharp">
builder.Services.Configure&lt;ApiSettings&gt;(builder.Configuration.GetSection(&quot;ExternalApi&quot;));
builder.Services.AddHttpClient&lt;ExternalApiClient&gt;();
</code></pre>
<p><strong>When to use each:</strong></p>
<ul>
<li><strong><code>IOptions&lt;T&gt;</code>:</strong> Used for settings that do not change during runtime.</li>
<li><strong><code>IOptionsSnapshot&lt;T&gt;</code>:</strong> Used in scoped services where the configuration may differ per request.</li>
<li><strong><code>IOptionsMonitor&lt;T&gt;</code>:</strong> Used when you need to react to configuration changes without restarting the application.</li>
</ul>
<h3>Automating Registration with Assembly Scanning</h3>
<p>In large projects, manually registering each service can be time consuming and error prone. While the built in container doesn't offer native assembly scanning, you can use reflection based utilities or third party libraries like <strong>Scrutor</strong> to automate service registration based on conventions.</p>
<p><strong>Example: Using Scrutor</strong></p>
<pre><code class="language-csharp">using Scrutor;

var builder = WebApplication.CreateBuilder(args);

// It will scan the services according to the contract and automatically register them.
builder.Services.Scan(scan =&gt; scan
    .FromAssemblyOf&lt;Program&gt;()                    // Scan the current assembly
    .AddClasses(classes =&gt; classes.Where(type =&gt; 
        type.Name.EndsWith(&quot;Service&quot;)))           // Find all classes ending with &quot;Service&quot;
    .AsImplementedInterfaces()                    // Register them by their interfaces
    .WithScopedLifetime());                       // Use scoped lifetime

// More specific scanning
builder.Services.Scan(scan =&gt; scan
    .FromAssemblies(typeof(IRepository&lt;&gt;).Assembly)
    .AddClasses(classes =&gt; classes.AssignableTo(typeof(IRepository&lt;&gt;)))
    .AsImplementedInterfaces()
    .WithTransientLifetime());
</code></pre>
<p><strong>Example: Custom reflection based scanning (without external library).</strong></p>
<pre><code class="language-csharp">namespace MyApp.Services;

using System.Reflection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(this IServiceCollection services)
    {
        var assembly = Assembly.GetExecutingAssembly();
        
        // Find all classes implementing IService marker interface
        var serviceTypes = assembly.GetTypes()
            .Where(t =&gt; t is { IsClass: true, IsAbstract: false } 
                     &amp;&amp; t.GetInterfaces().Any(i =&gt; i.Name == &quot;IService&quot;));

        foreach (var serviceType in serviceTypes)
        {
            var interfaceType = serviceType.GetInterfaces()
                .FirstOrDefault(i =&gt; i.Name == $&quot;I{serviceType.Name}&quot;);
            
            if (interfaceType != null)
            {
                services.AddScoped(interfaceType, serviceType);
            }
        }

        return services;
    }
}

// Usage in Program.cs
builder.Services.AddApplicationServices();
</code></pre>
<p><strong>Best Practices:</strong></p>
<ul>
<li>Using assembly scanning will be easier in large projects where many services are used in accordance with the rules.</li>
<li>Your naming conventions should be clearly documented (for example, all classes ending in &quot;Service&quot; are automatically registered).</li>
<li>In performance critical scenarios, the assembly scan should be performed carefully to avoid potential problems, as this may result in startup costs.</li>
<li>For true reflection overhead in .NET 9, consider using source generators.</li>
</ul>
<h2>Best Practices and Common Mistakes</h2>
<h3>Design for Explicit Dependencies</h3>
<p>This principle states that parent modules should not depend directly on lower level modules (such as services for data access, sending email, or specific API clients). Instead, both should depend on abstractions (interfaces).</p>
<p>This reverses the normal flow of dependencies, decoupling your code and making it more flexible and testable.</p>
<h4>Example</h4>
<h5>Bad: Violates DIP (Tight Coupling)</h5>
<p>Here, the top level <code>NotificationService</code> depends directly on the low level, concrete <code>EmailSender</code> class.</p>
<pre><code class="language-csharp">// Low level service
public class EmailSender
{
    public void SendEmail(string message)
    {
        Console.WriteLine($&quot;Sending email: {message}&quot;);
    }
}

// High level service
public class NotificationService
{
    // A direct dependency on a CONCRETE class
    private readonly EmailSender _emailSender;

    public NotificationService()
    {
        // The top level class is responsible for creating its own dependencies.
        _emailSender = new EmailSender(); 
    }

    public void NotifyUser(string message)
    {
        _emailSender.SendEmail(message);
    }
}
</code></pre>
<p><strong>Problems:</strong></p>
<ol>
<li><strong>Difficult to Test:</strong> You will not be able to test <code>NotificationService</code> without sending an email.</li>
<li><strong>Not Flexible:</strong> But what if you want to send an SMS instead? You will need to modify the <code>NotificationService</code> class.</li>
</ol>
<h5>Good: Following DIP (Loose Coupling)</h5>
<p>Here both classes depend on the <code>IMessageSender</code> interface.</p>
<pre><code class="language-csharp">// The Abstraction (Interface)
public interface IMessageSender
{
    void Send(string message);
}

// Low level service (depends on the abstraction)
public class EmailSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine($&quot;Sending email: {message}&quot;);
    }
}

// Another low level service
public class SmsSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine($&quot;Sending SMS: {message}&quot;);
    }
}

// High level service (also depends on the abstraction)
public class NotificationService
{
    // Dependency is on the Interface, not a concrete class
    private readonly IMessageSender _messageSender;

    // The dependency is injected via the constructor
    public NotificationService(IMessageSender messageSender)
    {
        _messageSender = messageSender;
    }

    public void NotifyUser(string message)
    {
        _messageSender.Send(message);
    }
}
</code></pre>
<p><strong>Benefits:</strong></p>
<ul>
<li><strong>Flexible:</strong> <code>NotificationService</code> doesn't care whether it is <code>EmailSender</code> or <code>SmsSender</code>. The DI container can be configured to provide both because it is dependent on an interface.</li>
<li><strong>Testable:</strong> You can create a <code>MockMessageSender</code> class that implements <code>IMessageSender</code> to use in your unit tests without sending a real message and perform your operations without needing any real information.</li>
</ul>
<h3>Service Disposal and <code>IAsyncDisposable</code></h3>
<p>If your service contains disposable sources (such as network connections or file streams), it must implement <code>IDisposable</code> or <code>IAsyncDisposable</code>. The DI container will automatically call <code>Dispose</code> or <code>DisposeAsync</code> for you at the end of the service's lifetime. This is an important behavior to prevent source issues.</p>
<p>In .NET 9, asynchronous disposal is the preferred model for services that perform I/O operations during cleanup. The container manages both asynchronous and synchronous disposal operations in a controlled manner.</p>
<p>In this example, <code>MyNetworkService</code> gets <code>HttpClient</code> via DI (which it will not dispose of) but also creates its own <code>FileStream</code> resource, which it is responsible for disposing of asynchronously.</p>
<pre><code class="language-csharp">public class MyNetworkService : IAsyncDisposable
{
    private readonly HttpClient _httpClient; 
    
    private readonly FileStream _logStream; 
    private bool _disposed = false;

    public MyNetworkService(HttpClient httpClient)
    {
        _httpClient = httpClient;
        
        _logStream = new FileStream($&quot;log_{Guid.NewGuid()}.txt&quot;, 
            FileMode.CreateNew, FileAccess.Write, FileShare.None, 
            4096, useAsync: true);
    }

    public async Task&lt;string&gt; FetchDataAsync()
    {
        var data = await _httpClient.GetStringAsync(&quot;https://api.example.com/data&quot;);
        await _logStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(data));
        return data;
    }

    // The container calls this automatically when the scope ends
    public async ValueTask DisposeAsync()
    {
        if (_disposed)
        {
            return;
        }

        // Asynchronous cleanup for resources WE OWN
        // We do NOT dispose of _httpClient here.
        await _logStream.FlushAsync();
        await _logStream.DisposeAsync();
        
        _disposed = true;
        GC.SuppressFinalize(this);
    }
}
</code></pre>
<p><strong>Using <code>await using</code> for Manual disposal:</strong></p>
<p>When you manually create service instances outside of the DI container, you should use the <code>await using</code> syntax to ensure proper asynchronous disposal.</p>
<pre><code class="language-csharp">// Assume HttpClient is coming from somewhere
public async Task ProcessDataAsync(HttpClient httpClient)
{
    await using var service = new MyNetworkService(httpClient);
    await service.FetchDataAsync();
    // DisposeAsync() is called automatically here
}
</code></pre>
<p><strong>Best Practices:</strong></p>
<ul>
<li>If your cleanup operation involves asynchronous operations (file I/O, database connections, network calls), it would be more appropriate to implement <code>IAsyncDisposable</code>.</li>
<li>If your service can be used in both synchronous and asynchronous contexts, you can implement both <code>IDisposable</code> and <code>IAsyncDisposable</code>.</li>
<li>The DI container will call <code>DisposeAsync()</code> if present; otherwise it will fallback to <code>Dispose()</code>.</li>
<li>It is recommended to always call <code>GC.SuppressFinalize(this)</code> at the end of your disposal method to avoid unnecessary terminations.</li>
</ul>
<h3>Handling Circular Dependencies</h3>
<p>A circular dependency occurs when Service A depends on Service B, and Service B, in turn, depends on Service A. The ASP.NET Core container automatically detects this situation during object resolution (at parse time) and throws an <code>InvalidOperationException</code> to prevent a stack overflow, usually with a message detailing the dependency loop.</p>
<p>To resolve this, you must refactor your design to break the circular reference. The most common solution is to introduce a new intermediary abstraction (like an interface) that one of the services can depend on, breaking the direct loop.</p>
<p><strong>You Should Avoid Asynchronous Logic in Constructors</strong></p>
<ul>
<li>Constructors must be fast and synchronous. You should not perform <code>await</code> or long running operations.</li>
<li>Asynchronous operation in constructors can lead to deadlocks and unpredictable initialization behavior.</li>
</ul>
<p>For asynchronous initialization you should use <code>IHostedService</code>, factory patterns or lazy initialization.</p>
<pre><code class="language-csharp">// Bad: Async work in constructor
public class BadService
{
    public BadService(IDataService dataService)
    {
        // This will deadlock or fail!
        var data = dataService.GetDataAsync().Result;
    }
}

// Good: Use IHostedService for async initialization
public class GoodInitializationService : IHostedService
{
    private readonly IDataService _dataService;

    public GoodInitializationService(IDataService dataService)
    {
        _dataService = dataService;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Proper async initialization
        var data = await _dataService.GetDataAsync();
    }

    public Task StopAsync(CancellationToken cancellationToken) =&gt; Task.CompletedTask;
}
</code></pre>
<p><strong>Captive Dependency Issues</strong></p>
<ul>
<li>It is created by injecting a shorter lived service (Scoped) into a longer lived service (Singleton).</li>
<li>The scoped service becomes &quot;captive&quot; and lives as long as the singleton service, which can lead to stale data and memory leaks.</li>
<li>The container's <strong>ValidateScopes</strong> option will detect this at runtime.</li>
</ul>
<pre><code class="language-csharp">// Bad: Scoped service captured by singleton
builder.Services.AddSingleton&lt;MySingletonService&gt;(); // Holds DbContext forever!
builder.Services.AddScoped&lt;AppDbContext&gt;();

// Good: Use IServiceScopeFactory in singletons
public class MySingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;
    
    public MySingletonService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }
    
    public async Task DoWorkAsync()
    {
        await using var scope = _scopeFactory.CreateAsyncScope();
        var dbContext = scope.ServiceProvider.GetRequiredService&lt;AppDbContext&gt;();
        // Use dbContext safely within this scope
    }
}
</code></pre>
<p><strong>Avoiding Static Shared State in Singleton Services</strong></p>
<ul>
<li>Singleton services should be stateless or have thread safe state management.</li>
<li>Avoid using static fields or shared mutable states that can cause race conditions.</li>
</ul>
<p>For immutable objects, the <code>ConcurrentDictionary</code> or appropriate locking mechanisms must be used.</p>
<pre><code class="language-csharp">// Bad: Shared mutable state in singleton
public class BadCacheService
{
    private Dictionary&lt;string, string&gt; _cache = new(); // Not thread safe!

    public void Add(string key, string value) =&gt; _cache[key] = value;
}

// Good: Thread safe state management
public class GoodCacheService
{
    private readonly ConcurrentDictionary&lt;string, string&gt; _cache = new();

    public void Add(string key, string value) =&gt; _cache[key] = value;
}
</code></pre>
<h3>Testing, Isolation, and Readability</h3>
<p>One of the primary goals of DI is testability. In integration tests, you can use the WebApplicationFactory to override service registrations with mock applications.</p>
<pre><code class="language-csharp">await using var application = new WebApplicationFactory&lt;Program&gt;()
    .WithWebHostBuilder(builder =&gt;
    {
        builder.ConfigureTestServices(services =&gt;
        {
            services.AddScoped&lt;IExternalService, MockExternalService&gt;();
        });
    });
</code></pre>
<h2>Performance Considerations</h2>
<p>The performance of <strong>Dependency injection</strong> in ASP.NET Core is adequate for most applications, but it's worth being aware of the mechanics.</p>
<h3>Resolution Cost, Service Graph Caching, and Object Pooling</h3>
<ul>
<li><strong>Service Graph Caching:</strong> When a service graph is first parsed, the container creates and caches an execution plan. This plan includes the entire dependency tree and how each object will be created. Subsequent solutions will use this cached plan, making them extremely fast.</li>
<li><strong>Transient and Singleton Resolving:</strong> Transient services have a slightly higher creation cost because a new instance is created each time. However, this cost is generally negligible unless you are resolving thousands of transient services per request.</li>
<li><strong>Object Pool:</strong> For high performance scenarios, it would be beneficial for performance management to consider using <code>ObjectPool&lt;T&gt;</code> from <code>Microsoft.Extensions.ObjectPool</code> to reuse expensive objects instead of creating new transient instances.</li>
</ul>
<h3>Benchmarking DI Performance</h3>
<p>Here is an example of a minimum benchmark comparing transient and singleton resolution times using <code>BenchmarkDotNet</code>.</p>
<pre><code class="language-csharp">using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using Microsoft.Extensions.DependencyInjection;

public class DependencyInjectionBenchmark
{
    private ServiceProvider _serviceProvider = null!;

    [GlobalSetup]
    public void Setup()
    {
        var services = new ServiceCollection();
        services.AddTransient&lt;ITransientService, TransientService&gt;();
        services.AddSingleton&lt;ISingletonService, SingletonService&gt;();
        services.AddScoped&lt;IScopedService, ScopedService&gt;();
        _serviceProvider = services.BuildServiceProvider();
    }

    [Benchmark]
    public ITransientService ResolveTransient()
    {
        return _serviceProvider.GetRequiredService&lt;ITransientService&gt;();
    }

    [Benchmark]
    public ISingletonService ResolveSingleton()
    {
        return _serviceProvider.GetRequiredService&lt;ISingletonService&gt;();
    }

    [Benchmark]
    public IScopedService ResolveScoped()
    {
        using var scope = _serviceProvider.CreateScope();
        return scope.ServiceProvider.GetRequiredService&lt;IScopedService&gt;();
    }
}

public interface ITransientService { }
public class TransientService : ITransientService { }
public interface ISingletonService { }
public class SingletonService : ISingletonService { }
public interface IScopedService { }
public class ScopedService : IScopedService { }

// Results:
// | Method           | Mean      | Error    | StdDev   |
// |----------------- |----------:|---------:|---------:|
// | ResolveSingleton |  3.5 ns   | 0.02 ns  | 0.02 ns  | ← Fastest (cached instance)
// | ResolveTransient | 45.2 ns   | 0.31 ns  | 0.29 ns  | ← Allocation overhead
// | ResolveScoped    | 78.4 ns   | 0.52 ns  | 0.48 ns  | ← Scope creation + resolution
</code></pre>
<p><strong>Key Points:</strong></p>
<ul>
<li>Singleton resolution is almost instantaneous (cached instance invocation).</li>
<li>Transient solution requires cost but is still fast.</li>
<li>Scoped solution includes the overhead of creating scope.</li>
</ul>
<p>For most applications, these differences are not noticeable. It should only be optimized if profiling reveals that DI is a bottleneck.</p>
<h3>Startup Time and Compile Time DI</h3>
<p>Application startup time is the primary driver of performance. .NET uses <strong>Source Generated Dependency Injection</strong> to move dependency graph resolution from runtime to compile time.</p>
<p><strong>Benefits:</strong></p>
<ul>
<li><strong>Faster Startup:</strong> There is no runtime reflection to create the service graph.</li>
<li><strong>Reduced Memory Usage:</strong> It produces smaller runtime.</li>
<li><strong>AOT Friendly:</strong> Provides support for Native AOT compilation, which is critical for cloud native and containerized applications.</li>
<li><strong>Compile Time Validation:</strong> Allows catching missing service records at compile time rather than runtime.</li>
</ul>
<p><strong>How to Enable</strong></p>
<p><strong>1. For Core Dependency Injection (DI) Generation</strong></p>
<p>This is used as the main constructor that creates the optimized service provider for <code>AddScoped</code>, <code>AddSingleton</code>, etc.</p>
<ul>
<li><strong>Activation</strong> This will be automatically enabled when you publish with native AOT.</li>
<li><strong>Project file</strong>
<pre><code class="language-xml">&lt;PropertyGroup&gt;
    &lt;PublishAot&gt;true&lt;/PublishAot&gt;
&lt;/PropertyGroup&gt;
</code></pre>
</li>
</ul>
<p><strong>2. For Configuration Binding Generation</strong></p>
<p>This is used to bind settings from sources like <code>appsettings.json</code> to your C# classes (<code>Bind</code>, <code>Configure</code>).</p>
<ul>
<li><strong>Project file:</strong>
<pre><code class="language-xml">&lt;PropertyGroup&gt;
    &lt;EnableConfigurationBindingGenerator&gt;true&lt;/EnableConfigurationBindingGenerator&gt;
&lt;/PropertyGroup&gt;
</code></pre>
</li>
</ul>
<p><strong>Example Code (using all generators):</strong></p>
<pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

// This call is optimized by 'EnableConfigurationBindingGenerator'
builder.Services.Configure&lt;MyOptions&gt;(builder.Configuration.GetSection(&quot;MyOptions&quot;));

// These calls are optimized by the Core DI generator (when PublishAot=true)
builder.Services.AddScoped&lt;IMyService, MyService&gt;();
var app = builder.Build();
</code></pre>
<p>When all generators are enabled, the compiler generates optimized service registration and parsing code, eliminating the reflection overhead.</p>
<p><strong>When to Use:</strong></p>
<ul>
<li>Microservices and serverless functions (when it is desired to minimize cold start time).</li>
<li>Native AOT scenarios (e.g. containerized applications, edge computing).</li>
<li>Large applications with complex dependency graphs.</li>
</ul>
<h2>Advanced Dependency Injection Patterns in .NET</h2>
<h3>Keyed Services</h3>
<p>Keyed services, introduced in .NET 8, allow you to register multiple implementations of an interface and resolve a specific implementation using a key. This will be a useful feature for polymorphic scenarios where you need to choose a strategy at runtime.</p>
<pre><code class="language-csharp">// Registration
builder.Services.AddKeyedSingleton&lt;INotificationService, EmailNotificationService&gt;(&quot;email&quot;);
builder.Services.AddKeyedSingleton&lt;INotificationService, SmsNotificationService&gt;(&quot;sms&quot;);

// Resolution in a consumer class
public class NotificationController([FromKeyedServices(&quot;email&quot;)] INotificationService emailService)
{
    // ...
}
</code></pre>
<h3>Open Generic Registrations</h3>
<p>To avoid manually registering each generic implementation (e.g., <code>IRepository&lt;User&gt;</code>, <code>IRepository&lt;Product&gt;</code>), you can use an explicit global registration.</p>
<pre><code class="language-csharp">// This single line registers the Repository&lt;T&gt; for any T requested via IRepository&lt;T&gt;
builder.Services.AddScoped(typeof(IRepository&lt;&gt;), typeof(Repository&lt;&gt;));
</code></pre>
<h3>The Decorator Pattern</h3>
<p>Decorators allow you to add functionality to a service without modifying it. This is a perfect example of the Open/Closed Principle. You can register a decorator that wraps the original service and adds cross cutting functionality like logging, caching, or validation.</p>
<p><strong>Without third party libraries (manual approach):</strong></p>
<pre><code class="language-csharp">// Original service interface
public interface IOrderProcessor
{
    Task ProcessOrderAsync(Order order);
}

// Base implementation
public class OrderProcessor : IOrderProcessor
{
    public async Task ProcessOrderAsync(Order order)
    {
        await Task.Delay(100);
        Console.WriteLine($&quot;Order {order.Id} processed.&quot;);
    }
}

// Logging decorator
public class LoggingOrderProcessorDecorator : IOrderProcessor
{
    private readonly IOrderProcessor _inner;
    private readonly ILogger&lt;LoggingOrderProcessorDecorator&gt; _logger;

    public LoggingOrderProcessorDecorator(IOrderProcessor inner, ILogger&lt;LoggingOrderProcessorDecorator&gt; logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task ProcessOrderAsync(Order order)
    {
        _logger.LogInformation(&quot;Processing order {OrderId}...&quot;, order.Id);
        try
        {
            await _inner.ProcessOrderAsync(order);
            _logger.LogInformation(&quot;Order {OrderId} processed successfully.&quot;, order.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, &quot;Failed to process order {OrderId}.&quot;, order.Id);
            throw;
        }
    }
}

// Manual registration (layering decorators)
builder.Services.AddScoped&lt;OrderProcessor&gt;();
builder.Services.AddScoped&lt;IOrderProcessor&gt;(provider =&gt;
{
    var baseProcessor = provider.GetRequiredService&lt;OrderProcessor&gt;();
    var logger = provider.GetRequiredService&lt;ILogger&lt;LoggingOrderProcessorDecorator&gt;&gt;();
    return new LoggingOrderProcessorDecorator(baseProcessor, logger);
});
</code></pre>
<p><strong>With Scrutor library (recommended for complex scenarios):</strong></p>
<pre><code class="language-csharp">builder.Services.AddScoped&lt;IOrderProcessor, OrderProcessor&gt;();
builder.Services.Decorate&lt;IOrderProcessor, LoggingOrderProcessorDecorator&gt;();
// Add more decorators
builder.Services.Decorate&lt;IOrderProcessor, CachingOrderProcessorDecorator&gt;();
</code></pre>
<p><strong>Middleware Integration Context</strong></p>
<p>Decorators work in a similar way with ASP.NET Core middleware. While the middleware operates at the HTTP pipeline level, decorators operate at the service level, allowing you to apply cross cutting concerns to business logic independent of HTTP concerns.</p>
<h3>Conditional Registrations</h3>
<p>You can conditionally register services based on runtime configuration or environment.</p>
<pre><code class="language-csharp">var builder = WebApplication.CreateBuilder(args);

// You can register different applications depending on the environment
if (builder.Environment.IsDevelopment())
{
    builder.Services.AddScoped&lt;IEmailService, FakeEmailService&gt;();
}
else
{
    builder.Services.AddScoped&lt;IEmailService, SmtpEmailService&gt;();
}

// Register based on configuration
var useRedis = builder.Configuration.GetValue&lt;bool&gt;(&quot;UseRedisCache&quot;);
if (useRedis)
{
    builder.Services.AddStackExchangeRedisCache(options =&gt; { /* ... */ });
}
else
{
    builder.Services.AddDistributedMemoryCache();
}
</code></pre>
<h3>Child Containers and Nested Scopes</h3>
<p>While the built in ASP.NET Core container doesn't support &quot;subcontainers&quot; like Autofac or other third party containers, you can achieve similar isolation using scopes.</p>
<p>It's important to differentiate this from the Service Locator anti pattern, which involves directly injecting IServiceProvider to manually resolve dependencies. Instead, the correct approach (especially within Singleton services) is to inject <code>IServiceScopeFactory</code>.</p>
<p><strong>Comprehensive embedded approach</strong></p>
<pre><code class="language-csharp">public class ParentService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public ParentService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public async Task DoIsolatedWorkAsync()
    {
        await using var scope = _scopeFactory.CreateAsyncScope();
        
        // Resolve services from the new scope's provider
        var isolatedService = scope.ServiceProvider.GetRequiredService&lt;IIsolatedService&gt;();
        await isolatedService.DoWorkAsync();
        // All services resolved from 'scope.ServiceProvider' are disposed here
    }
}
</code></pre>
<p><strong>Third party containers (Autofac example)</strong></p>
<pre><code class="language-csharp">// Autofac supports real subcontainers with invalid records
var childLifetimeScope = container.BeginLifetimeScope(builder =&gt;
{
    builder.RegisterType&lt;MockDatabaseService&gt;().As&lt;IDatabaseService&gt;();
});
</code></pre>
<p>The built in container's scoping mechanism is simpler but sufficient for most scenarios. You can use third party containers only when you need advanced features like property injection, assembly scanning with rules, or complex lifetime management.</p>
<h3>Testing with DI and <code>ConfigureTestServices</code></h3>
<p>One of DI's greatest strengths is testability. In integration tests, you can replace real services with mock or simulated services using WebApplicationFactory and ConfigureTestServices.</p>
<pre><code class="language-csharp">using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

public class OrderControllerTests : IClassFixture&lt;WebApplicationFactory&lt;Program&gt;&gt;
{
    private readonly WebApplicationFactory&lt;Program&gt; _factory;

    public OrderControllerTests(WebApplicationFactory&lt;Program&gt; factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task ProcessOrder_ReturnsSuccess()
    {
        //Replace real service with mock
        var client = _factory.WithWebHostBuilder(builder =&gt;
        {
            builder.ConfigureTestServices(services =&gt;
            {
                //Remove actual service
                services.RemoveAll&lt;IOrderProcessor&gt;();
                
                // Add mock service
                services.AddScoped&lt;IOrderProcessor, MockOrderProcessor&gt;();
                
                // Or use a mocking framework
                var mockProcessor = new Mock&lt;IOrderProcessor&gt;();
                mockProcessor.Setup(x =&gt; x.ProcessOrderAsync(It.IsAny&lt;Order&gt;()))
                            .ReturnsAsync(true);
                services.AddScoped(_ =&gt; mockProcessor.Object);
            });
        }).CreateClient();

        // Act
        var response = await client.PostAsJsonAsync(&quot;/api/orders&quot;, new Order { Id = 1 });

        // Assert
        response.EnsureSuccessStatusCode();
    }
}
</code></pre>
<p><strong>Key Benefits:</strong></p>
<ul>
<li>You can replace expensive external dependencies with in memory mocks.</li>
<li>You can test business logic in isolation.</li>
<li>You can run fast and accurate tests without needing external dependencies.</li>
</ul>
<h2>Example: A Background Service</h2>
<p>A common scenario where DI lifecycle management is critical is with singletons, such as background workers or IHostedServices. You cannot directly add a scoped service like DbContext to this service. Instead, you'd be better off adding an IServiceScopeFactory to manually create scopes.</p>
<p><strong>Hosted Service (<code>OrderProcessorWorker.cs</code>):</strong></p>
<pre><code class="language-csharp">public class OrderProcessorWorker : BackgroundService
{
    private readonly ILogger&lt;OrderProcessorWorker&gt; _logger;
    private readonly IServiceScopeFactory _scopeFactory;

    // Inject IServiceScopeFactory, not DbContext
    public OrderProcessorWorker(ILogger&lt;OrderProcessorWorker&gt; logger, IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation(&quot;Processing new orders...&quot;);

            // Create a new scope for this unit of work
            await using (var scope = _scopeFactory.CreateAsyncScope())
            {
                var orderRepository = scope.ServiceProvider.GetRequiredService&lt;IOrderRepository&gt;();
                var newOrders = await orderRepository.GetNewOrdersAsync();
                
                foreach (var order in newOrders)
                {
                    // Process order...
                }
                
                await orderRepository.SaveChangesAsync();
            }

            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}
</code></pre>
<p><strong>Registration (<code>Program.cs</code>):</strong></p>
<pre><code class="language-csharp">builder.Services.AddDbContext&lt;AppDbContext&gt;(/* ... */);
builder.Services.AddScoped&lt;IOrderRepository, OrderRepository&gt;();
builder.Services.AddHostedService&lt;OrderProcessorWorker&gt;();
</code></pre>
<p>This pattern ensures that each run of the worker uses a new <code>DbContext</code>, preventing problems such as memory leaks or stale data.</p>
<blockquote>
<p>While this example uses a simple <code>Task.Delay</code> loop within the <code>BackgroundService</code>, a robust pattern for managing decoupled background tasks involves an in memory queue. You can learn how to build this system by following this guide: <a href="https://abp.io/community/articles/how-to-build-an-in-memory-background-job-queue-in-asp.net-core-from-scratch-pai2zmtr">How to Build an In Memory Background Job Queue in ASP.NET Core From Scratch</a>.</p>
</blockquote>
<h2>Conclusion</h2>
<p>Understanding the <strong>ASP.NET Core Dependency Injection</strong> framework is essential for any .NET developer. By understanding the built in IoC container, choosing the right service lifecycles, and opting for explicit constructor injection, you can create modular, testable, and maintainable applications.</p>
<p><strong>.NET brings significant enhancements to the DI ecosystem</strong></p>
<ul>
<li><strong>Source Generated DI</strong> for faster startup and Native AOT support</li>
<li><strong>Keyed Services</strong> for advanced polymorphic resolution scenarios</li>
<li><strong>Enhanced lifetime validation</strong> catches captive dependencies at development time</li>
<li><strong>Improved <code>IAsyncDisposable</code></strong> support for proper source cleanup*</li>
<li><strong>Good integration with C# features</strong> such as primary constructors</li>
</ul>
<p>By embracing these features and implementing patterns such as decorators, manual scoping with <code>IServiceScopeFactory</code>, the Option Pattern for configuration, and proper asynchronous disposal, you can solve complex architectural challenges cleanly and efficiently.</p>
<p><strong>Key Points</strong></p>
<ul>
<li><strong>Always inject dependencies via constructors</strong> explicit, immutable, testable</li>
<li><strong>Understanding lifetime implications</strong> avoid dependent dependencies, you can use <code>IServiceScopeFactory</code> on singletons</li>
<li><strong>Leverage the Option Pattern</strong> type safe, validated configuration injection</li>
<li><strong>You can use TryAdd methods</strong> create mergeable records</li>
<li><strong>Leverage DI in your tests</strong> you can use <code>ConfigureTestServices</code> to inject mocks</li>
<li><strong>Measure performance first; don't assume DI is a bottleneck</strong> The internal container is efficient. Use a profiler to find real slowdowns before trying to optimize DI.</li>
<li><strong>Consider source generation</strong> for microservices, serverless and AOT scenarios</li>
</ul>
<p>The transition from legacy, fragmented DI environments to a unified, performant, and compile time optimized dependency injection system represents a significant development in the platform's history. Understanding and leveraging these capabilities is crucial for building high performance, cloud native .NET applications.</p>
<h3>Further Reading</h3>
<ul>
<li><a href="https://abp.io/docs/10.0/framework/fundamentals/dependency-injection">ABP Dependency Injection</a></li>
<li><a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection">Official Microsoft Docs on Dependency Injection in ASP.NET Core</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines">Source Generators for Dependency Injection</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory">IHttpClientFactory with .NET</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-9.0#keyed-services">Keyed Services DI Container</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/scoped-service">Use Scoped Services Within a Scoped Service</a></li>
<li><a href="https://github.com/khellang/Scrutor">Scrutor</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1d191e-12f2-87db-b6e5-53ae5e355274" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1d191e-12f2-87db-b6e5-53ae5e355274" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-build-an-in-memory-background-job-queue-in-asp.net-core-from-scratch-pai2zmtr</guid>
      <link>https://abp.io/community/posts/how-to-build-an-in-memory-background-job-queue-in-asp.net-core-from-scratch-pai2zmtr</link>
      <a10:author>
        <a10:name>oguzhan.agir</a10:name>
        <a10:uri>https://abp.io/community/members/oguzhan.agir</a10:uri>
      </a10:author>
      <category>rabbitmq</category>
      <category>background-worker</category>
      <category>asp.net-core</category>
      <category>hangfire</category>
      <category>background-jobs</category>
      <title>How to Build an In Memory Background Job Queue in ASP.NET Core from Scratch</title>
      <description>Discover how to create an in memory background job queue from scratch in ASP.NET Core. This article explains the producer-consumer architecture, System.Threading.Channels, and hosted services for running long running tasks asynchronously. Reduce API response times and learn when to move to advanced solutions like Hangfire or RabbitMQ for scalability and reliability.</description>
      <pubDate>Fri, 19 Sep 2025 13:43:07 Z</pubDate>
      <a10:updated>2026-10-14T16:33:14Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to Build an In Memory Background Job Queue in ASP.NET Core from Scratch</h1>
<p>In web applications, providing a fast and responsive API is crucial for the user experience. However, some operations, such as sending emails, generating reports, or adding users in bulk, can take significant time. Performing these tasks during a request can block threads, resulting in slow response times.</p>
<p>To significantly alleviate this issue, it would be more effective to run long running tasks in a background process. Instead of waiting for the request to complete, we can queue the task, provide an immediate response to the user, and perform the work in a separate background process.</p>
<p>While libraries like Hangfire or message brokers like RabbitMQ exist for this purpose, you don't need any external dependencies. In this blog post, we'll build an in memory background job queue from scratch in ASP.NET Core using only built in .NET features.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/structure.png" alt="Background Job Queue Structure in ASP.NET Core" /></p>
<h2>Core Components of Background Job System</h2>
<p>Before implementing the code, it is important to first understand the basic parts that make up the background job system and how they work to process queued tasks.</p>
<h3>What are IHostedService and BackgroundService?</h3>
<p><code>IHostedService</code> is an interface used in ASP.NET Core to implement long running background tasks managed by the application's lifecycle. When your application starts, it starts all registered <code>IHostedService</code> applications. When it shuts down, it instructs them to stop.</p>
<p><code>BackgroundService</code> is an abstract base class that implements <code>IHostedService</code>. It gives you a more efficient way to create a timed service by giving you a single overridable method <code>ExecuteAsync(CancellationToken stoppingToken)</code>. We'll use this as the basis for our job processor.</p>
<h3>The Producer/Consumer Structure</h3>
<ul>
<li><strong>Producer:</strong> This is the part of your application that creates jobs and adds them to the queue. In our example, the producer will be an API Controller.</li>
<li><strong>Consumer:</strong> This is a background service that constantly monitors the queue, pulls jobs from the queue, and executes them. Our <code>BackgroundService</code> application will be the consumer.</li>
<li><strong>Queue:</strong> This is the data structure between the producer and consumer that holds jobs waiting to be processed.</li>
</ul>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/producer-consumer.png" alt="Producer-Consumer Background Job Queue Structure" /></p>
<h3>Why System.Threading.Channels?</h3>
<p>Introduced in .NET Core 3.0, <code>System.Threading.Channels</code> provides a synchronization data structure designed for asynchronous producer, consumer scenarios.</p>
<ul>
<li><strong>Thread Safety:</strong> Handles all complex locking and synchronization operations internally, making it usable as a whole from multiple threads (for example, when multiple API requests are adding work simultaneously).</li>
<li><strong>Async Native:</strong> Designed for use with <code>async</code>/<code>await</code>, preventing thread blocking. You can asynchronously wait for an item in the queue to become available.</li>
<li><strong>Performance:</strong> Optimized for speed and low memory allocation. Generally superior to legacy constructs like <code>ConcurrentQueue</code> for asynchronous workflows because it eliminates the need for polling.</li>
</ul>
<h2>Implementing the Background Job Queue</h2>
<p>Now that we understand the architecture, we can implement the background job queue.</p>
<h3>Create the Project</h3>
<p>First, create a new ASP.NET Core Web API project using the .NET CLI.</p>
<pre><code class="language-bash">dotnet new sln -n BackgroundJobQueue

dotnet new webapi -n BackgroundJobQueue.Api

dotnet sln BackgroundJobQueue.sln add BackgroundJobQueue.Api/BackgroundJobQueue.Api.csproj
</code></pre>
<h3>Define the Job Queue Interface</h3>
<p>For dependency injection and testability, we'll start by defining an interface for our queue. This interface will define methods for adding a job (<code>EnqueueAsync</code>) and removing a job (<code>DequeueAsync</code>).</p>
<p>Create a new file named <code>IBackgroundTaskQueue.cs</code>.</p>
<pre><code class="language-csharp">namespace BackgroundJobQueue;

public interface IBackgroundTaskQueue
{
    // Adds a work item to the queue
    ValueTask EnqueueAsync(Func&lt;CancellationToken, ValueTask&gt; workItem);

    // Removes and returns a work item from the queue
    ValueTask&lt;Func&lt;CancellationToken, ValueTask&gt;&gt; DequeueAsync(CancellationToken cancellationToken);
}
</code></pre>
<p>We'll use <code>Func&lt;CancellationToken, ValueTask&gt;</code> to represent a work item. This allows us to queue any asynchronous method that accepts a <code>CancellationToken</code>.</p>
<h3>Implement the Queue Service with Channels</h3>
<p>Now, let's implement the <code>IBackgroundTaskQueue</code> interface using the <code>System.Threading.Channels</code> class. This class will manage the in memory channel.</p>
<p>Create a new file named <code>BackgroundTaskQueue.cs</code>.</p>
<pre><code class="language-csharp">using System.Threading.Channels;

namespace BackgroundJobQueue;

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel&lt;Func&lt;CancellationToken, ValueTask&gt;&gt; _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // BoundedChannelOptions specifies the behavior of the channel.
        var options = new BoundedChannelOptions(capacity)
        {
            // FullMode.Wait tells the writer to wait asynchronously if the queue is full.
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded&lt;Func&lt;CancellationToken, ValueTask&gt;&gt;(options);
    }

    public async ValueTask EnqueueAsync(Func&lt;CancellationToken, ValueTask&gt; workItem)
    {
        if (workItem is null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        // Writes an item to the channel. If the channel is full,
        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask&lt;Func&lt;CancellationToken, ValueTask&gt;&gt; DequeueAsync(CancellationToken cancellationToken)
    {
        // Reads an item from the channel. If the channel is empty,
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}
</code></pre>
<h3>Create the Consumer Service (QueuedHostedService)</h3>
<p>This is our consumer. It's a <code>BackgroundService</code> that continuously dequeues and executes jobs.</p>
<p>Create a new file named <code>QueuedHostedService.cs</code>.</p>
<pre><code class="language-csharp">namespace BackgroundJobQueue;

public class QueuedHostedService : BackgroundService
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger&lt;QueuedHostedService&gt; _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger&lt;QueuedHostedService&gt; logger)
    {
        _taskQueue = taskQueue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(&quot;Queued Hosted Service is running.&quot;);

        while (!stoppingToken.IsCancellationRequested)
        {
            // Dequeue a work item
            var workItem = await _taskQueue.DequeueAsync(stoppingToken);

            try
            {
                // Execute the work item
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, &quot;Error occurred executing {WorkItem}.&quot;, nameof(workItem));
            }
        }

        _logger.LogInformation(&quot;Queued Hosted Service is stopping.&quot;);
    }
}
</code></pre>
<p>The <code>try-catch</code> block here is critical. It ensures that if one job fails with an exception, it won't crash the entire background service.</p>
<h3>Register the Services in Program.cs</h3>
<p>Next we need to register the queue and hosted service in the dependency injection container inside <code>Program.cs</code>.</p>
<pre><code class="language-csharp">using BackgroundJobQueue;

var builder = WebApplication.CreateBuilder(args);

//...

// Register the background task queue as a Singleton
builder.Services.AddSingleton&lt;IBackgroundTaskQueue&gt;(_ =&gt; 
{
    // You can configure the capacity of the queue here
    if (!int.TryParse(builder.Configuration[&quot;QueueCapacity&quot;], out var queueCapacity))
    {
        queueCapacity = 100;
    }
    return new BackgroundTaskQueue(queueCapacity);
});

// Register the hosted service
builder.Services.AddHostedService&lt;QueuedHostedService&gt;();


var app = builder.Build();

//...

app.Run();
</code></pre>
<p>We register <code>IBackgroundTaskQueue</code> as a <strong>Singleton</strong> because we need a single, shared queue instance for the entire application. We register <code>QueuedHostedService</code> using <code>AddHostedService()</code> to ensure the .NET runtime manages its lifecycle.</p>
<h3>Create a Producer (API Controller)</h3>
<p>Finally, let's create a producer. This will be a simple API controller with an endpoint that enqueues a new background job.</p>
<p>Create a new controller named <code>JobsController.cs</code>.</p>
<pre><code class="language-csharp">using Microsoft.AspNetCore.Mvc;

namespace BackgroundJobQueue.Controllers;

[ApiController]
[Route(&quot;[controller]&quot;)]
public class JobsController : ControllerBase
{
    private readonly IBackgroundTaskQueue _queue;
    private readonly ILogger&lt;JobsController&gt; _logger;

    public JobsController(IBackgroundTaskQueue queue, ILogger&lt;JobsController&gt; logger)
    {
        _queue = queue;
        _logger = logger;
    }

    [HttpPost]
    public async Task&lt;IActionResult&gt; EnqueueJob()
    {
        // Enqueue a job that simulates a long running task
        await _queue.EnqueueAsync(async token =&gt;
        {
            // Simulate a 5 second task
            var guid = Guid.NewGuid();
            _logger.LogInformation(&quot;Job {Guid} started.&quot;, guid);
            await Task.Delay(TimeSpan.FromSeconds(5), token);
            _logger.LogInformation(&quot;Job {Guid} finished.&quot;, guid);
        });

        return Ok(&quot;Job has been enqueued.&quot;);
    }
}
</code></pre>
<p>Now, when you run your application and send a POST request to <code>/jobs</code>, the API will respond instantly with &quot;Job has been enqueued.&quot; while the 5 second task runs in the background. You'll see the log messages appear in your console after the delay.</p>
<h2>Important Considerations and Advanced Topics</h2>
<h3>Shutdown</h3>
<p>The <code>CancellationToken</code> passed to ExecuteAsync is important. The host triggers this token when you stop your application. Our while loop condition <code>(!stoppingToken.IsCancellationRequested)</code> and passing the token to <code>DequeueAsync</code> cause the service to stop listening for new items and exit.</p>
<h3>Error Handling</h3>
<p>Our <code>try-catch</code> block prevents a single failed job from crashing the consumer. For production scenarios, you can consider a more advanced error handling strategy, such as a retry mechanism. Libraries like <strong>Polly</strong> can be integrated here to automatically retry failed jobs with policies like fallback.</p>
<h3>Limits of the In Memory Queue</h3>
<p>The biggest drawback of this approach is that the queue is <strong>in memory</strong>. If your application process restarts or crashes for any reason, any work waiting in the queue will be lost. This solution is best for idempotent or non critical tasks.</p>
<h3>When Should You Use External Libraries?</h3>
<p>Built from the ground up, this solution is powerful, but it's important to know when to turn to more advanced tools.</p>
<ul>
<li><p><strong>Hangfire:</strong> Use Hangfire when you need job continuity (saves jobs to a database), automatic retries, a management dashboard, scheduled jobs, and delayed execution. Background tasks are an ideal solution while processing.</p>
</li>
<li><p><strong>RabbitMQ</strong> When building a distributed system (such as a microservices architecture), you can use a custom message broker. These brokers provide scalable queues that isolate your services and guarantee message delivery.</p>
</li>
</ul>
<h2>Frequently Asked Questions</h2>
<p><strong>Q1: What happens to the jobs in the queue if my application restarts or crashes?</strong></p>
<p><strong>A:</strong> Because this is an <strong>in-memory</strong> queue, any unprocessed work is <strong>lost</strong> when the application closes or crashes. This solution is best suited for non-critical or idempotent tasks (i.e., they can be safely rerun without causing problems). For guaranteed business continuity, you should use a solution that stores jobs in a database or message broker, such as <strong>Hangfire</strong> or <strong>RabbitMQ</strong>.</p>
<p><strong>Q2: How can I implement retry logic for jobs that fail due to an exception?</strong></p>
<p><strong>A:</strong> The <code>try-catch</code> block in <code>QueuedHostedService</code> prevents the entire background service from crashing, but it doesn't automatically retry the failed job. You can integrate a library like <strong>Polly</strong> to add robust retry capabilities. You can wrap the <code>await workItem(stoppingToken);</code> call in a Polly retry policy to automatically rerun the job a configured number of times with delays.</p>
<p><a href="https://github.com/App-vNext/Polly">Polly Official Documentation</a></p>
<p><strong>Q3: Will this work in a load balanced environment with multiple server instances?</strong></p>
<p><strong>A:</strong> No, this implementation is instance-specific. The queue exists only in the memory of the server instance that receives the API request. If you deploy your application across multiple servers, a job queued on Server A will be processed only by Server A. For a shared queue that can be processed by any server in a web environment, you should use a centralized, distributed message broker such as <strong>RabbitMQ</strong>, <strong>Azure Service Bus</strong>.</p>
<p><strong>Q4: What happens if jobs are added to the queue faster than the background service can process them?</strong></p>
<p><strong>A:</strong> This application uses a BoundedChannel with a fixed capacity (for example, 100 in our example). When the queue is full, setting <code>FullMode = BoundedChannelFullMode.Wait</code> causes the producer (API controller) to asynchronously wait until space becomes available in the queue. This creates &quot;backpressure&quot; that prevents your application from running out of memory. However, this also means that if the background service is constantly overloaded, your API endpoint will become slower to respond as it waits to queue new jobs.</p>
<p><strong>Q5: How can I monitor the number of jobs currently in the queue?</strong></p>
<p><strong>A:</strong> For basic monitoring, you can inject <code>IBackgroundTaskQueue</code> into a service and periodically record the queue count. A more advanced approach is to create a custom metrics endpoint (for example, for Prometheus). You can create a custom controller that accesses the <code>Count</code> property of the <code>ChannelReader</code> to report the current queue depth and set alerts if the queue grows too long.</p>
<h2>Conclusion</h2>
<p>We've built a performant, in memory background job queue in ASP.NET Core using only built in framework features. This is a way to increase your API's responsiveness and improve the user experience by disabling long running tasks.</p>
<p>Using <code>IHostedService</code> and <code>System.Threading.Channels</code>, we've created an efficient implementation of the producer, consumer model. While this in memory approach has some limitations, it's a powerful tool for many common scenarios. As your needs grow, you can consider more feature rich tools like Hangfire or RabbitMQ, knowing you've mastered the fundamentals.</p>
<h2>References</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services">Background tasks with hosted services in ASP.NET Core</a></li>
<li><a href="https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/">An Introduction to System.Threading.Channels</a></li>
<li><a href="https://docs.abp.io/en/abp/latest/Background-Jobs">ABP Framework - Background Jobs</a></li>
<li><a href="https://andrewlock.net/controlling-ihostedservice-execution-order-in-aspnetcore-3/">Creating a queued background task service with IHostedService by Andrew Lock</a></li>
<li><a href="https://docs.hangfire.io/en/latest/getting-started/aspnet-core-applications.html">Hangfire Documentation</a></li>
<li><a href="https://www.rabbitmq.com/getstarted.html">RabbitMQ Tutorials</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1c7463-b737-6d5b-91ba-64eea608b153" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1c7463-b737-6d5b-91ba-64eea608b153" medium="image" />
    </item>
  </channel>
</rss>