CacheBoundary support for Blazor#65772
Conversation
d61f98f to
20c296c
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
20c296c to
ace51a8
Compare
There was a problem hiding this comment.
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/NotCacheComponentplus key computation, JSON segment (“template + holes”) format, and anIMemoryCache-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. |
javiercn
left a comment
There was a problem hiding this comment.
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.
|
|
||
| namespace Microsoft.AspNetCore.Components.Endpoints; | ||
|
|
||
| internal readonly struct CacheStoreOptions |
There was a problem hiding this comment.
Are these options lifted from the MVC implementation?
| /// <summary> | ||
| /// Provides a store for caching rendered component output as a JSON template-with-holes representation. | ||
| /// </summary> | ||
| internal abstract class CacheComponentStore : IDisposable |
There was a problem hiding this comment.
If there is no shared logic at all here, it might as well be an interface
There was a problem hiding this comment.
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
There was a problem hiding this comment.
HybridCache was discussed in the design doc. The main problem is sync lifecycle of CacheBoundary.
CacheBoundary support for Blazor
Description
Introduces
CacheBoundaryandNotCacheBoundary— two new Blazor components that enable output caching of server-side rendered (SSR) component subtrees. On cache hit, child components inside aCacheBoundaryare 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 cacheCacheKey— explicit key for disambiguation when multipleCacheBoundaryinstances share the same parentEnabled— toggle caching on/off (defaulttrue)ExpiresAfter/ExpiresOn/ExpiresSliding— expiration policiesPriority—CacheItemPriorityfor the cache entryVaryByQuery,VaryByRoute,VaryByHeader,VaryByCookie— vary cache by comma-separated request dimension namesVaryByUser— vary by authenticated identityVaryByCulture— vary by current culture/UI cultureVaryBy— arbitrary custom string to vary byNotCacheBoundary(Microsoft.AspNetCore.Components.NotCacheBoundary) — marker component that opts child content out of an enclosingCacheBoundary'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 enclosingCacheBoundary. WhenExcluded = true, the component becomes a "hole" in the cached output. The optionalVaryByproperty (aCacheBoundaryVaryByflags 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):htmlsegments — raw HTML markup that was renderedholesegments — components that must be re-rendered on every request (e.g., auth views,NotCacheBoundary, interactive render mode boundaries, sections)On cache hit,
CacheBoundary.BuildRenderTreereplays HTML segments asMarkupContentand re-opens hole components with fresh parameters sourced from the currentChildContentrender tree.Hole detection (
IsHoleComponent)The renderer uses an attribute-based approach: any component decorated with
[CacheBoundaryPolicy(Excluded = true)]is treated as a hole. TheVaryByproperty on the attribute enables conditional exclusion — for example,AuthorizeViewCoreis marked[CacheBoundaryPolicy(Excluded = true, VaryBy = CacheBoundaryVaryBy.User)], so it becomes a hole only when the enclosingCacheBoundarydoes not vary by user.Built-in components marked as holes:
AuthorizeViewCore(conditionally — excluded unlessVaryByUseris enabled on theCacheBoundary, viaVaryBy = CacheBoundaryVaryBy.User)NotCacheBoundarySSRRenderModeBoundarySectionOutlet,SectionContentEditForm,InputBase<TValue>,ValidationMessage<TValue>,ValidationSummaryAntiforgeryTokenHeadOutletComponents with streaming rendering boundaries are also treated as holes.
Renderer integration
EndpointHtmlRenderer.WriteComponentHtmlis extended to:TextWriterin aCacheBoundaryTextWriterthat captures HTML while still writing it through to the response, then serialize and store the captured segments.CacheBoundary.BuildRenderTreewhich replays the cached template.IsHoleComponent.Infrastructure
CacheBoundaryKeyResolver— computes a deterministic SHA-256 cache key from the component's tree position key,CacheKey, and allVaryBy*dimension values resolved from the currentHttpContext.ICacheBoundaryStore/MemoryCacheBoundaryStore— pluggable cache backend; default usesMemoryCachewith a configurable size limit.CacheStoreOptions— passes expiration and priority settings from the component to the store.CacheBoundaryTextWriter— aTextWriterdecorator that tee-writes to both the response and an internal segment buffer, with pause/resume support for creating holes.EndpointComponentState— assigns aTreePositionKeyFactoryto eachCacheBoundaryinstance, producing a key from the parent component type,CacheBoundarytype, the sequence number within the parent's render tree, and any@keydirective.DI registration
ICacheBoundaryStoreis registered as a singleton (MemoryCacheBoundaryStore) inAddRazorComponents.Testing
Unit tests (5 new test files):
CacheBoundaryJsonTest— serialization/deserialization round-trips for HTML, holes, keys (int, string, Guid), special characters, and error casesCacheBoundaryKeyResolverTest— deterministic key generation, VaryBy* dimension isolation, collision resistance between different dimensionsCacheBoundaryTextWriterTest— capture/pause/resume lifecycle, hole creation, forwarding to inner writerCacheBoundaryRenderTest— fallback to fresh render when dependencies are missing or cached data is corruptIsHoleComponentTest— hole classification for all known component types includingAuthorizeViewwith/withoutVaryByUserE2E tests (
CacheBoundaryTest):Enabled=falsedisables cachingNotCacheBoundary) remain functional across cache hitsCacheBoundarydoes not re-execute inner component on outer cache hitCacheKeyenables distinct cache entries in loopsFixes #55520
Fixes #65756