Blazor in .NET 10 Gets Circuit State Persistence, Passkeys, and a Declarative State Model
Introduction
As technology continues to evolve, the .NET platform consistently brings innovation and improvements to developers. As a crucial part of the .NET ecosystem, ASP.NET Core introduces exciting new features and optimizations in every version. This article will delve into the major updates in ASP.NET Core for .NET 10, particularly the various enhancements within the Blazor framework, aiming to provide developers with a clear, comprehensive overview to help you better leverage these new features to build more powerful and secure web applications. We will focus on improvements in security, performance, development experience, and routing, providing relevant code examples and detailed explanations.
Main Body
Blazor Web App Security Enhancements
In .NET 10, the security of Blazor Web Apps has been significantly improved. Several new and updated security samples have been officially provided, covering different authentication and authorization scenarios.
1. New and Updated Blazor Web App Security Samples
The security samples for Blazor Web Apps have been comprehensively updated, mainly covering the following aspects:
- OpenID Connect (OIDC) Protection: Provides detailed guides and examples on how to protect a Blazor Web App using OIDC.
- Microsoft Entra ID (formerly Azure AD) Protection: Updated examples for protecting a Blazor Web App using Microsoft Entra ID.
- Windows Authentication Protection: Added examples for protecting a Blazor Web App using Windows Authentication.
All OIDC and Entra sample solutions now include a separate Web API project (MinimalApiJwt) to demonstrate how to securely configure and call external Web APIs. The method for calling Web APIs uses token handlers and named HTTP clients to interface with OIDC identity providers, or uses the Microsoft Identity Web package/API for Microsoft Entra ID.
These sample solutions are configured in C# code within the Program file. Additionally, new guidance is provided for configuring solutions from application settings files (e.g., appsettings.json), located in the "Provide configuration via JSON configuration provider (app settings)" section of the OIDC or Entra articles.
The Entra article and sample app also include new guidance on:
- How to use encrypted distributed token caches for web farm hosting scenarios.
- How to use Azure Key Vault with Azure Managed Identities to protect data.
Blazor UI Components and Performance Optimization
1. QuickGrid RowClass Parameter
To provide more flexible UI style control, the QuickGrid component has a new RowClass parameter. Developers can now dynamically apply stylesheet classes to grid rows based on specific conditions of the row item. For example, defining a method to apply different CSS classes based on the IsArchived property of MyGridItem:
<QuickGrid ... RowClass="GetRowCssClass">
...
</QuickGrid>
@code {
private string GetRowCssClass(MyGridItem item) =>
item.IsArchived ? "row-archived" : null;
}
This greatly enhances the customization capability of QuickGrid, making data presentation more intuitive and expressive.
2. Closing QuickGrid Column Options
Now, the QuickGrid's new method HideColumnOptionsAsync can be used to close the column options UI. This is useful for scenarios where column options need to be automatically closed after a user performs a specific action (like applying a filter), thereby improving user experience.
<QuickGrid @ref="movieGrid" Items="movies">
<PropertyColumn Property="@(m => m.Title)" Title="Title">
<ColumnOptions>
<input type="search" @bind="titleFilter" placeholder="Filter by title"
@bind:after="@(() => movieGrid.HideColumnOptionsAsync())" />
</ColumnOptions>
</PropertyColumn>
<PropertyColumn Property="@(m => m.Genre)" Title="Genre" />
<PropertyColumn Property="@(m => m.ReleaseYear)" Title="Release Year" />
</QuickGrid>
@code {
private QuickGrid<Movie>? movieGrid;
private string titleFilter = string.Empty;
private IQueryable<Movie> movies = new List<Movie> { ... }.AsQueryable();
private IQueryable<Movie> filteredMovies =>
movies.Where(m => m.Title!.Contains(titleFilter));
}
3. Response Streaming Enabled by Default and Opt-Out
Before .NET 10, response streaming for HttpClient requests was optionally enabled; now it is enabled by default. This means calling HttpContent.ReadAsStreamAsync on HttpResponseMessage.Content (response.Content.ReadAsStreamAsync()) returns a BrowserHttpReadStream instead of a MemoryStream. BrowserHttpReadStream does not support synchronous operations, such as Stream.Read(Span<Byte>). If code uses synchronous operations, developers can choose to disable response streaming or manually copy the Stream to a MemoryStream.
To opt out of global response streaming, add the <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse> property to the project file, or set the DOTNET_WASM_ENABLE_STREAMING_RESPONSE environment variable to false or 0.
<WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
To opt out of response streaming for a single request, set SetBrowserResponseStreamingEnabled on the HttpRequestMessage to false:
requestMessage.SetBrowserResponseStreamingEnabled(false);
4. Blazor Scripts as Static Web Assets
To improve performance and optimize resource loading, in .NET 10 and later, Blazor scripts are now served as static web assets with automatic compression and fingerprinting, instead of being provided from embedded resources in the ASP.NET Core shared framework. This helps better leverage browser caching and CDNs, improving application load speed.
5. Blazor WebAssembly Performance Profiling and Diagnostic Counters
The new version introduces comprehensive performance profiling and diagnostic counters for Blazor WebAssembly applications. These counters provide detailed observability of component lifecycles, navigation, event handling, and circuit management, helping developers identify and resolve performance bottlenecks.
6. Preloaded Blazor Framework Static Resources
In Blazor Web Apps, framework static resources are automatically preloaded using Link header information, allowing the browser to preload resources before fetching and rendering the initial page. In standalone Blazor WebAssembly apps, framework resources are scheduled for high-priority download and cached early in the browser's index.html page processing. This mechanism can significantly shorten the application's initial load time.
7. Blazor WebAssembly Static Resource Preloading in Blazor Web Apps
To better preload WebAssembly assets in Blazor Web Apps, Blazor replaces the <link> header with the LinkPreload component (<LinkPreload />). This allows the application's base path configuration (<base href="..." />) to correctly identify the application's root directory. By default, the Blazor Web App template adopts this feature in .NET 10. Apps upgrading to .NET 10 can implement this feature by placing the App component after the base URL tag (App.razor) in the head content (<base>) of the LinkPreload component.
<head>
...
<base href="/" />
+ <LinkPreload />
...
</head>
8. Custom Blazor Caching and BlazorCacheBootResources MSBuild Property Removed
Since all Blazor client files are now fingerprinted and cached by the browser, Blazor's custom caching mechanism and the BlazorCacheBootResources MSBuild property have been removed from the framework. Developers should remove this property from the client project's project file, as it no longer has any effect.
Blazor Routing and Navigation Improvements
1. Route Template Highlights
The [Route] attribute now supports route syntax highlighting to help developers better visualize the structure of route templates, reducing route configuration errors.
2. MapsTo No Longer Scrolls to Top for Same-Page Navigation
Previously, NavigationManager.NavigateTo would scroll to the top of the page during same-page navigation. In .NET 10, this behavior has changed; the browser no longer scrolls to the top when navigating to the same page. This means the viewport is no longer reset when updating the current page's address (e.g., changing query strings or fragments), improving user experience, especially in single-page applications.
3. Reconnect UI Component Added to Blazor Web App Project Template
The Blazor Web App project template now includes a ReconnectModal component, which contains collocated stylesheets and JavaScript files, designed to improve developer control over the reconnection UI when the client loses its WebSocket connection to the server. This component does not programmatically insert styles, ensuring compliance with stricter Content Security Policy (CSP) settings for style-src directives. New reconnection UI features include indicating reconnection status by setting specific CSS classes on reconnection UI elements and dispatching a new components-reconnect-state-changed event to change reconnection status. Code can use the new reconnection state "retrying" indicated by CSS classes and the new event to better differentiate the stages of the reconnection process.
4. Ignoring Query Strings and Fragments When Using NavLinkMatch.All
When using the NavLinkMatch.All value for the NavLink parameter, the Match component will now ignore query strings and fragments. This means if the URL path matches but the query string or fragment changes, the link will retain the active class. To revert to the original behavior, set the Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragment AppContext switch to true. Developers can also customize matching behavior by overriding the ShouldMatch method of NavLink.
public class CustomNavLink : NavLink
{
protected override bool ShouldMatch(string currentUriAbsolute)
{
// Custom matching logic
}
}
5. NavigationManager.NavigateTo No Longer Throws NavigationException
Previously, during static server-side rendering (SSR), calling NavigationManager.NavigateTo would throw a NavigationException before converting to a redirect response, interrupting execution. In .NET 10, calling NavigationManager.NavigateTo during static SSR no longer throws a NavigationException. Its behavior is consistent with interactive rendering, performing the navigation without throwing an exception. Code that relies on NavigationException being thrown should be updated. For example, in the default Blazor Identity UI, IdentityRedirectManager used to throw an InvalidOperationException after calling RedirectTo to ensure it wasn't called during interactive rendering. This exception and the [DoesNotReturn] attribute should now be removed.
6. Blazor Router Has NotFoundPage Parameter
Blazor now provides an improved method for displaying a "not found" page when navigating to a non-existent page. A page type can be specified to render when NavigationManager.NotFound is called by passing it to the Router component using the NotFoundPage parameter. This approach is recommended over the NotFound render fragment (<NotFound>...</NotFound>) because it supports routing, adapts to status code page re-execution middleware, and is compatible with non-Blazor scenarios. If both the NotFound render fragment and NotFoundPage are defined, the page specified by NotFoundPage takes precedence.
<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>This content is ignored because NotFoundPage is defined.</NotFound>
</Router>
Project Blazor templates now include a NotFound.razor page by default. This page is automatically rendered whenever NavigationManager.NotFound is called in the application, making it easier to handle missing routes and providing a consistent user experience.
7. Handling "Not Found" Responses in Static SSR and Global Interactive Rendering with NavigationManager
NavigationManager now includes a NotFound method for handling cases where a requested resource is not found during static server-side rendering (static SSR) or global interactive rendering.
- Static Server-Side Rendering (SSR): Calling
NotFoundsets the HTTP status code to 404. - Interactive Rendering: Notifies the Blazor router (
Routercomponent) to render "not found" content. - Streaming Rendering: If enhanced navigation is active, streaming rendering renders the "not found" content without reloading the page. When enhanced navigation is blocked, the framework redirects to the "not found" content, refreshing the page.
Streaming NavigationManager.NotFound content rendering uses the following order:
NotFoundPagepassed to theRoutercomponent (if present).- The configured status code page re-execution middleware page.
- If neither of the above methods is adopted, no action is taken.
Non-streaming NavigationManager.NotFound content rendering uses the following order:
NotFoundPagepassed to theRoutercomponent (if present).- If "not found" render fragment content exists, that content is used. Not recommended in .NET 10 or later.
DefaultNotFound404 content ("Not found" plain text).
UseStatusCodePagesWithReExecute takes precedence when handling browser address routing issues (like mistyped URLs or clicking invalid links). When NavigationManager.OnNotFound is called, the NotFound event can be used for notification.
Blazor Development Experience and Interoperability
1. Declarative Model for Saving Component and Service State
State can now be specified declaratively, enabling it to be persisted from components and services via the [SupplyParameterFromPersistentComponentState] attribute. During prerendering, properties with this attribute are automatically persisted through the PersistentComponentState service. When the component is rendered interactively or the service is instantiated, the state is retrieved.
Previously, preserving component state during prerendering using the PersistentComponentState service involved a significant amount of code. This code can now be simplified using the new declarative model:
@page "/movies"
@inject IMovieService MovieService
@if (MoviesList == null)
{
<p><em>Loading...</em></p>
}
else
{
<QuickGrid Items="MoviesList.AsQueryable()">
...
</QuickGrid>
}
@code {
[SupplyParameterFromPersistentComponentState]
public List<Movie>? MoviesList { get; set; }
protected override async Task OnInitializedAsync()
{
MoviesList ??= await MovieService.GetMoviesAsync();
}
}
State can be serialized for multiple components of the same type, and declarative state can be established in services by calling RegisterPersistentService on the component builder (Razor) with a custom service type and render mode in AddRazorComponents for use throughout the application.
2. New JavaScript Interop Features
Blazor has added support for the following JS interop features:
- Creating instances of JS objects using constructors and obtaining an
IJSObjectReference/IJSInProcessObjectReference.NET handle to the referenced instance. - Reading or modifying the values of JS object properties, including data properties and accessor properties.
The following asynchronous methods are available on IJSRuntime and IJSObjectReference:
InvokeNewAsync(string identifier, object?[]? args): Asynchronously invokes the specified JS constructor.var classRef = await JSRuntime.InvokeNewAsync("jsInterop.TestClass", "Blazor!"); var text = await classRef.GetValueAsync<string>("text"); var textLength = await classRef.InvokeAsync<int>("getTextLength");GetValueAsync<TValue>(string identifier): Asynchronously reads the value of the specified JS property.var valueFromDataPropertyAsync = await JSRuntime.GetValueAsync<int>( "jsInterop.testObject.num");SetValueAsync<TValue>(string identifier, TValue value): Asynchronously updates the value of the specified JS property.await JSRuntime.SetValueAsync("jsInterop.testObject.num", 30);
These methods have overloads that accept a CancellationToken parameter or a TimeSpan timeout parameter.
The following synchronous methods are available on IJSInProcessRuntime and IJSInProcessObjectReference:
InvokeNew(string identifier, object?[]? args): Synchronously invokes the specified JS constructor.var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); var classRef = inProcRuntime.InvokeNew("jsInterop.TestClass", "Blazor!"); var text = classRef.GetValue<string>("text"); var textLength = classRef.Invoke<int>("getTextLength");GetValue<TValue>(string identifier): Synchronously reads the value of the specified JS property.var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); var valueFromDataProperty = inProcRuntime.GetValue<int>( "jsInterop.testObject.num");SetValue<TValue>(string identifier, TValue value): Synchronously updates the value of the specified JS property.var inProcRuntime = ((IJSInProcessRuntime)JSRuntime); inProcRuntime.SetValue("jsInterop.testObject.num", 20);
3. JavaScript Bundler Support
Blazor's build output can now generate bundler-friendly output during publishing by setting the MSBuild property WasmBundlerFriendlyBootConfig to true, making it compatible with JavaScript bundlers (such as Gulp, Webpack, and Rollup). This provides developers with greater flexibility to use existing JavaScript toolchains in Blazor applications.
4. Setting the Environment in Standalone Blazor WebAssembly Apps
Starting from .NET 10, the Properties/launchSettings.json file is no longer used to control the environment in standalone Blazor WebAssembly apps. Now, developers should use the <WasmApplicationEnvironmentName> property in the application's project file (.csproj) to set the environment.
<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>
The default environments are: Development (for build) and Production (for publish).
5. Inlined Boot Configuration
Blazor's boot configuration, which existed in a file named blazor.boot.json before .NET 10, is now inlined into the dotnet.js script. This primarily affects developers who directly manipulate the blazor.boot.json file.
6. Improved Form Validation
Blazor now features improved form validation, including support for validating properties of nested objects and collection items. To opt into the new validation features, perform the following steps:
Call the extension method
AddValidationin the service registration fileProgram.builder.Services.AddValidation();Declare the form model type in a C# class file, not in a Razor component (
.razor).Annotate the root form model type with the
[ValidatableType]attribute.[ValidatableType] public class Order { public Customer Customer { get; set; } = new(); public List<OrderItem> OrderItems { get; set; } = []; } public class Customer { [Required(ErrorMessage = "Name is required.")] public string? FullName { get; set; } [Required(ErrorMessage = "Email is required.")] public string? Email { get; set; } public ShippingAddress ShippingAddress { get; set; } = new(); }
In the component, continue to use the DataAnnotationsValidator component inside the EditForm component:
<EditForm Model="Model">
<DataAnnotationsValidator />
<h3>Customer Details</h3>
<div class="mb-3">
<label>
Full Name
<InputText @bind-Value="Model!.Customer.FullName" />
</label>
<ValidationMessage For="@(() => Model!.Customer.FullName)" />
</div>
@* ... form continues ... *@
</EditForm>
@code {
public Order? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
// ... code continues ...
}
The requirement to declare model types outside of Razor component (.razor) files is because both the new validation feature and the Razor compiler itself use source generators. Currently, the output of one source generator cannot be used as the input for another.
Web Authentication API (Passkey) Support for ASP.NET Core Identity
ASP.NET Core Identity now supports passkey authentication based on WebAuthn and FIDO2 standards. The Web Authentication (WebAuthn) API, widely known as passkeys, is a modern, phishing-resistant authentication method that enhances security and user experience by leveraging public-key cryptography and device-based authentication. This feature allows users to log in without passwords using secure, device-based authentication methods (such as biometrics or security keys). The Preview 6 Blazor Web App project template provides out-of-the-box passkey management and login functionality.
Circuit State Persistence
During server-side rendering, a Blazor Web App can preserve user session (circuit) state even if the connection to the server is disconnected for an extended period or actively paused, as long as a full page refresh is not triggered. This allows users to resume sessions across scenarios like browser tab throttling, mobile users switching apps, network interruptions, or active resource management (pausing inactive circuits) without losing unsaved work.
Persisting state requires fewer server resources than persisting a circuit:
- Even when disconnected, a circuit might continue executing work, consuming CPU, memory, and other resources. Persisting state only consumes a fixed amount of memory controlled by the developer.
- Persisting state represents a subset of the memory consumed by the application, so the server does not need to track the application's components and other server-side objects.
State is preserved in two cases:
- Component State: State used by components for interactive server rendering, such as a list of items retrieved from a database or a form the user is filling out.
- Scoped Services: State held in server-side services, like the current user.
State persistence is enabled by default when AddInteractiveServerComponents is called in the AddRazorComponents file. MemoryCache is the default storage implementation for a single application instance, storing up to 1,000 persisted circuits for two hours, which is configurable. Developers can change the defaults for the memory provider using the following options:
PersistedCircuitInMemoryMaxRetained: The maximum number of circuits to retain. The default is 1,000 circuits.PersistedCircuitInMemoryRetentionPeriod: The maximum retention period as aTimeSpan. The default is 2 hours.
services.Configure<CircuitOptions>(options => {
options.PersistedCircuitInMemoryMaxRetained = {CIRCUIT COUNT};
options.PersistedCircuitInMemoryRetentionPeriod = {RETENTION PERIOD};
});
Annotate component properties with [SupplyFromPersistentComponentState] to enable circuit state persistence.
@foreach (var item in Items) {
<ItemDisplay @key="@($"unique-prefix-{item.Id}")" Item="item" />
}
@code {
[SupplyFromPersistentComponentState]
public List<Item> Items { get; set; }
protected override async Task OnInitializedAsync()
{
Items ??= await LoadItemsAsync();
}
}
To persist state for scoped services, annotate service properties with [SupplyFromPersistentComponentState], add the service to the service collection, and call the RegisterPersistentService extension method:
public class CustomUserService {
[SupplyFromPersistentComponentState]
public string UserData { get; set; }
}
services.AddScoped<CustomUserService>();
services.AddRazorComponents()
.AddInteractiveServerComponents()
.RegisterPersistentService<CustomUserService>(RenderMode.InteractiveAuto);
Client-side fingerprinting
In .NET 10, developers can opt-in to enable client-side fingerprinting for JavaScript modules in standalone Blazor WebAssembly apps. During build/publish in a standalone Blazor WebAssembly app, the framework substitutes placeholders in index.html with values calculated during the build to fingerprint static assets. The fingerprint is embedded into the blazor.webassembly.js script filename.
The following markup must exist in the wwwroot/index.html file to adopt the fingerprinting feature:
<head>
...
+ <script type="importmap"></script>
</head>
<body>
...
- <script src="_framework/blazor.webassembly.js"></script>
+ <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>
In the project file (.csproj), add the <OverrideHtmlAssetPlaceholders> property set to true:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
+ <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
</Project>
Any script in index.html marked with a fingerprint placeholder will be fingerprinted by the framework. For example, a script file named scripts.js located in the app's wwwroot/js folder is fingerprinted by adding #[.{fingerprint}] before the file extension (.js):
<script src="js/scripts#[.{fingerprint}].js"></script>
To fingerprint other JS modules in a standalone app, use the <StaticWebAssetFingerprintPattern> property in the Blazor WebAssembly app's project file (.csproj).
<StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.mjs"
Expression="#[.{fingerprint}]!" />
Files are automatically placed in the import map, and when resolving imports for JavaScript interop, the browser uses the import map to resolve the fingerprinted files.
Conclusion
ASP.NET Core in .NET 10 brings many important updates, especially to the Blazor framework, which has seen significant improvements in security, performance, user experience, and development efficiency. From enhanced Blazor Web App security samples, style and operation control for the QuickGrid component, to response streaming enabled by default and client-side fingerprinting, these features are all designed to help developers build more secure, faster, and more maintainable modern web applications. The new JavaScript interop features and declarative state management model also greatly simplify the development process. Furthermore, routing and navigation improvements, particularly the optimization of NavigationManager.NavigateTo behavior and the introduction of the NotFoundPage parameter, make the user experience of Blazor applications smoother and more controllable. Passkey authentication support further strengthens application security. Overall, ASP.NET Core in .NET 10 continues to be committed to improving developer productivity and provides a more solid foundation for building high-performance web applications. Developers should actively explore and leverage these new features to fully realize the potential of .NET 10.
Series Articles
.NET 10 New Features Series Article 1 - New Features in the Runtime