Telerik blogs

.NET 10 refines some of Blazor’s rougher edges while leaving the fundamentals intact.

.NET 10 just landed, and with it come some changes for Blazor. But what’s changed, and how will it affect your projects?

Here’s a quick run down of the new and changed features to keep an eye on if you plan to upgrade.

Persistent Component State

This is probably the biggest, most impactful change.

  • Persistent state can now be configured using the new PersistentState attribute.
  • It can be used to share data between and interactive renders.
  • It can also automatically restore users component state when they connect to a new circuit.

Preloading—Fixing the Dreaded ‘Flash’ of Data

You may be using prerendering for your Blazor apps. Prerendering is enabled by default and enables your users to get a snappy, fast-loading first response when they land on your site. (It also helps search engines to index it.)

But, this prerendering creates a tricky problem. If you render a component interactively, with prerendering enabled, your component is initialized and rendered twice.

If you then fetch data in your component, that data will be fetched twice too (once during prerendering, and again when your component renders interactively).

This creates a visible “flash” as the data appears, briefly, then disappears, only to be replaced by the data that was fetched second time around.

Here’s an example of a page that fetches contact details.

Blazor Server Prerendering and Interactive Render Flow. Diagram showing browser-server-client interactions for Home.razor with prerender and interactive render, including contact data retrieval from the database.

We can change this using persistent state.

This is a mechanism whereby you can take that data from the first render, and reuse it for the second render (thereby fixing the flashing state problem, and cutting in half the number of requests to your database).

New in .NET 10 is a handy attribute that can do this for you, for any data you want to persist and re-use this way.

[PersistentState]
public List<Contact>? Contacts { get; set; }

With this, the Contacts data will be fetched once during prerendering, but then persisted by Blazor.

Blazor Server Prerendering and Interactive Render with Persisted State. Diagram illustrates browser-server interactions for Home.razor, highlighting persisted data and state in both HTML and render phases, with contact data from the database.

This is actually done by serializing the data and embedding it in the HTML returned to the browser. Then Blazor can pick that state up and use it during interactive rendering.

You Can Also Persist State When Users Lose Their Connection to the Server

But PersistentState also has another trick up its sleeve.

Imagine you have some kind of UI state, like the sort options the user has chosen for a grid showing a list of contacts.

You can imagine a scenario where the user selects to sort the contacts by name, in descending order, but then goes off to make a cup of tea. When they come back, their connection to the server has been lost.

Blazor will automatically reconnect but … disaster! They’ve lost their sort order.

This is because that state was initially on one circuit, but by the time they got back from their tea break, Blazor had discarded that circuit and given them a new one. So their contacts flipped back to their original sort order.

PersistentState can help with this problem too.

Blazor Server logic during circuit eviction and auto-persisting state to memory. Diagram showing browser-server disconnect, the eviction of the circuit, and the persisting of Home.razor state to server memory.

When you decorate properties with the PersistentState attribute, it now signals to Blazor to take that state and persist it to memory when a circuit is evicted.

[PersistentState]
public ContactSortState? SortState { get; set; }

Then, when the user’s browser reconnects to the server and a new circuit is issued, Blazor can automatically restore that state.

What’s more, if you enable HybridCache for your application, it will be persisted to memory first and then to a second cache (for example, you might want to use a Redis Cache).

Blazor Server circuit eviction, persisted state recovery, and circuit re-connection. Diagram showing browser reconnection, eviction of circuit, auto-persisting Home.razor state to memory/Redis, and restoring state on new circuit for DOM diff and interactive render

Validation Improvements

The validation system for Blazor has been updated. There are two notable improvements here:

  • Nested types validation
  • Validation attributes for entire objects (classes or records)

DataAnnotations Validator Now Handles Nested Types

In previous versions of .NET, if you had a form model with a nested property like this:

public class NewContactForm
{
    public Contact Contact { get; set; }
    public string? Honeypot { get; set; }
}

public class Contact
{
    public int Id { get; set; }
    [Required]
    public string Name { get; set; }
    [Required]
    public string Email { get; set; }
    public string Phone { get; set; }
    public string Company { get; set; }
    public string Role { get; set; }
}

Then attempted to use it in an EditForm with the DataAnnotations validator, the properties in the Contact object wouldn’t trigger validation.

In .NET 10 you can enable this with a couple of tweaks.

First you’ll need to register the new validation system in Program.cs.

builder.Services.AddValidation();

Then you can decorate your root level type (NewContactForm in this case) with the [ValidatableType] attribute.

Validation Attributes for Entire Classes or Records

Validation attributes enable you to create custom logic for how a field should be validated, but previously they were limited to individual properties.

Now you can use a ValidationAttribute with a class or record.

For example here’s a (contrived!) example of validation logic that attempts to check for “bot-like” behavior in a form.

public class NoBotAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value, ValidationContext _)
    {
        var botWarningSigns = 0;
        if (value is not NewContactForm registration)
            return new ValidationResult("Invalid registration data.");
        // Honeypot field: bots often fill hidden fields
        if (!string.IsNullOrEmpty(registration.Honeypot))
        {
            botWarningSigns += 1;
        }

        // other checks here...
        return botWarningSigns >= 1
            ? new ValidationResult("Bot-like behavior detected!")
            : ValidationResult.Success;
    }
}

This checks if the form entry includes a field for a “honeypot” field (a hidden text input that a human wouldn’t typically fill in).

We can then apply this to our NewContactForm class.

using BestPartsDemo.Models;

namespace BestPartsDemo.Components.Pages.Contacts;

[NoBot]
[ValidatableType]
public class NewContactForm
{
    public Contact Contact { get; set; }
    public string? Honeypot { get; set; }
}

And now we’ll get the validation error if a user fills in that (hidden) field.

There are a few notable improvements to nav and nav links.

Here are some highlights:

  • More easily configured 404 error pages
  • More intelligent NavLink route matching

New NotFoundPage Attribute for Router

You can now configure which component should be displayed in the event of an attempt to navigate to a non-existent page in Blazor, using the new NotFoundPage attribute in your Routes.razor file.

<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
    	...
    </Found>
</Router>

Note you may also need to include this line in Program.cs so that this also works in all scenarios (such as static server-side rendering).

app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);

Previously, when you used the NavLink component, its matching logic would take into account querystring values (and fragments). This effectively meant it broke nav links when you used the querystring.

If you had a NavLink that should be active when someone visited the homepage for your app, but they ended up at your homepage with a querystring value of ?broken=1 (for example), the NavLink would not light up (wouldn’t get its active CSS class).

Now, by default, the querystring will be ignored.

Hand in hand with this, you can implement your own matching logic in a custom NavLink class.

public class ContactsNavLink : NavLink
{
    protected override bool ShouldMatch(string uriAbsolute)
    {
        return uriAbsolute.Contains("/contacts");
    }
}

Here, if the URI contains /contacts, the NavLink will be considered active.

Customizable Reconnection Modal

The source code for the Blazor Server reconnection modal is now included in new Blazor Web App projects.

If you include a ReconnectModal component and render it in your app, it will be used in preference to the default one.

Reconnection Modal HTML and solution explorer showing .css and .js files

By default you’ll find this component is rendered in App.razor.

<body>
<Routes/>
<ReconnectModal/>
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>

New APIs for Controlling Blazor Server Connections

There are new JavaScript calls you can use to pause and resume Blazor Server circuits.

For example, you might want to detect when the user navigates away from your site (to another tab) and pause their circuit at that point, then resume it when they come back.

document.addEventListener('visibilitychange', ()=> {
    if(document.hidden) {
        console.debug("Document hidden - pausing Blazor");
        Blazor.pauseCircuit();
    } else {
        console.debug("Document visible - resuming Blazor");
        Blazor.resumeCircuit();
    }
});

Fingerprinted and Compressed Blazor Scripts

The blazor scripts are now served as a static web asset with automatic compression and fingerprinting.

This means they will get unique filenames based on their contents (which will ensure users always get the latest version) and the compression takes the blazor.web.js script down by about 76%.

<script src="@Assets["_framework/blazor.web.js"]"></script>

The Blazor script used for Blazor WASM is also fingerprinted and compressed.

For Blazor WASM standalone apps, the same is true, albeit the mechanism is slightly different.

WebAssembly File Fingerprinting Syntax

Here you can see the [.{fingerprint}].js placeholder. This will be replaced during build/publish of the app, so long as the OverrideHtmlAssetPlaceholders property is set to trey in the .csproj for the project.

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
+   <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
  </PropertyGroup>
</Project>

New Metrics for Blazor

There are a slew of new metrics to pour over in .NET 10. A good place to visualize these is using MS Aspire.

Here’s how you can enable them via your ServiceDefaults project for Aspire (metrics and tracing).

builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics =>
        {
            metrics.AddAspNetCoreInstrumentation()
                .AddHttpClientInstrumentation()
                .AddRuntimeInstrumentation()
                .AddMeter("Microsoft.AspNetCore.Components")
                .AddMeter("Microsoft.AspNetCore.Components.Lifecycle")
                .AddMeter("Microsoft.AspNetCore.Components.Server.Circuits");
        })
        .WithTracing(tracing =>
        {
            tracing.AddSource(builder.Environment.ApplicationName)              
                .AddSource("Microsoft.AspNetCore.Components")
                .AddSource("Microsoft.AspNetCore.Components.Server.Circuits")
                .AddHttpClientInstrumentation();
        });

Note the new ones are all the ones that start with Microsoft.AspNetCore.Components.

With that you’ll find these metrics light up in Aspire.

screenshot of Aspire showing new asp.net component metric

Other Notable Improvements

While these were some of the highlights, there are quite a few other changes for Blazor in .NET 10.

Here’s a list of some of the other notable changes for you to explore at your leisure!

  • New and updated web app security samples
  • Programmatically detect when NotFound triggered (and programmatically trigger a 404)
  • New JS Interop features (call JS class constructors and properties)
  • Option to disable navigation exceptions when navigating during static rendering
  • Navigating to the same page no longer resets the browser scroll position

You can download .NET 10 from Microsoft here.

Using Visual Studio? this release is compatible with Visual Studio 2026, or of course you can use another IDE such as JetBrains Rider.

To learn about Progress Telerik .NET 10 updates, read: Day-Zero Support for .NET 10 Across Progress Telerik.


Jon Hilton
About the Author

Jon Hilton

Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.

Related Posts

Comments

Comments are disabled in preview mode.