From 957db9f32d0fcad4e8a82a1c0829bdff89da089e Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:48:27 -0400 Subject: [PATCH 1/2] Improve state persistence service example --- .../prerendered-state-persistence.md | 78 +++++++++++++++---- aspnetcore/blazor/state-management/server.md | 2 + 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/aspnetcore/blazor/state-management/prerendered-state-persistence.md b/aspnetcore/blazor/state-management/prerendered-state-persistence.md index 5f033fdb1031..ffca696569cd 100644 --- a/aspnetcore/blazor/state-management/prerendered-state-persistence.md +++ b/aspnetcore/blazor/state-management/prerendered-state-persistence.md @@ -32,6 +32,10 @@ The first logged count occurs during prerendering. The count is set again after To retain the initial value of the counter during prerendering, Blazor supports persisting state in a prerendered page using the service (and for components embedded into pages or views of Razor Pages or MVC apps, the [Persist Component State Tag Helper](xref:mvc/views/tag-helpers/builtin-th/persist-component-state-tag-helper)). +By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser. + +The persisted prerendered state is transferred to the client, where it's used to restore the component state. During client-side rendering (CSR, `InteractiveWebAssembly`), the data is exposed to the browser and must not contain sensitive, private information. During interactive server-side rendering (interactive SSR, `InteractiveServer`), [ASP.NET Core Data Protection](xref:security/data-protection/introduction) ensures that the data is transferred securely. The `InteractiveAuto` render mode combines WebAssembly and Server interactivity, so it's necessary to consider data exposure to the browser, as in the CSR case. + :::moniker range=">= aspnetcore-10.0" @@ -136,6 +140,8 @@ In the following example that serializes state for multiple components of the sa } ``` +## Serialize state for services + In the following example that serializes state for a dependency injection service: * Properties annotated with the `[PersistentState]` attribute are serialized during prerendering and deserialized when the app becomes interactive. @@ -148,12 +154,19 @@ In the following example that serializes state for a dependency injection servic > [!NOTE] > Only persisting scoped services is supported. - +Serialized properties are identified from the actual service instance: -`CounterService.cs`: +* This approach allows marking an abstraction as a persistent service. +* Enables actual implementations to be internal or different types. +* Supports shared code in different assemblies. +* Results in each instance exposing the same properties. + +The following counter service, `CounterTracker`, marks its current count property, `CurrentCount` with the the `[PersistentState]` attribute. The property is serialized during prerendering and deserialized when the app becomes interactive wherever the service is injected. + +`CounterTracker.cs`: ```csharp -public class CounterService +public class CounterTracker { [PersistentState] public int CurrentCount { get; set; } @@ -165,19 +178,60 @@ public class CounterService } ``` -In `Program.cs`: +In the `Program` file, register the scoped service and register the service for persistence with `RegisterPersistentService`. In the following example, the `CounterTracker` service is available for both the Interactive Server and Interactive Webassembly render modes if a component renders in either of those modes because it's registered with `RenderMode.InteractiveAuto`. + +If the `Program` file doesn't already use the namespace, add the following `using` statement to the top of the file: ```csharp +using Microsoft.AspNetCore.Components.Web; +``` + +Where services are registered in the `Program` file: + +```csharp +builder.Services.AddScoped(); + builder.Services.AddRazorComponents() - .RegisterPersistentService(RenderMode.InteractiveAuto); + .RegisterPersistentService(RenderMode.InteractiveAuto); ``` -Serialized properties are identified from the actual service instance: +Inject the `CounterTracker` service into a component and use it to increment a counter. For demonstration purposes in the following example, the value of the service's `CurrentCount` property is set to 10 only during prerendering. -* This approach allows marking an abstraction as a persistent service. -* Enables actual implementations to be internal or different types. -* Supports shared code in different assemblies. -* Results in each instance exposing the same properties. +`Pages/Counter.razor`: + +```razor +@page "/counter" +@inject CounterTracker CounterTracker + +Counter + +

Counter

+ +

Rendering: @RendererInfo.Name

+ +

Current count: @CounterTracker.CurrentCount

+ + + +@code { + protected override void OnInitialized() + { + if (!RendererInfo.IsInteractive) + { + CounterTracker.CurrentCount = 10; + } + } + + private void IncrementCount() + { + CounterTracker.IncrementCount(); + } +} +``` + +To use preceding component to demonstrate persisting the count of 10 in `CounterTracker.CurrentCount`, navigate to the component and refresh the browser, which triggers prerendering. When prerendering occurs, you briefly see indicate "`Static`" before displaying "`Server`" after final rendering. The counter starts at 10. + +## Use the `PersistentComponentState` service directly instead of the declarative model As an alternative to using the declarative model for persisting state with the `[PersistentState]` attribute, you can use the service directly, which offers greater flexibility for complex state persistence scenarios. Call to register a callback to persist the component state during prerendering. The state is retrieved when the component renders interactively. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown. @@ -268,10 +322,6 @@ When the component executes, `currentCount` is only set once during prerendering :::moniker-end -By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser. - -The persisted prerendered state is transferred to the client, where it's used to restore the component state. During client-side rendering (CSR, `InteractiveWebAssembly`), the data is exposed to the browser and must not contain sensitive, private information. During interactive server-side rendering (interactive SSR, `InteractiveServer`), [ASP.NET Core Data Protection](xref:security/data-protection/introduction) ensures that the data is transferred securely. The `InteractiveAuto` render mode combines WebAssembly and Server interactivity, so it's necessary to consider data exposure to the browser, as in the CSR case. - :::moniker range=">= aspnetcore-10.0" ## Serialization extensibility for persistent component state diff --git a/aspnetcore/blazor/state-management/server.md b/aspnetcore/blazor/state-management/server.md index c7c96908419c..cb6ebfb2a4fb 100644 --- a/aspnetcore/blazor/state-management/server.md +++ b/aspnetcore/blazor/state-management/server.md @@ -51,6 +51,8 @@ An app can only persist *app state*. UIs can't be persisted, such as component i ## Circuit state persistence + + During server-side rendering, Blazor Web Apps can persist a user's session (circuit) state when the connection to the server is lost for an extended period of time or proactively paused, as long as a full-page refresh isn't triggered. This allows users to resume their session without losing unsaved work in the following scenarios: * Browser tab throttling From 0a1f2b8143ded318385dcaa36593be2805ffddd7 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Mon, 6 Oct 2025 09:56:18 -0400 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../blazor/state-management/prerendered-state-persistence.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/state-management/prerendered-state-persistence.md b/aspnetcore/blazor/state-management/prerendered-state-persistence.md index ffca696569cd..ffe4c5eb659b 100644 --- a/aspnetcore/blazor/state-management/prerendered-state-persistence.md +++ b/aspnetcore/blazor/state-management/prerendered-state-persistence.md @@ -161,7 +161,7 @@ Serialized properties are identified from the actual service instance: * Supports shared code in different assemblies. * Results in each instance exposing the same properties. -The following counter service, `CounterTracker`, marks its current count property, `CurrentCount` with the the `[PersistentState]` attribute. The property is serialized during prerendering and deserialized when the app becomes interactive wherever the service is injected. +The following counter service, `CounterTracker`, marks its current count property, `CurrentCount` with the `[PersistentState]` attribute. The property is serialized during prerendering and deserialized when the app becomes interactive wherever the service is injected. `CounterTracker.cs`: @@ -178,7 +178,7 @@ public class CounterTracker } ``` -In the `Program` file, register the scoped service and register the service for persistence with `RegisterPersistentService`. In the following example, the `CounterTracker` service is available for both the Interactive Server and Interactive Webassembly render modes if a component renders in either of those modes because it's registered with `RenderMode.InteractiveAuto`. +In the `Program` file, register the scoped service and register the service for persistence with `RegisterPersistentService`. In the following example, the `CounterTracker` service is available for both the Interactive Server and Interactive WebAssembly render modes if a component renders in either of those modes because it's registered with `RenderMode.InteractiveAuto`. If the `Program` file doesn't already use the namespace, add the following `using` statement to the top of the file: