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.
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.
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.
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).
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.
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.
RestoreBehavior | AllowUpdates | Result – direct page load | Result – enhanced navigation | Result – reconnection |
---|---|---|---|---|
SkipInitialValue | TRUE | Data fetched twice | Data fetched once | Existing data restored |
SkipInitialValue | FALSE | Data fetched twice | Data fetched twice | Existing data restored |
SkipLastSnapshot | TRUE | Data fetched once | Data fetched once | Data refetched |
SkipLastSnapshot | FALSE | Data fetched once | Data fetched twice | Data refetched |
The three scenarios shown are:
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!)
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.
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
.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:
NavigateTo
preserves scroll positionBased 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 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.