Telerik blogs

The first release candidate for .NET 10 delivers few surprises, improves persistent component state and delivers more useful metrics.

It’s September, which can only mean one thing: It’s .NET Release Candidate time!

.NET 10 RC1 has landed, and with it come a few interesting last-minute improvements.

PersistentState

New in .NET 10 is the PersistentState attribute. This is a renamed version of the attribute that shipped in earlier .NET 10 preview releases. Goodbye SupplyParameterFromPersistentComponentState. We barely knew you.

But it isn’t just a name change. The attribute has some subtle new behavior, especially relevant if you’re using enhanced navigation.

First, a quick recap.

If you apply the PersistentState attribute to data in your component, two things will happen.

  1. The state will be persisted during the first render then reused for subsequent renders. This means, if you’re prerendering your components, the data fetched during prerendering is then reused when the component renders again (using one of Blazor’s interactive render modes).
  2. If you’re using Interactive Server render mode, any data marked with this attribute will be persisted when the underlying circuit is evicted. So your users might lose their connection, reconnect to a new circuit, but keep their persisted state.

Now Plays Nicely with Enhanced Navigation

The big change in RC1 is how this all works when you’re using enhanced navigation. Enhanced navigation is a useful way to provide a “SPA-like” feel for your Blazor apps.

With enhanced navigation, when a user follows a link which routes to a Razor component in your app, Blazor will intercept the navigation, issue a fetch request, retrieve the new page content then patch it into the existing DOM.

This makes for a smoother experience, and avoids reloading the entire page when only a small part of it has actually changed. You’ll encounter this if your app is set up for per page/component interactivity.

For example, take this slightly modified version of the famous “Weather Data” Blazor page.

@page "/weather"
@rendermode InteractiveServer

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<!-- markup to show the weather here -->
@code {
    
    public WeatherForecast[]? Forecasts { get; set; }

    protected override async Task OnInitializedAsync()
    {
        if (Forecasts == null)
        {
            // Simulate asynchronous loading to demonstrate streaming rendering
            await Task.Delay(500);

            var startDate = DateOnly.FromDateTime(DateTime.Now);
            var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
            Forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = summaries[Random.Shared.Next(summaries.Length)]
            }).ToArray();
        }
    }   
}

When a user navigates to this page within your app (meaning they started on a different page then followed a link to this one), they will likely end up using enhanced navigation.

But it won’t look great because they’ll see one set of weather data, then a “flash,” followed by different weather data.

This is thanks to prerendering.

When the new page is requested, Blazor renders your component once on the server, returns that initial HTML to the browser, then spins up interactive server mode and renders the component again.

The result is a visible flash when the initial data is shown, then the second set of data is shown when the component starts running interactively.

Architecture diagram showing browser-based weather application rendering flow. Two browser windows are connected by an arrow, representing navigation between pages. Below each browser are two rendering approaches: On the left, a 'Prerender' box contains a blue 'Weather.razor' component that receives weather data from a database via server request. On the right, an 'Interactive Render' box contains the same blue 'Weather.razor' component that also receives weather data from a database, but uses DOM diffing for client-side updates. Both approaches connect to identical cylindrical database icons representing weather data storage.

As you can see here, there is one call to fetch the weather data during prerendering, and a second during the interactive render.

Now this is precisely what PersistentState is designed to fix.

We can decorate the Forecasts property with the PersistentState attribute. In theory, this means the initial data will be persisted (during prerendering) and then reused during the second (interactive) render.

@code {
    
    [PersistentState]
    public WeatherForecast[]? Forecasts { get; set; }
    
    // rest of code
    
}

But it turns out this doesn’t work with enhanced navigation. When users navigate internally (within your app), enhanced navigation will be used and they’ll still see that “flash” as the data is fetched, then fetched again.

So what gives?

Well, the new default in .NET 10 is that persisted component state won’t be applied for interactive components during enhanced navigation.

This is to avoid any dramas if the user ends up navigating to the same page they’re already on and has data they want to keep, for example in a form. In that case, they probably don’t want their form values to be overridden by persisted component state.

But there is a way to bypass this and have persistent state work as we might expect (for data like this, which the user isn’t changing).

We can set AllowUpdates = true on the [PersistentState] attribute.

@code {
    
    [PersistentState(AllowUpdates = true)]
    public WeatherForecast[]? Forecasts { get; set; }
    
    // rest of code
    
}

This indicates to Blazor that we’re happy for the persisted state to be used during enhanced navigation.

When a user navigates to this weather page, we want the data that’s fetched during prerendering to be applied when the component renders a second time (rendered interactively).

Architecture diagram showing browser-based weather application with two rendering approaches. Two browser windows are connected by an arrow representing page navigation. The left side shows a 'Prerender' approach where the browser makes a weather request and receives HTML with persisted data. A blue 'Weather.razor' component receives weather data from a cylindrical database icon below. The right side shows an 'Interactive Render' approach where the browser performs rendering with persisted state and uses DOM diffing for updates. It contains the same blue 'Weather.razor' component but operates client-side. Both approaches connect to the same weather data database at the bottom of the diagram.

With that, the “flash” disappears and we get just one version of the data.

Note that persisted data is usually serialized (using JSON) and included in the rendered HTML for the initial prerender, then passed back to the component when it spins up in one of the interactive render modes.

More Control over Persisted State

In addition to enabling persisted state during enhanced navigation, we also get more fine-grained control over how persisted state is applied in general.

Remember how Blazor will now (as of .NET 10) attempt to restore persisted state when reconnecting to a new circuit on the server?

Should you decide you don’t want this to happen, you can switch this behavior off using the new RestoreBehavior parameter.

@code {
    [PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
	public WeatherForecast[]? Forecasts { get; set; }
}

By default, when a user loses their connection and Blazor connects them to a new circuit, Blazor will take the weather data they had, persist it when the circuit is evicted and restore it to the new circuit.

From the user’s perspective, they won’t notice any change. The weather data that they had before being disconnected will be the same weather data they see on the page after they’re reconnected.

But when you set RestoreBehavior to SkipLastSnapshot, the persisted state won’t be restored during reconnection.

Instead, new weather data will be retrieved and the user will see the data change in the UI when their browser reconnects to the server.

Alternatively, you may be happy for the persisted state to be used during reconnection, but don’t want it used between prerendering and interactive rendering. In that case you can set RestoreBehavior to SkipInitialValue.

@code {
    [PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)]
    public WeatherForecast[]? Forecasts { get; set; }    
}

In this scenario, let’s say a user hits this weather page directly when visiting your app. With SkipInitialValue, Blazor won’t attempt to share the Forecasts data between prerendering and the subsequent interactive rendering.

Which means that, for that first load, we’re back to the flash as the data is loaded once, then loaded again during the second render.

But, the persisted data will be used when the user loses their connection and reconnects.

Clear? Me neither!

Let’s try putting all this into a table to see if it clears things up.

Here are the various combinations of options and results (how many times the weather data is fetched) for a number of different scenarios.

RestoreBehaviorAllowUpdatesResult – direct page loadResult – enhanced navigationResult – reconnection
SkipInitialValueTRUEData fetched twiceData fetched onceExisting data restored
SkipInitialValueFALSEData fetched twiceData fetched twiceExisting data restored
SkipLastSnapshotTRUEData fetched onceData fetched onceData refetched
SkipLastSnapshotFALSEData fetched onceData fetched twiceData refetched

The three scenarios shown are:

  • Direct page load – When a user directly requests our weather page (for example by entering the URL directly in the browser)
  • Enhanced navigation – When the user arrives at the weather page via internal navigation (within the app, following a link from another page)
  • Reconnection – When the user loses their circuit and Blazor reconnects them to a new one

The takeaway here is that you can now precisely control how persisted state is restored. (It just takes a little bit of work to figure out which combination you need for your specific situation!)

Validation Improvements

Onto simpler changes…

You can now apply validation attributes to classes and records (not just properties). This opens the door to validating complex objects with custom logic.

Take this (somewhat contrived) example.

[NoBot]
public class UserRegistration
{
    [Required(ErrorMessage = "Username is required")]
    [StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")]
    public string? Username { get; set; }

    [Required(ErrorMessage = "Email is required")]
    [EmailAddress(ErrorMessage = "Please enter a valid email address")]
    public string? Email { get; set; }

    public string? Honeypot { get; set; }
}

See the NoBot attribute?

That points to a custom validation attribute, which can work with the entire object (not just individual fields).

public class NoBotAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(object? value, ValidationContext _)
    {
        int botWarningSigns = 0;

        if (value is not UserRegistration registration)
            return new ValidationResult("Invalid registration data.");

        // Honeypot field: bots often fill hidden fields
        if (!string.IsNullOrEmpty(registration.Honeypot))
        {
            botWarningSigns += 1;
        }          

        // Username and email should not be identical
        if (!string.IsNullOrWhiteSpace(registration.Username) &&
            !string.IsNullOrWhiteSpace(registration.Email) &&
            registration.Username.Equals(registration.Email, StringComparison.OrdinalIgnoreCase))
        {
            botWarningSigns += 1;
        }

        if(botWarningSigns >= 2){
            return new ValidationResult("Bot-like behavior detected!");
        }

        return ValidationResult.Success;
    }
}

So as well as the standard validation attributes on the class’s properties, we’ve also written some custom logic to assign a “score” based on warning signs that the form may have been filled in by a bot, then fail validation if that score exceeds a certain number.

It’s also now possible to indicate that a property should skip validation.

For example, if you have an Address class that is reused in a couple of places and you don’t want it to be validated in one of those cases. You can apply the [SkipValidation] attribute to a specific property or parameter, or to a type.

New ASP.NET Core Identity Metrics

If you’re using ASP.NET Core Identity, you can now tap into some handy built-in metrics for key user and sign-in operations.

Notably the following (in Microsoft.AspNetCore.Identity):

  • aspnetcore.identity.user.create.duration
  • aspnetcore.identity.user.update.duration
  • aspnetcore.identity.user.delete.duration
  • aspnetcore.identity.user.check_password_attempts
  • aspnetcore.identity.user.generated_tokens
  • aspnetcore.identity.user.verify_token_attempts
  • aspnetcore.identity.sign_in.authenticate.duration
  • aspnetcore.identity.sign_in.check_password_attempts
  • aspnetcore.identity.sign_in.sign_ins
  • aspnetcore.identity.sign_in.sign_outs
  • aspnetcore.identity.sign_in.two_factor_clients_remembered
  • aspnetcore.identity.sign_in.two_factor_clients_forgotten

All the Other Changes Landed in Earlier Releases

.NET 10 is on track to release in November 2025. If this is the first time you’ve looked at .NET 10, there are lots more changes not covered here, which were already included in earlier preview releases and are now baked into RC1.

Here are some of the highlights:

  • The Blazor script is now served as a static asset (complete with precompression, preloading and fingerprinting)
  • NavigateTo preserves scroll position
  • The web app template now includes the markup, CSS and JS for the Blazor Server reconnection UI component
  • New Blazor metrics and traces for monitoring the performance of your Blazor apps
  • Improved component not found handling (404s)
  • Improved JavaScript Interop
  • Persistent component state can be stored in memory or hybrid cache
  • New APIs for pausing and resuming circuits
  • Validation for nested objects
  • Passkey support

Based on this release, it seems Blazor in .NET 10 is firmly on track to be the most stable and performant release of Blazor so far.

Long-standing challenges (most notably around how Blazor Server disconnects are handled) have been addressed, performance improved and observability enhanced (both in development and production using the new metrics).

RC1 is the first of the “go-live” releases, and at this point it seems unlikely any major changes will land between now and the RTM release in November.


See more details about .NET 10 in our previous updates:


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.