Skip to content

Fix TempData and SupplyParameterFromSession persistence for streaming SSR case#66832

Open
dariatiurina wants to merge 17 commits into
dotnet:mainfrom
dariatiurina:66745-streamingssr-tempdata-sessiondata
Open

Fix TempData and SupplyParameterFromSession persistence for streaming SSR case#66832
dariatiurina wants to merge 17 commits into
dotnet:mainfrom
dariatiurina:66745-streamingssr-tempdata-sessiondata

Conversation

@dariatiurina

@dariatiurina dariatiurina commented May 25, 2026

Copy link
Copy Markdown
Contributor

Fix TempData and SupplyParameterFromSession persistence for streaming SSR case

Summary

Fixes a bug where [SupplyParameterFromSession], [SupplyParameterFromTempData], and the ITempData cascading parameter values set during streaming SSR were silently lost. Persistence previously relied on Response.OnStarting callbacks, which fire just before the first response chunk is flushed — so any values modified later (after an await in OnInitializedAsync, while streaming chunks are being sent) were never written.

The fix moves persistence to run after all rendering (including streaming) completes, and pre-issues the session cookie before streaming begins so that Set-Cookie headers are not blocked by Response.HasStarted.

Why I think it's acceptable to always have session established, when customers set session in their app, because it will be established anyway in most cases and it is not a big memory or performance hit.

Changes

  • RazorComponentEndpointInvoker.cs

    • New TryRegisterSessionEstablishment(HttpContext) registers a Response.OnStarting callback that performs a no-op session.Set(...) + session.Remove(...) on a sentinel key (__AspNetCore.Components.Endpoints.SessionEstablishment). This flips the session middleware's establishment gate so the session cookie is issued before the first response chunk flushes — enabling server-side session writes to succeed after streaming begins. No-op when session middleware is not registered.
    • The above is invoked after services are initialized and before any streaming output.
    • After streaming/rendering completes (and before persisted component state is emitted), the invoker now explicitly calls:
      1. SessionCascadingValueSupplier.PersistAllValues() (when the supplier is registered)
      2. TempDataProviderServiceCollectionExtensions.PersistTempData(context)
  • SessionCascadingValueSupplier.cs — Removed the _onStartingRegistered field and the Response.OnStarting(PersistAllValues) registration that used to happen inside CreateSubscription. Persistence is now driven explicitly by the invoker after rendering completes.

  • TempDataProviderServiceCollectionExtensions.cs

    • Removed the Response.OnStarting(...) callback that used to call tempDataService.Save in GetOrCreateTempData.
    • Added PersistTempData(HttpContext) — retrieves the per-request ITempData from HttpContext.Items and calls TempDataService.Save. If the active ITempDataProvider is CookieTempDataProvider and Response.HasStarted is true, the method logs a warning (new CookieTempDataNotPersistedAfterResponseStarted event) and returns without saving, since cookie TempData cannot work once response headers are frozen.
    • The class is now partial to host the LoggerMessage source-generated logger.
  • TempDataService.csSave now accepts ITempData instead of the concrete TempData type, using pattern matching (tempData is not TempData concrete || !concrete.WasLoaded) to guard the Save() call. This makes the signature match the new persistence entry point.

Testing

  • New E2E tests in StreamingSessionPersistenceTest.cs (uses RazorComponentEndpointsNoInteractivityStartup, enables --UseSessionStorageTempDataProvider=true and --UseSession=true, and clears the .AspNetCore.Session cookie per test):

    • StreamingSSR_PersistsSupplyParameterFromSession_AfterAsyncRendering
    • StreamingSSR_PersistsSupplyParameterFromTempData_AfterAsyncRendering
    • StreamingSSR_PersistsTempDataCascadingParameter_AfterAsyncRendering
  • New negative E2E test in TempDataCookieTest.cs:

    • StreamingSSR_CookieTempData_DoesNotPersistValuesWrittenAfterFirstFlush — asserts that cookie-based TempData written during streaming is not persisted (expected behavior — the new warning is logged).
  • New test page StreamingSessionPersistence.razor ([StreamRendering]) — writes a [SupplyParameterFromSession] value, a [SupplyParameterFromTempData] value, and an ITempData cascading parameter value after await Task.Delay(100) in OnInitializedAsync. The await is intentional: it puts the writes after the streaming phase has begun, which is the scenario that used to silently drop values.

  • Updated unit tests

    • SessionCascadingValueSupplierTest — added PersistAllValues_KeepsKey_WhenCallbackReturnsValue; renamed SetRequestContext_DoesNotRegisterOnStarting_UntilSubscriptionCreated to SetRequestContext_DoesNotPersist_UntilExplicitlyCalled to reflect the new explicit-call model.
    • SessionSubscriptionTest.CreateSubscription_RegistersValueCallbackAndReturnsSubscription — replaced the FireOnStartingAsync() step with a direct await _supplier.PersistAllValues().
    • SessionStorageTempDataProviderTest.Save_RemovesSessionKey_WhenNoDataToSave renamed to Save_RemovesSessionEntry_WhenNoDataToSave and now also asserts that reloading yields empty TempData.

Behavior Change to Call Out

The session cookie (.AspNetCore.Session) is now always issued on responses served by the Razor Components endpoint when session middleware is registered, even if the user code never calls ISession.Set(...) or writes a [SupplyParameterFromSession] / TempData value. This is the deliberate effect of TryRegisterSessionEstablishment: it flips the session middleware's establishment gate during OnStarting (via a no-op Set+Remove) so that subsequent session writes performed after streaming begins can still be persisted. Apps that previously never received a session cookie from these endpoints will now receive an empty session cookie.

Note on cookie-based TempData

Cookie-based TempData fundamentally cannot work in streaming SSR — Set-Cookie headers cannot be written after the response body has started. The new warning (CookieTempDataNotPersistedAfterResponseStarted) makes this explicit and recommends switching to TempDataProviderType.SessionStorage via RazorComponentsServiceOptions. Session-backed persistence ([SupplyParameterFromSession] and session-based TempData) works correctly because it is server-side and the new session-establishment callback ensures the session cookie is issued before streaming begins.

Fixes #66745

@github-actions github-actions Bot added the area-blazor Includes: Blazor, Razor Components label May 25, 2026
@dariatiurina dariatiurina self-assigned this May 25, 2026
@dariatiurina dariatiurina added this to the 11.0-preview6 milestone May 25, 2026
@dariatiurina dariatiurina marked this pull request as ready for review May 26, 2026 09:33
Copilot AI review requested due to automatic review settings May 26, 2026 09:33
@dariatiurina dariatiurina requested a review from a team as a code owner May 26, 2026 09:33

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

This PR fixes persistence timing for [SupplyParameterFromSession] and TempData during streaming server-side rendering (SSR) by moving persistence from HttpResponse.OnStarting callbacks to explicit persistence calls made after rendering/streaming completes.

Changes:

  • Added explicit post-render persistence calls for session-supplied cascading values and TempData in RazorComponentEndpointInvoker.
  • Removed Response.OnStarting-based persistence registration from SessionCascadingValueSupplier and TempData creation.
  • Updated session-related unit tests to call PersistAllValues() explicitly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs Adds explicit session + TempData persistence after rendering/streaming completes.
src/Components/Endpoints/src/SessionCascadingValueSupplier.cs Removes OnStarting registration so persistence is driven externally.
src/Components/Endpoints/src/TempData/TempDataProviderServiceCollectionExtensions.cs Removes OnStarting persistence and adds PersistTempData(HttpContext) helper.
src/Components/Endpoints/test/Session/SessionSubscriptionTest.cs Updates subscription test to use explicit persistence instead of firing OnStarting.
src/Components/Endpoints/test/Session/SessionCascadingValueSupplierTest.cs Renames/updates test to validate explicit persistence behavior.

Comment thread src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs
Comment thread src/Components/Endpoints/test/Session/SessionCascadingValueSupplierTest.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@dariatiurina dariatiurina requested a review from ilonatommy June 1, 2026 10:41

@ilonatommy ilonatommy 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.

I will divide my review into smaller parts:

  1. Temp data persistance happens too early.
  2. Session storage persistance happens too early.

The comments focus on 1) for now.

Comment thread src/Components/Endpoints/src/TempData/TempDataCascadingValueSupplier.cs Outdated

@ilonatommy ilonatommy 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.

One more pass for TempData

Comment thread src/Components/Endpoints/src/TempData/SessionStorageTempDataProvider.cs Outdated
Comment thread src/Components/Endpoints/src/TempData/SessionStorageTempDataProvider.cs Outdated
Comment thread src/Components/Endpoints/src/TempData/SessionStorageTempDataProvider.cs Outdated

@ilonatommy ilonatommy 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.

Partial review.

Comment thread src/Components/Endpoints/src/DependencyInjection/TempDataService.cs Outdated
Comment thread src/Components/Endpoints/src/DependencyInjection/TempDataService.cs Outdated
// so that TempData / [SupplyParameterFromSession] values written during async
// rendering can still be persisted after Response.HasStarted. No-op when session
// middleware is not registered.
TryRegisterSessionEstablishment(context);

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.

This now runs for every component endpoint even if the endpoint doesn't use the supply-param storage. We could avoid it by calling it in subscription methods. We can move TryRegisterSessionEstablishment to SessionCascadingValueSupplier. Sessions supplier's responsibility is connected with session establishment so a method releasing cookie matches that responsibility.

I'm not sure though if it won't be too late, please check the case of streaming deferred children. Add a test: SSR page with streaming that renders a streaming child component. That child starts streaming in OnInitializedAsync and holds SupplyParameterFromTempData.

In case that scenario would fail, please try to think how to avoid over-releasing the cookie, we don't want behavioral change for endpoints not using this feature.

I was considering if we should prevent this call in non-streaming scenarios but there's no need if we already move it to subscription. The cookie must be released in these cases anyway so it's fine to do it a bit earlier.

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.

I moved cookie creation to GetOrCreateTempData and CreateSubscription. Now only when TempData and [SupplyParameterFromSession] are registered and called, we create cookie for session. This does break case, when component renders in the streaming context (see StreamingSSR_DeferredChildSubscription_DoesNotPersistSession_OnFirstRequest test that was added in this PR). This is much less of a problem then enabling cookies for every page, when users enable session for their application.

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.

This does break case, when component renders in the streaming context

If we continue trying to have this feature working in streaming then we have to clearly define what scenarios are supported. Docs need clear information when it's safe to use the parameter or prevent them from using the parameter in scenarios that are silently no-op.

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.

Yep. Compare the behavior against MVC/RazorPages after await FlushAsync() on a razor page / mvc view

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.

@javiercn From my investigation MVC TempData just silently skips persisting elements after response has started.

@dariatiurina

Copy link
Copy Markdown
Contributor Author

@ilonatommy Here are options to consider for our problem. I am open to dropping support based on what MVC does and just improve handling (e.g. right now on the main we will encounter runtime error and we should log a warning in that case).

How streaming SSR gets enabled

Streaming is opt-in per component (subtree) via [StreamRendering] (Microsoft.AspNetCore.Components.StreamRenderingAttribute). A streamed patch is only actually emitted when the component's async lifecycle suspends on an incomplete Task (typically the first real await in OnInitializedAsync / OnParametersSetAsync). The five ways a component ends up streaming:

  1. On a @page[StreamRendering] on a routable page.
  2. On a non-routable component[StreamRendering] on a child component, including components shipped from a Razor Class Library; only that subtree streams.
  3. Inherited from a streaming ancestor — a descendant without the attribute automatically streams if any ancestor does.
  4. Re-enabled in a subtree — a descendant under a non-streaming ancestor can opt in with [StreamRendering] directly.
  5. Under prerendering — streaming also governs the prerender pass of components in Interactive Server / WebAssembly / Auto render modes.

Opt out for a subtree with [StreamRendering(false)].

The problem with session-backed state

[SupplyParameterFromSession], [SupplyParameterFromTempData], and the cascading ITempData TempData parameter all ultimately need the session cookie (.AspNetCore.Session) to reach the client. The cookie is installed via an OnStarting callback, which runs immediately before the response headers go out. Once the response has started flushing — i.e. once a streaming component has suspended on its first incomplete await — no further Set-Cookie headers can be added.

The fix lives in SessionEstablishmentHelper.TryRegisterSessionEstablishment, which preemptively registers an OnStarting callback that touches the session (forcing the cookie). It is called from two places:

  • SessionCascadingValueSupplier.CreateSubscription — when a [SupplyParameterFromSession] parameter is first subscribed.
  • TempDataProviderServiceCollectionExtensions.GetOrCreateTempData — when the cascading ITempData value is first resolved, only when the configured provider is SessionStorageTempDataProvider.

The helper short-circuits when the response has already started:

if (session is null || context.Response.HasStarted)
{
    return;
}

So persistence works only when the subscription / cascading-value resolution happens before the first streaming flush.

Supported

Declaring [SupplyParameterFromSession], [SupplyParameterFromTempData], or accepting the cascading ITempData on a component that is part of the initial render tree is supported. The subscription / cascading-value resolution runs synchronously before the first await, the cookie callback is registered in time, and values written after the await (during streaming) are persisted at end-of-request.

This is what StreamingSessionPersistence.razor + StreamingSSR_Persists*_AfterAsyncRendering cover:

@page "/streaming-session-persistence"
@attribute [StreamRendering]

@code {
    [SupplyParameterFromSession] public string? Email { get; set; }
    [SupplyParameterFromTempData] private string SupplyParameterFromTempDataValue { get; set; } = string.Empty;
    [CascadingParameter] public ITempData? TempData { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);   // first flush happens here

        Email = "set-during-streaming";
        SupplyParameterFromTempDataValue = "tempdata-set-during-streaming";
        TempData!["Message"] = "streaming-tempdata-message";
    }
}

Non-streaming components are trivially supported as well — the response is buffered, so the cookie callback always fires in time.

Not supported

If the first use of session-backed state only happens in a part of the tree that materializes after the first flush (e.g. a child component conditionally rendered after the parent's await), TryRegisterSessionEstablishment short-circuits on Response.HasStarted, no cookie is issued, and the value is silently lost on the next request.

This is what StreamingParentWithDeferredChild.razor + StreamingSSR_DeferredChildSubscription_DoesNotPersistSession_OnFirstRequest cover:

@* Parent — streaming; child only mounts after the first await *@
@page "/streaming-parent-with-deferred-child"
@attribute [StreamRendering]

@if (_done) { <StreamingDeferredSessionChild /> } else { <p>Streaming...</p> }

@code {
    bool _done;
    protected override async Task OnInitializedAsync() { await Task.Delay(50); _done = true; }
}
@* Child — its [SupplyParameterFromSession] is only subscribed AFTER the parent flushed *@
@code {
    [SupplyParameterFromSession] public string? DeferredChildEmail { get; set; }
    protected override async Task OnInitializedAsync() { /* write after own await */ }
}

Same limitation applies to a TempData cascading value first resolved in a deferred subtree.

How MVC handles the same problem

MVC has the same HTTP constraint and the same failure mode: if TempData is mutated after the response has started (e.g. after await Response.Body.FlushAsync() in a view), the mutation is silently dropped for both the cookie and session-backed providers. MVC emits no warning and no log — it's treated as a programmer responsibility.

Guidance for users about the deferred-subscription limitation

  1. Documentation — call out the limitation in the official docs for [SupplyParameterFromSession], [SupplyParameterFromTempData], and the cascading ITempData parameter, with a pointer from the streaming SSR docs.
  2. Advisory analyzer — a Roslyn analyzer could surface an informational diagnostic when [SupplyParameterFromSession], [SupplyParameterFromTempData], or the cascading ITempData parameter is used in a component that is, or sits under, [StreamRendering]. The diagnostic would not claim a bug; it would tell the user that this combination is supported only when the component is part of the initial render tree, and that persistence will silently fail if the component is first mounted after the parent's first flush. Users can then audit their render flow.
  3. Runtime warningTryRegisterSessionEstablishment could log a Warning when it short-circuits on Response.HasStarted in a streaming context, mirroring the existing CookieTempDataNotPersistedAfterResponseStarted log.

@ilonatommy

ilonatommy commented Jun 5, 2026

Copy link
Copy Markdown
Member

Declaring [SupplyParameterFromSession], [SupplyParameterFromTempData], or accepting the cascading ITempData on a component that is part of the initial render tree is supported.

I think the proposed rule really covers the supported scenarios. Suggestion: "first initial render tree" is an implementation detail of blazor, we could rephrase it to:
"Declaring [SupplyParameterFromSession], [SupplyParameterFromTempData], or accepting the cascading ITempData on a component that is rendered as part of the page's synchronously resolved markup is supported."

With examples that you prepared it should be understandable.

component that is part of the initial render tree

Child component rendered in top-level (not inside an async @if/@for blocks) is a part of intial render tree. We're missing that in examples.

  1. Under prerendering — streaming also governs the prerender pass of components in Interactive Server / WebAssembly / Auto render modes.

Prerendering doesn't stream by default if the component doesn't set [StreamRendering].

@dariatiurina

Copy link
Copy Markdown
Contributor Author

@javiercn After discussion with Ilona I decided to go with analyzer that will tell users that their [SupplyParameterFromSession] may not work in some scenarios and have a note in the docs. If you have some else idea, I would love to hear it!

@javiercn

javiercn commented Jun 8, 2026

Copy link
Copy Markdown
Member

A few things here:

  • Streaming rendering is way more prevalent in Blazor than it is in MVC/Razor Pages (is a more first-class feature).
  • If MVC/Razor Pages emits a warning, I think it's fair to do so in Blazor too.
  • You can't in general with an analyzer determine this happened. You can't pierce into library code. When suggesting an analyzer, you must provide concrete details on what situations the analyzer will detect. Otherwise, you are relying on a "magic want" that will solve your problems.

@dariatiurina dariatiurina force-pushed the 66745-streamingssr-tempdata-sessiondata branch from acfd2ea to 295808b Compare June 8, 2026 15:41
@dariatiurina dariatiurina requested a review from ilonatommy June 8, 2026 15:47
@dariatiurina

Copy link
Copy Markdown
Contributor Author

In the end I decided that doing analyzers can be too vague (I started doing them and got stuck on trying to write short and easily understandable message for this case), so we will have runtime warning that will tell users that we silently skip over persisting values and coverage in docs to tell of this complication.

Comment thread src/Components/Endpoints/src/SessionEstablishmentHelper.cs Outdated
Comment thread src/Components/Endpoints/src/SessionEstablishmentHelper.cs Outdated
Comment thread src/Components/Endpoints/src/SessionEstablishmentHelper.cs Outdated
Comment thread src/Components/Endpoints/src/SessionEstablishmentHelper.cs Outdated
@dariatiurina dariatiurina requested a review from ilonatommy June 12, 2026 15:53
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.

Fix TempData and SupplyParameterFromSession to support streaming SSR

4 participants