Skip to content

Fix TempData and SupplyParameterFromSession persistence for streaming SSR case#66832

Merged
dariatiurina merged 22 commits into
dotnet:mainfrom
dariatiurina:66745-streamingssr-tempdata-sessiondata
Jun 16, 2026
Merged

Fix TempData and SupplyParameterFromSession persistence for streaming SSR case#66832
dariatiurina merged 22 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 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 value modified later (e.g. after an await in OnInitializedAsync, once streaming has begun) was never written.

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

Changes

  • RazorComponentEndpointInvoker.cs — After all components (including streaming) finish rendering, and before persisted component state is emitted, the invoker now explicitly persists values:

    1. SessionCascadingValueSupplier.PersistAllValues() (when the supplier is registered)
    2. TempDataService.Persist(context)
  • SessionEstablishmentHelper.cs (new) — Adds TryRegisterSessionEstablishment(HttpContext). It 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. It is idempotent per request and logs a warning (once per request) when:

    • no session is available (session middleware not registered) — SessionDoesNotExist
    • the response has already started, so the cookie can no longer be issued — SessionStateNotPersistedAfterResponseStarted
  • SessionCascadingValueSupplier.cs — Removed the _onStartingRegistered field and the Response.OnStarting(PersistAllValues) registration that used to happen inside CreateSubscription. Instead, CreateSubscription now calls SessionEstablishmentHelper.TryRegisterSessionEstablishment so the session cookie is established early. Actual persistence is now driven explicitly by the invoker after rendering completes.

  • TempDataProviderServiceCollectionExtensions.cs — In GetOrCreateTempData, removed the Response.OnStarting(...) callback that used to call TempDataService.Save. When the active ITempDataProvider is SessionStorageTempDataProvider, it now calls SessionEstablishmentHelper.TryRegisterSessionEstablishment to ensure the session cookie is issued before streaming.

  • TempDataService.cs

    • Added Persist(HttpContext) — retrieves the per-request ITempData from HttpContext.Items and calls Save. If the active provider is CookieTempDataProvider and Response.HasStarted is true, it logs a warning (new CookieTempDataNotPersistedAfterResponseStarted event) and returns without saving, since cookie TempData cannot work once response headers are frozen.
    • Save now accepts ITempData instead of the concrete TempData type, using pattern matching (tempData is not TempData data || !data.WasLoaded) to guard the Save() call.
    • The class is now partial to host the LoggerMessage source-generated logger.

Testing

  • New unit tests in SessionEstablishmentHelperTest.cs covering TryRegisterSessionEstablishment: logging when no session feature is present, logging when the response has already started, no logging on the happy path, and once-per-request / once-per-each-request log de-duplication.

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

    • StreamingSSR_PersistsSupplyParameterFromSession_AfterAsyncRendering
    • StreamingSSR_PersistsSupplyParameterFromTempData_AfterAsyncRendering
    • StreamingSSR_PersistsTempDataCascadingParameter_AfterAsyncRendering
    • StreamingSSR_DeferredChildSubscription_DoesNotPersistSession_OnFirstRequest — a child that first subscribes to session after streaming has begun does not get its value persisted on the first request (the cookie can no longer be established).
  • New negative E2E test in TempDataCookieTest.cs:

    • StreamingSSR_CookieTempData_DoesNotPersistValuesWrittenAfterFirstFlush — asserts cookie-based TempData written during streaming is not persisted (expected behavior — the new warning is logged).
  • New test pages under Pages/StreamingRendering/:

    • StreamingSessionPersistence.razor ([StreamRendering]) — writes a [SupplyParameterFromSession] value, a [SupplyParameterFromTempData] value, and an ITempData cascading value after await Task.Delay(100), i.e. after the streaming phase has begun.
    • StreamingParentWithDeferredChild.razor / StreamingDeferredSessionChild.razor — a parent that only mounts a session-writing child after streaming has started.
  • 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, now also asserting that reloading yields empty TempData.

Behavior Change to Call Out

When a page actually uses session-backed features — i.e. a component has a [SupplyParameterFromSession] parameter (which creates a subscription) or the session-storage TempData provider is active — the session cookie (.AspNetCore.Session) is now issued before streaming begins, even if no value is ultimately written. This is the deliberate effect of TryRegisterSessionEstablishment, which flips the session middleware's establishment gate during OnStarting so that session writes performed after streaming begins can still be persisted. Pages that do not use any session-backed feature are unaffected (the helper is not invoked for them).

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
Comment thread src/Components/Endpoints/src/SessionEstablishmentHelper.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.

Approving, pending corrections discussed offline:

  • Small updates to the warning message.
  • Update to warning deduplication from per-process (flag check) to per-request (key check).

@dariatiurina dariatiurina enabled auto-merge (squash) June 15, 2026 17:37
@ilonatommy ilonatommy self-requested a review June 16, 2026 10:11
@dariatiurina dariatiurina merged commit fb7deb6 into dotnet:main Jun 16, 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.

Fix TempData and SupplyParameterFromSession to support streaming SSR

4 participants