Skip to content

CacheBoundary support for Blazor#65772

Open
dariatiurina wants to merge 7 commits intodotnet:mainfrom
dariatiurina:55520-cache
Open

CacheBoundary support for Blazor#65772
dariatiurina wants to merge 7 commits intodotnet:mainfrom
dariatiurina:55520-cache

Conversation

@dariatiurina
Copy link
Copy Markdown
Contributor

@dariatiurina dariatiurina commented Mar 13, 2026

CacheBoundary support for Blazor

Description

Introduces CacheBoundary and NotCacheBoundary — two new Blazor components that enable output caching of server-side rendered (SSR) component subtrees. On cache hit, child components inside a CacheBoundary are not instantiated or rendered, and the previously captured HTML is replayed directly.

For more details see design document.

Changes

New public API

  • CacheBoundary (Microsoft.AspNetCore.Components.CacheBoundary) — wraps child content and caches its rendered HTML during SSR. Supports the following 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
    • PriorityCacheItemPriority for the cache entry
    • VaryByQuery, VaryByRoute, VaryByHeader, VaryByCookie — vary cache by comma-separated request dimension names
    • VaryByUser — vary by authenticated identity
    • VaryByCulture — vary by current culture/UI culture
    • VaryBy — arbitrary custom string to vary by
  • NotCacheBoundary (Microsoft.AspNetCore.Components.NotCacheBoundary) — marker component that opts child content out of an enclosing CacheBoundary's cache. Decorated with [CacheBoundaryPolicy(Excluded = true)], its content is recorded as a "hole" in the cached template and always re-rendered.

  • CacheBoundaryPolicyAttribute (Microsoft.AspNetCore.Components.CacheBoundaryPolicyAttribute) — attribute that controls how a component interacts with an enclosing CacheBoundary. When Excluded = true, the component becomes a "hole" in the cached output. The optional VaryBy property (a CacheBoundaryVaryBy flags enum) lifts the exclusion when the cache boundary varies by all specified dimensions.

  • CacheBoundaryVaryBy (Microsoft.AspNetCore.Components.CacheBoundaryVaryBy) — a [Flags] enum describing which vary-by dimensions are active: None, Query, Route, Header, Cookie, User, Culture.

  • RazorComponentsServiceOptions.CacheBoundarySizeLimit — configurable maximum size (bytes) for the in-memory cache (default 100 MB).

Cache format: template-with-holes

Cached output is serialized as a JSON array of segments (CacheBoundaryJson):

  • html segments — raw HTML markup that was rendered
  • hole segments — components that must be re-rendered on every request (e.g., auth views, NotCacheBoundary, interactive render mode boundaries, sections)

On cache hit, CacheBoundary.BuildRenderTree replays HTML segments as MarkupContent and re-opens hole components with fresh parameters sourced from the current ChildContent render tree.

Hole detection (IsHoleComponent)

The renderer uses an attribute-based approach: any component decorated with [CacheBoundaryPolicy(Excluded = true)] is treated as a hole. The VaryBy property on the attribute enables conditional exclusion — for example, AuthorizeViewCore is marked [CacheBoundaryPolicy(Excluded = true, VaryBy = CacheBoundaryVaryBy.User)], so it becomes a hole only when the enclosing CacheBoundary does not vary by user.

Built-in components marked as holes:

  • AuthorizeViewCore (conditionally — excluded unless VaryByUser is enabled on the CacheBoundary, via VaryBy = CacheBoundaryVaryBy.User)
  • NotCacheBoundary
  • SSRRenderModeBoundary
  • SectionOutlet, SectionContent
  • EditForm, InputBase<TValue>, ValidationMessage<TValue>, ValidationSummary
  • AntiforgeryToken
  • HeadOutlet

Components with streaming rendering boundaries are also treated as holes.

Renderer integration

EndpointHtmlRenderer.WriteComponentHtml is extended to:

  1. On cache miss with caching enabled: wrap the output TextWriter in a CacheBoundaryTextWriter that captures HTML while still writing it through to the response, then serialize and store the captured segments.
  2. On cache hit: delegate to CacheBoundary.BuildRenderTree which replays the cached template.
  3. When writing child components inside a cache capture: automatically pause capture and create holes for components identified by IsHoleComponent.

Infrastructure

  • CacheBoundaryKeyResolver — computes a deterministic SHA-256 cache key from the component's tree position key, CacheKey, and all VaryBy* dimension values resolved from the current HttpContext.
  • ICacheBoundaryStore / MemoryCacheBoundaryStore — pluggable cache backend; default uses MemoryCache with a configurable size limit.
  • CacheStoreOptions — passes expiration and priority settings from the component to the store.
  • CacheBoundaryTextWriter — a TextWriter decorator that tee-writes to both the response and an internal segment buffer, with pause/resume support for creating holes.
  • EndpointComponentState — assigns a TreePositionKeyFactory to each CacheBoundary instance, producing a key from the parent component type, CacheBoundary type, the sequence number within the parent's render tree, and any @key directive.

DI registration

ICacheBoundaryStore is registered as a singleton (MemoryCacheBoundaryStore) in AddRazorComponents.

Testing

  • Unit tests (5 new test files):

    • CacheBoundaryJsonTest — serialization/deserialization round-trips for HTML, holes, keys (int, string, Guid), special characters, and error cases
    • CacheBoundaryKeyResolverTest — deterministic key generation, VaryBy* dimension isolation, collision resistance between different dimensions
    • CacheBoundaryTextWriterTest — capture/pause/resume lifecycle, hole creation, forwarding to inner writer
    • CacheBoundaryRenderTest — fallback to fresh render when dependencies are missing or cached data is corrupt
    • IsHoleComponentTest — hole classification for all known component types including AuthorizeView with/without VaryByUser
  • E2E tests (CacheBoundaryTest):

    • Verifies cached content persists across navigations while non-cached content changes
    • Verifies Enabled=false disables caching
    • Verifies holes (forms, NotCacheBoundary) remain functional across cache hits
    • Verifies nested CacheBoundary does not re-execute inner component on outer cache hit
    • Verifies CacheKey enables distinct cache entries in loops

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
Comment thread src/Components/test/E2ETest/Tests/CacheBoundaryTest.cs
Comment thread src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceOptions.cs Outdated
Comment thread src/Components/Endpoints/src/CacheComponent/CacheComponent.cs Outdated
Copy link
Copy Markdown
Member

@javiercn javiercn left a comment

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

namespace Microsoft.AspNetCore.Components.Endpoints;

internal readonly struct CacheStoreOptions
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.

Are these options lifted from the MVC implementation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes

Comment thread src/Components/Endpoints/src/CacheComponent/CacheComponentKeyResolver.cs Outdated
Comment thread src/Components/Endpoints/src/CacheBoundary/CacheBoundaryStore.cs Outdated
/// <summary>
/// Provides a store for caching rendered component output as a JSON template-with-holes representation.
/// </summary>
internal abstract class CacheComponentStore : IDisposable
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.

If there is no shared logic at all here, it might as well be an interface

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.

I see now there is only MemoryCacheComponentStore. I would have expected to see an implementation with HybridCache. See how PersistentComponentState sets up a similar set of options for the caching backend

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

HybridCache was discussed in the design doc. The main problem is sync lifecycle of CacheBoundary.

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
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

3 participants