.NET 11 Preview 7 introduces CacheView for caching the rendered HTML of a Blazor static-SSR subtree. On a hit, Blazor replays the stored markup without instantiating the child components or running their lifecycle. That can remove database calls, allocations, and rendering work, but it also turns the cache key into a data-isolation boundary. The safe implementation is not “wrap an expensive component and choose ten minutes.” It is to define who may share the exact same HTML, encode every changing input in the key, keep request-specific fragments live, and prove the boundary with concurrent HTTP tests.
Table of Contents
Start with the actual CacheView contract
CacheView caches output during static server-side rendering. On a miss, Blazor renders the child subtree and stores the resulting markup. On a hit, it replays that markup; constructors, dependency resolution, lifecycle methods, queries, and render work inside the cached subtree do not run again. This is output caching at a component boundary, not data caching.
Caching is skipped for non-GET requests, when Enabled="false", and when the CacheView itself is inside a streaming-rendering subtree. A streaming child can remain live inside an otherwise cached boundary, but the stored part is still static SSR markup. Interactive state after hydration is a separate concern.
The default expiration is 30 seconds when no expiration parameter is supplied. That default prevents entries from living forever, but it does not make an incomplete key safe. If two requests are not allowed to receive byte-for-byte equivalent HTML, they must not share an entry even for one second.
Build one narrow cache boundary
A good first candidate is public, read-only, expensive to render, and governed by a clear freshness rule: a catalog summary, documentation navigation tree, public pricing fragment, or precomputed statistics panel. Avoid the authenticated layout, an entire page, and any fragment that mixes user data with public content.
@page "/catalog"
@using Microsoft.AspNetCore.Components
<CacheView CacheKey="catalog-results"
VaryByQuery="category,page"
VaryByCulture="true"
VaryBy="@ContentRevision"
ExpiresAfter="TimeSpan.FromMinutes(5)">
<section data-render-id="@RenderProbe.Next()">
<CatalogResults Category="@Category" Page="@Page" />
</section>
</CacheView>
@code {
[SupplyParameterFromQuery(Name = "category")]
public string Category { get; set; } = "all";
[SupplyParameterFromQuery(Name = "page")]
public int Page { get; set; } = 1;
[Inject] public CatalogRenderProbe RenderProbe { get; set; } = default!;
private const string ContentRevision = "catalog-v42";
}
The render marker is useful in an integration environment because it proves that identical requests reuse markup. In a production component, remove the diagnostic marker or expose only non-sensitive metrics. The content revision gives a release or publishing workflow a deterministic way to stop sharing old entries without waiting for the five-minute lifetime.
Make the cache key complete and bounded
Preview 7 can vary by query, route values, headers, cookies, authenticated user, culture, and an arbitrary application string. CacheKey disambiguates boundaries at the same render-tree position. It is especially important when a loop renders several CacheView instances; without a unique key for each instance, Blazor detects the collision and throws rather than letting entries overwrite one another.
VaryByRoutecovers entity identifiers carried in the route.VaryByQuerycovers filters, sorting, paging, and view modes; use*only when every query parameter truly changes output.VaryByCulturecovers resources and culture-sensitive formatting.VaryByUserseparates authenticated identities, but does not automatically cover tenant, role, entitlement, or experiment state.VaryBycan carry a normalized tenant tier, feature-set version, or content revision.
A key must be complete and bounded. Raw search text, arbitrary headers, and attacker-controlled cookies can create unbounded cardinality and consume the cache budget. Normalize values into a small approved set, reject invalid dimensions before rendering, and avoid caching one-off queries that will never hit again. Expiration limits time; cardinality control limits memory and distributed-store churn.
Keep per-request components out of stored HTML
Component authors can declare how a component behaves inside a cache boundary. CacheBehavior.Rerender creates a live hole that runs on every request. CacheBehavior.Throw rejects unsafe placement. A CacheCondition can make that placement valid only when the enclosing cache varies by a required request dimension.
[CacheBehavior(CacheBehavior.Rerender)]
public sealed class CurrentRequestTime : ComponentBase
{
}
[CacheBehavior(CacheBehavior.Throw)]
[CacheCondition(CacheVaryBy.User)]
public sealed class UserSubscriptionBadge : ComponentBase
{
}
Built-in safeguards fail closed for common cases. AuthorizeView requires VaryByUser="true"; QuickGrid requires query variation; Virtualize cannot be placed inside CacheView. Antiforgery tokens, HeadOutlet, interactive render-mode boundaries, and streaming children render fresh while surrounding output can remain cached.
There is a subtle limitation: a rerendered component’s parameter values are captured when the entry is created and replayed on hits. If a live user greeting receives the current user name as a normal parameter, it can rerender for Bob with Alice’s captured parameter. Vary by user or move the component outside the cache. Live components with RenderFragment or RenderFragment<T> parameters are not supported because the captured fragment cannot be safely replayed later.
For the authentication side of this decision, see Blazor Server App Authentication with Entra ID. A correct sign-in flow identifies the user; it does not prove that shared cached markup is isolated correctly.
Choose memory or HybridCache deliberately
The default store is in memory and is bounded by RazorComponentsServiceOptions.CacheViewSizeLimit, whose documented default is 100 MB. A value of zero prevents new entries. Start with an explicit budget and observe eviction and hit rate instead of accepting the process-wide default without knowing how much HTML the application produces.
builder.Services.AddSingleton<CatalogRenderProbe>();
builder.Services.AddRazorComponents(options =>
{
options.CacheViewSizeLimit = 50 * 1024 * 1024;
})
.AddInteractiveServerComponents();
// Optional: when registered, CacheView uses HybridCache automatically.
builder.Services.AddHybridCache();
HybridCache can provide local and distributed tiers across application instances. That may improve hit rate after load balancing, but it also distributes a wrong-key incident to more users. Sliding expiration is not supported with HybridCache; use ExpiresAfter or ExpiresOn. Concurrent requests for the same key are coalesced so one request creates the entry, which reduces stampedes but makes the first-render path especially important to test.
Treat expiration and invalidation as content policy
ExpiresAfter sets an absolute lifetime from creation, ExpiresOn sets an absolute point in time, and ExpiresSliding extends an in-memory entry while it remains active. Choose from the content contract. A public product description may tolerate minutes; entitlement, availability, inventory, or a security decision may require a much shorter window or no rendered-output cache at all.
Expiration is a fallback, not precise invalidation. When a publishing or deployment event makes old HTML wrong immediately, change an application-owned revision in VaryBy. This creates a new key namespace without depending on store-specific removal APIs. It also gives rollback a clear choice: restore the prior revision or advance to another known value.
Prove hits and isolation through HTTP
Component tests are not enough because the risk crosses requests. Exercise the real endpoint with WebApplicationFactory, extract the render marker, and assert both reuse and separation. The test below assumes the diagnostic data-render-id from the earlier component is enabled only in the integration environment.
public sealed class CatalogCacheViewTests(
WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client = factory.CreateClient();
[Fact]
public async Task Same_key_hits_but_different_query_is_isolated()
{
string books1 = await RenderId("/catalog?category=books&page=1");
string books2 = await RenderId("/catalog?category=books&page=1");
string games = await RenderId("/catalog?category=games&page=1");
Assert.Equal(books1, books2); // cache hit
Assert.NotEqual(books1, games); // different key dimension
}
private async Task<string> RenderId(string url)
{
string html = await _client.GetStringAsync(url);
Match match = Regex.Match(html, "data-render-id=\\\"([^\\\"]+)\\\"");
Assert.True(match.Success, "Expected the integration render marker.");
return match.Groups[1].Value;
}
}
Extend the suite one dimension at a time: page, route value, culture, anonymous versus authenticated, user A versus user B, tenant, feature set, and content revision. Send concurrent identical requests and confirm one render marker; send concurrent different-key requests and confirm no marker crosses the boundary. For sensitive content, assert the absence of the other user’s unique canary text, not only that the IDs differ.
Account for Preview 7 limitations
CacheViewis a .NET 11 Preview 7 feature; API and behavior can change before general availability.- A
CacheViewcannot be nested inside anotherCacheView. - The component caches static SSR output only for
GETrequests. - A cache boundary inside a streaming subtree is skipped.
- Live component parameters are captured on entry creation.
- Live components cannot accept render-fragment parameters inside the boundary.
ExpiresSlidingis unavailable withHybridCache.
Pin the Preview SDK in global.json, keep the first adoption behind an application switch, and rerun isolation tests on every Preview update. Do not copy Preview code into a production LTS branch merely because the component name looks final.
Production adoption checklist
- Select one public, read-only, expensive static-SSR fragment.
- List every value that can change its HTML.
- Map each value to a bounded vary-by dimension or move it outside the cache.
- Assign
CacheKeywhere repeated render-tree positions can collide. - Declare rerender, throw, and conditional behavior for reusable custom components.
- Set an explicit memory budget and a content-specific expiration.
- Use a content revision when an event must invalidate old output immediately.
- Prove a hit, every separation dimension, concurrency, and sensitive-marker isolation through HTTP.
- Canary the Preview feature and retain a switch that renders normally.
CacheView can remove real server work, but performance is the second result. The first result must be proof that every request sharing an entry is entitled to receive exactly the same stored HTML.