Skip to content

CacheBoundary support for Blazor#65772

Merged
dariatiurina merged 63 commits into
dotnet:mainfrom
dariatiurina:55520-cache
Jul 10, 2026
Merged

CacheBoundary support for Blazor#65772
dariatiurina merged 63 commits into
dotnet:mainfrom
dariatiurina:55520-cache

Conversation

@dariatiurina

@dariatiurina dariatiurina commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

CacheBoundary support for Blazor

Description

Introduces CacheBoundary — a new Blazor component that enables output caching of server-side rendered (SSR) component subtrees. On a cache hit, child components inside a CacheBoundary are not instantiated or rendered; instead, the previously captured output is replayed directly as render tree frames.

For more details, see the design document.

How It Works

All coordination behind a CacheBoundary (key resolution, single-flight stampede protection, store interaction, cached-content deserialization, capture lifecycle, and background persistence) lives in the internal CacheBoundaryService. The CacheBoundary component stays focused on rendering, and CacheBoundaryTextWriter on writing.

Cache miss path

  1. CacheBoundary.SetParametersAsync calls CacheBoundaryService.PrepareAsync, which computes a cache key via CacheBoundaryKeyResolver from the component's tree position, CacheKey, and the VaryBy* dimension values read from the current HttpContext, then calls ICacheBoundaryStore.GetOrCreateAsync. Single-flight coordination ensures concurrent requests for the same key share one factory invocation. Caching is skipped entirely for non-GET requests, when Enabled is false, and when the boundary is rendered inside a streaming render context.
  2. On a cache miss, EndpointHtmlRenderer.WriteComponentHtml wraps the output TextWriter in a CacheBoundaryTextWriter (via CacheBoundaryService.TryBeginWrite) that tee-writes to both the response and an internal segment buffer.
  3. When hole components are encountered during capture, the writer records a hole entry (the component node serialized from its parent's frames) between markup segments.
  4. After the subtree walk completes, CacheBoundaryService.EndCapture finalizes the capture and CacheBoundaryTextWriter.GetSerializedRenderFragment() assembles the captured segments — markup segments plus hole component nodes (serialized via RenderFragmentSerializer.SerializeFrames) — into a SerializedRenderFragment object. That object (not a pre-serialized byte[]) is handed back to the store, which persists it: the HybridCache-backed store serializes it through the registered IHybridCacheSerializer<SerializedRenderFragment>, while the in-memory store keeps the object as-is. Persistence is observed in the background; failures are logged and do not fail the render.

Cache hit path

  1. CacheBoundaryService.PrepareAsync returns the cached SerializedRenderFragment (already deserialized by the store's serializer when coming from HybridCache, or the live object from the in-memory store), which CacheBoundary.BuildRenderTree renders.
  2. The fragment is a list of RenderTreeNode objects (markup nodes, component nodes, attributes, and nested RenderFragment parameters).
  3. RenderFragmentSerializer.Deserialize converts the nodes back into a RenderFragment that emits render tree frames directly into the builder — no child component instances are created and no lifecycle methods fire.

Same-request key collisions

Multiple CacheBoundary instances in one request can resolve to the same key (e.g. a reusable component containing a CacheBoundary used more than once, or a loop without an explicit CacheKey). They share one cache entry. On a warm request both simply hit it. On a cold request the first becomes the single-flight creator; because its captured output is only produced during HTML emission (after the render pass reaches quiescence), a sibling that resolved to the same key renders fresh that one time instead of deadlocking, and shares the cached entry on subsequent requests. The in-flight creator resolutions are tracked per request in HttpContext.Items.

Note: when two distinct CacheBoundary instances resolve to the same key in one request, PrepareAsync throws an actionable InvalidOperationException instructing the developer to set a unique CacheKey. The shared-entry behavior above applies to re-renders of the same boundary instance.

Hole detection

A "hole" is a component whose subtree must be re-rendered on every request because its content depends on per-request state (auth identity, form bindings, interactive render mode boundaries, etc.).

Detection is attribute-based via [CacheBoundaryPolicy] and lives in CacheBoundaryService.IsCacheableComponent(Type, CacheBoundaryVaryBy) (called from EndpointHtmlRenderer during capture). The method returns true when the component can be baked into the cached HTML and false when it must be treated as a hole:

  1. The component type is checked for [CacheBoundaryPolicy] (lookup is cached per-type in a ConcurrentDictionary that is cleared on hot-reload).
  2. Attribute absent → cacheable (returns true); the component is baked into cached HTML.
  3. Attribute present with VaryBy = None → unconditional hole (returns false, e.g. AntiforgeryToken, HeadOutlet, SSRRenderModeBoundary).
  4. Attribute present with VaryBy flags set → conditional hole. The component is cacheable only when the enclosing CacheBoundary's active VaryBy dimensions cover all of the attribute's required dimensions; otherwise it is a hole.
    • Example: AuthorizeViewCore is [CacheBoundaryPolicy(Disallow = true, VaryBy = CacheBoundaryVaryBy.User)]. If the boundary sets VaryByUser="true", the per-user dimension is in the cache key, so AuthorizeView is safe to cache. Otherwise it throws.
  5. If Disallow = true and the component would be a hole → an InvalidOperationException is thrown instead, instructing the developer to move the component outside the boundary or configure the boundary to vary by the required dimensions. Used for components whose parameters (delegates, expressions, complex objects) cannot be serialized.
  6. Components whose EndpointComponentState.StreamRendering is true also force a hole (streaming children can't update past a cached parent).

Built-in components marked with [CacheBoundaryPolicy]

Component Attribute Behavior
AuthorizeViewCore [CacheBoundaryPolicy(Disallow = true, VaryBy = User)] Throws unless VaryByUser is active on the boundary
QuickGrid [CacheBoundaryPolicy(Disallow = true, VaryBy = Query)] Throws unless VaryByQuery is active on the boundary
Virtualize [CacheBoundaryPolicy(Disallow = true)] Always throws
AntiforgeryToken [CacheBoundaryPolicy] Unconditional hole
HeadOutlet [CacheBoundaryPolicy] Unconditional hole
SSRRenderModeBoundary [CacheBoundaryPolicy] Unconditional hole

Choosing the cache store

By default CacheBoundary uses an in-memory store (MemoryCacheBoundaryStore). The store selection follows the same pattern as the circuit persistence provider (ICircuitPersistenceProvider): when a HybridCache service is registered, the HybridCache-backed store (HybridCacheBoundaryStore) is selected automatically; otherwise the in-memory store is used.

The mechanism is overwrite-safe and order-independent:

  • RazorComponentsServiceOptions.CacheBoundaryHybridCache is the single source of truth for which HybridCache to use.
  • DefaultCacheBoundaryHybridCache (an IPostConfigureOptions<RazorComponentsServiceOptions> with an optional HybridCache? = null constructor parameter) populates that option from the registered HybridCache only when it has not been explicitly set, so an explicit value is never overwritten. This mirrors DefaultHybridCache for circuit options.
  • The concrete ICacheBoundaryStore is resolved lazily from IOptions<RazorComponentsServiceOptions>: option set → HybridCacheBoundaryStore, otherwise MemoryCacheBoundaryStore.

An application can therefore opt in simply by calling AddHybridCache(), or point boundaries at a separately-configured HybridCache instance by setting CacheBoundaryHybridCache explicitly. With the HybridCache-backed store, ExpiresSliding is unsupported and throws NotSupportedException rather than being silently mapped to a different concept; use ExpiresAfter or ExpiresOn instead.

New public API

  • CacheBoundary (Microsoft.AspNetCore.Components) — wraps child content and caches its rendered HTML during SSR. Parameters:

    • ChildContent — the content to cache
    • CacheKey — explicit key for disambiguation when multiple CacheBoundary instances share the same parent
    • Enabled — toggle caching on/off (default true)
    • ExpiresAfter / ExpiresOn / ExpiresSliding — expiration policies (ExpiresSliding is not supported by the HybridCache-backed store)
    • VaryByQuery, VaryByRoute, VaryByHeader, VaryByCookie — vary by comma-separated request dimension names (VaryByQuery="*" varies by all query parameters)
    • VaryByUser — vary by authenticated identity
    • VaryByCulture — vary by current culture/UI culture
    • VaryBy — arbitrary custom string to vary by
  • CacheBoundaryPolicyAttribute (Microsoft.AspNetCore.Components) — controls how a component interacts with an enclosing CacheBoundary. When present, the component becomes a hole in the cached output. Properties:

    • Disallow — when true, throws InvalidOperationException instead of creating a hole (for components with non-serializable parameters). Defaults to false.
    • VaryByCacheBoundaryVaryBy flags that lift the exclusion when the boundary varies by those dimensions
  • CacheBoundaryVaryBy (Microsoft.AspNetCore.Components) — [Flags] enum: None, Query, Route, Header, Cookie, User, Culture

  • RazorComponentsServiceOptions.CacheBoundarySizeLimit — configurable maximum size (bytes) for the in-memory cache. Default 100 MB; 0 configures a zero-byte limit so entries are not cached. Negative values throw ArgumentOutOfRangeException.

  • RazorComponentsServiceOptions.CacheBoundaryHybridCache — the HybridCache instance used by CacheBoundary. When left unset it is populated automatically from the registered HybridCache service if one is available; otherwise the in-memory store is used. Set it explicitly to use a separately-configured HybridCache.

Internal infrastructure

  • CacheBoundaryService (singleton) — owns all coordination behind CacheBoundary: cache-key resolution, single-flight stampede protection, store interaction, cached-content deserialization, capture-writer creation and lifecycle, background persistence, hole classification (IsCacheableComponent), nested-boundary validation (ThrowIfNestedInsideCapturingBoundary), and the associated logging.
  • CacheBoundaryKeyResolver — computes a deterministic SHA-256 key from tree position, CacheKey, and the active VaryBy* dimension values from HttpContext. All inputs are length-prefixed to prevent delimiter-injection collisions; vary-by name lists are sorted for order independence.
  • ICacheBoundaryStore — pluggable cache backend abstraction. Its GetOrCreateAsync operates on SerializedRenderFragment objects (not byte[]), leaving serialization to the store implementation. Two implementations ship in this PR; the store is resolved lazily based on RazorComponentsServiceOptions.CacheBoundaryHybridCache:
    • MemoryCacheBoundaryStore (default) — uses MemoryCache with the configurable size limit, plus a per-key ConcurrentDictionary for single-flight coordination. It stores the SerializedRenderFragment object directly and sizes each entry from SerializedRenderFragment.ContentSize (captured markup length plus a per-hole overhead) instead of a serialized byte length.
    • HybridCacheBoundaryStore (used when a HybridCache is available) — delegates single-flight, stampede protection, local/distributed tiering, and serialization to HybridCache; tags entries so Clear() can evict all CacheBoundary entries on hot reload.
  • SerializedRenderFragmentHybridCacheSerializerIHybridCacheSerializer<SerializedRenderFragment> registered so HybridCache serializes cached fragments with the same System.Text.Json options as the rest of the component serialization pipeline (ServerComponentSerializationSettings.JsonSerializationOptions).
  • DefaultCacheBoundaryHybridCacheIPostConfigureOptions<RazorComponentsServiceOptions> that auto-populates CacheBoundaryHybridCache from a registered HybridCache only when unset (mirrors circuit persistence's DefaultHybridCache).
  • CacheStoreOptions — carries the expiration settings (ExpiresAfter, ExpiresOn, ExpiresSliding) from the component to the store.
  • CacheBoundaryRenderState — per-render coordination state for a single CacheBoundary instance, spanning the two phases of a cache miss (the render that begins the single-flight, and the later HTML emission that produces and persists the entry).
  • CacheBoundaryTextWriter — a TextWriter decorator that tee-writes to the response and an internal segment buffer, with capture/validation modes for hole creation. On completion, GetSerializedRenderFragment() assembles captured markup segments interleaved with hole component nodes into a SerializedRenderFragment, tracking the total captured size for cache accounting.
  • EndpointComponentState — assigns a TreePositionKeyFactory to each CacheBoundary instance, producing a key from the parent component type, the sequence number within the parent's render tree, and any @key directive. The sequence disambiguates multiple CacheBoundary siblings under the same parent.
  • RenderFragmentSerializer — shared serializer/deserializer for render tree frames ↔ RenderTreeNode JSON. Updated to capture Sequence, RenderModeName, and the render-mode Prerender flag on component nodes; the deserializer restores render mode via AddComponentRenderMode when replaying cached component nodes. SerializedRenderFragment also carries a [JsonIgnore] ContentSize used for in-memory cache sizing.
  • ComponentKeyHelper — shared utility (new file in src/Components/Shared/src/) for serializing @key values (primitives, Guid, DateTimeOffset, DateOnly, TimeOnly). Extracted from PersistentStateValueProviderKeyResolver so it is reused by both persistent state and cache boundary key computation.

Testing

  • Unit tests (src/Components/Endpoints/test/):

    • CacheBoundaryKeyResolverTest — deterministic key generation, VaryBy* dimension isolation, collision resistance between dimensions, delimiter-injection resistance.
    • CacheBoundaryRenderTest — fallback to ChildContent when the cached fragment is empty; cache hit skips ChildContent invocation; two distinct boundaries resolving to the same key throw an actionable error; the same boundary instance prepared twice reuses its in-flight resolution.
    • CacheBoundaryTextWriterTest — capture/markup/hole segment assembly.
    • IsCacheableComponentTest — component classification with/without the attribute, conditional exclusion with VaryBy flags, Disallow behavior, inherited attribute detection.
    • MemoryCacheBoundaryStoreTest / HybridCacheBoundaryStoreTest — store-level get-or-create, single-flight, and expiration behavior.
    • RazorComponentsServiceOptionsTestCacheBoundarySizeLimit validation.
    • RenderFragmentSerializerTest — round-trip serialization of markup, components, attributes, render modes, and nested fragments.
  • E2E tests (src/Components/test/E2ETest/Tests/), defined in CacheBoundaryTestBase and run against both stores:

    • CacheBoundaryCachesData — cached content persists across navigations while non-cached content changes.
    • CacheBoundaryDoesNotCacheDataWhenNotEnabledEnabled=false disables caching.
    • CacheBoundaryCorrectlyCreatesHoles[CacheBoundaryPolicy]-marked components remain functional across cache hits.
    • EditFormWithFormComponents_CachesStaticContent_AndFormStillSubmits — form holes stay functional while surrounding static content is cached.
    • CacheBoundaryInLoopUsesVaryByForDistinctEntriesVaryBy produces distinct cache entries in a loop.
    • CacheBoundaryMultipleHolesOfSameType_PreserveCorrectOrder — multiple holes of the same component type within a single CacheBoundary preserve correct order across cache hits.
    • CacheBoundaryCachesHardcodedHole — a [CacheBoundaryPolicy] hole hard-coded inside an intermediate component's own markup is cached around (static wrapper output cached, hole re-renders fresh).
    • CacheBoundaryTreatsStreamingChildAsHole — a streaming-rendered child is treated as a hole: surrounding static markup is served from the cache while the streaming child re-renders fresh on every request.
    • CacheBoundaryMemoryCacheTest runs these against the default in-memory store; CacheBoundaryHybridCacheTest runs them against the HybridCache-backed store to verify parity.

Limitations

Holes are located in their actual render-tree parent's frames, so a [CacheBoundaryPolicy] hole is supported regardless of how it reaches the cached subtree — whether it sits directly in the CacheBoundary's ChildContent, is passed through an intermediate component as @ChildContent, or is hard-coded in an intermediate component's own markup/BuildRenderTree. The CacheBoundaryCachesHardcodedHole E2E test exercises the hard-coded case: the wrapper's static output is served from the cache while the hole re-renders fresh on every request.

A CacheBoundary cannot be nested inside another CacheBoundary. The inner boundary's output would be frozen into the outer cache entry and replayed on later requests, which is unsafe for per-request content. This is detected during capture (ThrowIfNestedInsideCapturingBoundary) and throws an actionable InvalidOperationException.

The other case that is deliberately not supported is a hole component with a RenderFragment parameter. A hole is re-rendered on every request, but its parameters are captured once and replayed, so a RenderFragment parameter would be frozen to the content of the first render. Hard-coded hole with multiple RenderFragment parameters is an ambiguous edge case that is hard to define predictably for users, so we cut off the entire "hole with a RenderFragment parameter" case for consistency rather than supporting only some shapes of it. This is detected up front (ThrowIfHoleHasRenderFragmentParameter) and throws an actionable InvalidOperationException instructing the developer to remove the RenderFragment parameter or move the component outside the CacheBoundary.

Streaming

var renderBoundaryMarkers = allowBoundaryMarkers && componentState.StreamRendering;
var captureWriter = output as CacheBoundaryTextWriter;
if (captureWriter is not null && captureWriter.IsCapturing &&
    (!CacheBoundaryService.IsCacheableComponent(componentState.Component.GetType(), captureWriter.VaryBy) || renderBoundaryMarkers))
{
    captureWriter.PauseCapture();
    ...
    captureWriter.CreateHole(holeComponentType, holeBoundary?.RenderMode, holeCapture, ...);
}

While a CacheBoundary is capturing, every child it writes is inspected, and a streaming-rendered child (componentState.StreamRendering) is treated exactly like a [CacheBoundaryPolicy] hole: capture is paused and the component is recorded as a hole instead of being frozen into the cache entry. Consequently, streaming output is never cached — on every request (cache hit or miss) the hole re-renders live and streams its own updates, while only the static markup surrounding it is served from the cache. This keeps caching safe, because streaming content is inherently asynchronous and per-request and must not be captured into a shared entry.

Note: Output caching of a CacheBoundary that is itself rendered inside a streaming render context (i.e. underneath a [StreamRendering] ancestor) is not supported. In that case EndpointComponentState sets CacheBoundary.IsInStreamingContext, and CacheBoundaryService.PrepareAsync skips caching entirely, so the boundary's content always renders fresh. This only affects boundaries nested within a streaming subtree; a streaming child inside a CacheBoundary is still handled (as a hole, per above).

Fixes #55520
Fixes #65756

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dariatiurina
dariatiurina marked this pull request as ready for review April 15, 2026 18:02
@dariatiurina
dariatiurina requested a review from a team as a code owner April 15, 2026 18:02
Copilot AI review requested due to automatic review settings April 15, 2026 18:02
@dariatiurina dariatiurina self-assigned this Apr 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds <CacheComponent> support for Blazor SSR by capturing rendered HTML into a cache (with “hole” components that always re-render) and restoring cached output on subsequent requests.

Changes:

  • Introduces CacheComponent / NotCacheComponent plus key computation, JSON segment (“template + holes”) format, and an IMemoryCache-backed store.
  • Hooks SSR rendering to capture cacheable output and record hole segments during EndpointHtmlRenderer.WriteComponentHtml.
  • Adds unit tests (JSON, key resolver, hole classification, writer behavior) and E2E coverage via a new test page and endpoints for clearing/observing cache behavior.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Components/Endpoints/src/CacheComponent/CacheComponent.cs Public SSR cache component that restores cached HTML and reconstructs holes.
src/Components/Endpoints/src/CacheComponent/NotCacheComponent.cs Public opt-out marker that forces fresh rendering inside cached trees.
src/Components/Endpoints/src/CacheComponent/CacheComponentKeyResolver.cs Builds deterministic cache keys with optional vary-by dimensions.
src/Components/Endpoints/src/CacheComponent/CacheComponentJson.cs JSON serialization for cached HTML + hole segments.
src/Components/Endpoints/src/CacheComponent/CacheComponentStore.cs Store abstraction for cached JSON entries.
src/Components/Endpoints/src/CacheComponent/MemoryCacheComponentStore.cs MemoryCache-backed store honoring expiration/priority and size limit.
src/Components/Endpoints/src/CacheComponent/CacheStoreOptions.cs Internal options passed to the store when setting entries.
src/Components/Endpoints/src/CacheComponent/CacheComponentVaryBy.cs Internal flags used by hole classification logic.
src/Components/Endpoints/src/Rendering/CacheComponentTextWriter.cs TextWriter wrapper that captures HTML segments and injects hole segments.
src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs Resolves the cache store from DI for use during SSR rendering.
src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs Captures CacheComponent output on miss; pauses capture for hole components/boundaries.
src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs Adds CacheComponentSizeLimit option for MemoryCache sizing.
src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs Registers the default singleton cache store.
src/Components/Endpoints/src/PublicAPI.Unshipped.txt Declares new public API surface for shipping.
src/Components/Endpoints/test/CacheComponentJsonTest.cs Unit tests for segment serialization/deserialization.
src/Components/Endpoints/test/CacheComponentKeyResolverTest.cs Unit tests for key stability and vary-by dimensions.
src/Components/Endpoints/test/CacheComponentRenderTest.cs Unit tests for restore fallback/logging behavior.
src/Components/Endpoints/test/CacheComponentTextWriterTest.cs Unit tests for capture/pause/hole segment behavior.
src/Components/Endpoints/test/IsHoleComponentTest.cs Verifies which components are treated as holes.
src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CacheComponentTest/CacheComponentTest.razor SSR test page exercising caching, holes, nesting, and looped keys.
src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/CacheComponentTest/InnerCachedComponent.razor Test component used to validate nested-cache behavior.
src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs Adds test-only endpoints to clear cache and read render-count.
src/Components/test/E2ETest/Tests/CacheComponentTest.cs E2E validations for caching, holes, nesting, and loop behavior.

Comment thread src/Components/Endpoints/src/CacheBoundary/CacheBoundaryJson.cs Outdated
Comment thread src/Components/test/E2ETest/Tests/CacheBoundaryTest.cs Outdated
Comment thread src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs Outdated
Comment thread src/Components/Endpoints/src/CacheComponent/CacheComponent.cs Outdated

@javiercn javiercn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a good starting point!

There are a few design issues that we need to solve, but the general direction looks good.

One super important aspect that we need to take into account in this feature is performance. We should minimize the number of allocations (especially intermediate allocations) and leverage pooling as much as possible to minimize the impact of caching on the throughput.

I have more feedback and I still have to look at the tests, but want to make the current set of points available.

Comment thread src/Components/Endpoints/src/CacheComponent/NotCacheComponent.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/CacheStoreOptions.cs
Comment thread src/Components/Endpoints/src/CacheComponent/CacheComponentKeyResolver.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/CacheBoundaryStore.cs Outdated
Comment thread src/Components/Endpoints/src/CacheComponent/CacheComponentStore.cs Outdated
Comment thread src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs Outdated
Comment thread src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs Outdated
Comment thread src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs Outdated
Comment thread src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs Outdated
@dariatiurina dariatiurina added this to the 11.0-preview5 milestone Apr 23, 2026
dariatiurina and others added 2 commits April 24, 2026 10:41
Co-authored-by: Copilot <copilot@github.com>
@dariatiurina dariatiurina changed the title Cache Component support for Blazor CacheBoundary support for Blazor Apr 24, 2026
Comment thread src/Components/Endpoints/src/CacheBoundary/CacheBoundaryService.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/CacheBoundaryService.cs Outdated
Comment thread src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/HybridCacheBoundaryStore.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/MemoryCacheBoundaryStore.cs Outdated
Comment thread src/Components/Endpoints/src/Rendering/CacheBoundaryTextWriter.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/CacheBoundaryService.cs
Comment thread src/Components/Endpoints/src/CacheBoundary/CacheBoundaryService.cs Outdated
Comment thread src/Components/Endpoints/src/Rendering/CacheBoundaryTextWriter.cs
Comment thread src/Components/Endpoints/src/Rendering/EndpointComponentState.cs
Comment thread src/Components/Endpoints/src/CacheBoundary/CacheBoundary.cs Outdated
Comment thread src/Components/test/E2ETest/Tests/CacheBoundaryTestBase.cs
Comment thread src/Components/Endpoints/test/CacheBoundaryRenderTest.cs Outdated
@javiercn

Copy link
Copy Markdown
Member

The three main things that I see we should address are the duplicated stampede support, the public API, and the string allocation concerns. Check DistributedCacheTagHelper and other abstractions in this area for inspiration

@maraf maraf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 👍

Comment on lines +18 to +19
private static readonly ComponentParametersTypeCache _parametersTypeCache = new();
private static readonly ConcurrentDictionary<Type, CacheBoundaryPolicyAttribute?> _policyByComponentType = new();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the service is a singleton, is there a reason to split between "static" and "instance" members?

Comment thread src/Components/Endpoints/src/Rendering/CacheBoundaryTextWriter.cs Outdated
Comment thread src/Components/Components/src/CacheBoundaryPolicyAttribute.cs Outdated
Comment thread src/Components/Web/src/Head/HeadOutlet.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/MemoryCacheBoundaryStore.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/MemoryCacheBoundaryStore.cs Outdated

@javiercn javiercn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for pushing this through!

@dariatiurina
dariatiurina enabled auto-merge (squash) July 9, 2026 17:12
@dariatiurina
dariatiurina merged commit d1866a2 into dotnet:main Jul 10, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CacheBoundary Add a Blazor Cache component

6 participants