Read More on Telerik Blogs
March 25, 2026 Web, Blazor
Get A Free Trial

Tour the notable Blazor changes and new features in .NET 11 Previews 1 and 2 for an early glimpse into what’s coming for Blazor in .NET 11.

Ever since the significant changes that landed with .NET 8, we’ve seen incremental updates to Blazor over several releases.

This year seems set to continue that trend, with some key gaps being filled and refinements to key features. So no structural shifts, new hosting models or render modes, but we do finally get a Label component for forms!

Here are some notable Blazor changes and new features that landed in .NET 11 Previews 1 and 2.

TempData for Static SSR

Ever since .NET 8, we’ve had SSR mode for Blazor. It’s a way of building applications using Blazor’s Razor components, but without resorting to Blazor Server or Blazor WASM to run those components.

With SSR, when a user requests your page, Blazor runs on the server, locates the relevant page component, renders it and returns the generated HTML which is then displayed in the browser.

In this mode you can also use forms to capture user input.

But sometimes you want to take information and persist it between page navigations. For example, maybe a user enters their name in a form, and you want to show them a personalized message on the subsequent confirmation screen, “Thanks, Jon, for your order.”

Prior to .NET 11, this required you to pass the value along somehow, with common solutions involving query string, session state or custom cookies.

But MVC and Razor Pages developers have long been able to use something called Temp Data to store that information between screens. Temp Data is specifically for capturing short-lived messages or data that only need to survive one redirect, then disappear.

In .NET 11, Temp Data is coming to Blazor and will be available as a cascading parameter in static SSR components, with no extra registration needed beyond the usual AddRazorComponents():

[CascadingParameter]
public ITempData? TempData { get; set; }

Let’s say you’re about to redirect your user to a different page. You can write a value to TempData:

private void HandleSubmit()
{
    TempData["SuccessMessage"] = $"Thanks {Input.Name}, your message has been sent!";
    Navigation.NavigateTo("/contact/confirmation");
}

Then pick that up on the page they’re redirected to:

protected override void OnInitialized()
{
    // Get() reads and marks for deletion - one-shot read
    confirmationMessage = TempData?.Get("SuccessMessage") as string;
}

Under the hood, these values are stored in a cookie in the user’s browser.

Once you’ve retrieved a value via Get, it’s marked for deletion. So if you want to check the value without it being deleted, you can use Peek instead. If you want to keep something that has been marked for deletion you can call Keep to cancel the deletion.

IHostedService in Blazor WebAssembly

If you’re using Blazor WebAssembly, you may well have realized there’s one thing you can’t easily do with Blazor running in the browser: schedule jobs to run in the background.

This is a challenge because sometimes you want to separate background work from your component lifecycles. For example, you might want to poll an API (maybe a pricing or notifications endpoint).

In WASM apps, prior to .NET 11, this has been tricky to achieve, often requiring the use of timers within specific components (tied to component lifecycles and only running when the component is rendered), or singleton services with a manual timer.

.NET 11 simplifies this with IHostedService.

You can add a hosted service to your Blazor WASM app and it will start with the app (not a specific component), and run independently of navigation.

AddHostedService<T>() now works with WebAssemblyHostBuilder:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddHostedService<PricePollingService>();
await builder.Build().RunAsync();

The service itself is standard BackgroundService, identical to its server-side equivalent:

public class PricePollingService(PriceState state) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await FetchPricesAsync();
            state.NotifyUpdate();
            await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
        }
    }
}

This example uses a Singleton PriceState service and a component which listens for updates to that state.

The PriceState service acts as a shared state container which the background service writes to:

Program.cs

builder.Services.AddSingleton<PriceState>();

PriceState.cs

public class PriceState
{
    public event Action? OnChange;

    public List<PriceQuote> Quotes { get; private set; } = [];
    public DateTimeOffset LastUpdated { get; private set; }

    public void UpdateQuotes(List<PriceQuote> quotes)
    {
        Quotes = quotes;
        LastUpdated = DateTimeOffset.UtcNow;
        OnChange?.Invoke();
    }
}

And then you can subscribe to the OnChange event when that data changes:

PriceWatcher.razor

@implements IDisposable
@inject PriceState State

<p>Last updated: @State.LastUpdated</p>

<!-- render price data here -->
@code {
    protected override void OnInitialized()
    {
        State.OnChange += HandleStateChanged;
    }

    private void HandleStateChanged()
    {
        InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        State.OnChange -= HandleStateChanged;
    }
}

Important note: WASM is single-threaded. BackgroundService gives you a convenient way to abstract background tasks, but not true parallelism.

I/O-bound work (polling, fetching) is typically fine because it’s mostly waiting (await), which frees the thread up in the meantime. But CPU-heavy work would block the UI.

But fear not, there’s a solution for that too!

Web Worker Template

For anything CPU-heavy, you want to keep the main thread (which also handles the UI) free.

.NET 11 introduces dotnet new webworker, which scaffolds a Razor class library with the JS plumbing needed to run .NET code in a browser web worker. Modern web browsers support web workers as a convenient way to run tasks completely separately from your main application’s thread.

The template will create a WebWorkerClient class which uses a factory pattern to create worker instances. Then you can define your worker methods using [JSExport].

Here’s an example, which computes primes (a CPU-heavy task).

PrimesWorker.cs

[SupportedOSPlatform("browser")]
public static partial class PrimesWorker
{
    [JSExport]
    public static string ComputePrimes(int limit)
    {
        // CPU-intensive work runs on the worker thread
        // UI stays responsive
        var result = SieveOfEratosthenes(limit);
        return JsonSerializer.Serialize(new PrimeResult(primes.Count, primes.TakeLast(5).ToArray(), sw.ElapsedMilliseconds));
    }
}

public record PrimeResult(int Count, int[] LastFive, long ElapsedMs);

You can invoke this method from a component (via a web worker).

private const int limit = 50_000_000;

var worker = await WebWorkerClient.CreateAsync(JSRuntime);
var result = await worker.InvokeAsync<PrimeResult>(
    "WebWorkerAppDemo.PrimesWorker.ComputePrimes",
    args: [limit]);

If you were to run that same prime number code directly in a component, it would lock UI updates for as long as it’s running. Try it with the above approach (using web workers), and the UI will stay entirely responsive throughout.

Some important notes about web workers:

  • Class must be static partial with [SupportedOSPlatform("browser")].
  • Return types: primitives or strings only (complex types must be serialized to JSON).
  • Your project needs <AllowUnsafeBlocks>true</AllowUnsafeBlocks> set in its csproj.
  • The worker loads a second .NET runtime, so startup is slow but compute is genuinely parallel.
  • Template produces a class library, which you can then reference from your main project.

Relative Navigation

This is one of those “if you know, you know” challenges.

In previous versions of .NET, when you want to render a relative link, things got tricky. Say you wanted a link to an FAQ page at /faq. .NET would assume you wanted to navigate to /faq at the app root. But if you were already on /docs/, you might well have wanted that link to go to /docs/faq.

The workaround up to now has been to use hardcoded full paths everywhere, which is asking for trouble when you start moving things around.

NavigationManager.NavigateTo() and NavLink now accept RelativeToCurrentUri, which, when set to true, means relative paths will resolve against the current page path instead of the app root:

@page "/docs/getting-started"

@* .NET 11 - resolves to /docs/getting-started/overview *@
<NavLink href="overview" RelativeToCurrentUri="true">Overview</NavLink>

@* Before - had to hardcode the full path *@
<NavLink href="/docs/getting-started/overview">Overview</NavLink>

Programmatic navigation:

Navigation.NavigateTo("faq", new NavigationOptions
{
    RelativeToCurrentUri = true
});

Note: Path must not have a leading slash for relative resolution (“overview” not “/overview”).

DisplayName and Label Components

This may well be one of those features that leaves you wondering how it took so long to appear, but Blazor now has a Label component (and a handy DisplayName one too).

Both components read display names from model attributes:

public class ProductModel
{
    [Display(Name = "Production Date")]  // DisplayName and Label read this
    public DateOnly? ProductionDate { get; set; }

    [DisplayName("Unit Price (£)")]     // Also supported, but [Display] takes precedence
    public decimal Price { get; set; }

    public int StockCount { get; set; }  // Falls back to raw property name
}

Here’s how you can use them in a form:

@* Label wraps the input - accessible by default *@
<Label For="@(() => Model.ProductionDate)" class="form-label">
    <InputDate @bind-Value="Model.ProductionDate" class="form-control" />
</Label>

@* DisplayName standalone *@
<th><DisplayName For="@(() => Model.Price)" /></th>

Previously you had to write your own <label> components, so this is a welcome addition.

EnvironmentBoundary Component

Ever felt the need to show different information on the screen depending on which environment your app’s running in? (Think UI that only shows up when you’re running locally, or on staging.)

Now you can conditionally render content based on the hosting environment:

<EnvironmentBoundary Include="Development">
    <p>Debug tools enabled</p>
</EnvironmentBoundary>

<EnvironmentBoundary Exclude="Production">
    <p>Test banner</p>
</EnvironmentBoundary>

<EnvironmentBoundary Include="Staging,Production">
    <p>Analytics script</p>
</EnvironmentBoundary>

Previously, you had to inject IWebHostEnvironment and write manual conditional statements. This declarative approach is cleaner (note the matching of environment is case-insensitive).

BasePath Component

Another small but handy change.

<BasePath /> in App.razor replaces the hardcoded <base href="/">:

<head>   
    <BasePath />   
</head>

This will take the value from NavigationManager.BaseUri and essentially returns whatever UsePathBase sets (which you can configure in Program.cs).

This is handy if you’re running your app on a subpath, as the base href will be automatically set to the correct value.

SignalR ConfigureConnection

If you need to configure your SignalR connection for Blazor Server, you now have easier config available via AddInteractiveServerRenderMode.

In .NET 11, it accepts a ConfigureConnection callback for the underlying SignalR connection:

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode(options =>
    {
        options.ConfigureConnection = connectionOptions =>
        {
            connectionOptions.ApplicationMaxBufferSize = 512 * 1024;
            connectionOptions.TransportMaxBufferSize = 512 * 1024;
            connectionOptions.Transports = HttpTransportType.WebSockets;
          	connectionOptions.AllowStatefulReconnects = true;
            connectionOptions.CloseOnAuthenticationExpiration = true;
        };
    });

Previously, you had to resort to global SignalR hub config. Now you can target Blazor’s hub specifically.

You’ll probably find yourself spending time here if you run into errors because your components hold too much state, you want to tweak stateful reconnection logic or have other reasons to tweak how the SignalR part of Blazor Server works.

What to Watch

That wraps up a quick tour of some of the highlights of .NET 11 Previews 1 and 2 for Blazor.

Here are a few things that look set to land in future previews:

  • SessionData for static SSR – The session-scoped counterpart to TempData
  • Cache component – Component-level output caching
  • Client-side validation without a circuit – No need to resort to Blazor Server for your statically rendered forms
  • Aspire integration improvements
  • Virtualize with variable-height items
  • [SupplyParameterFromTempData] attribute

.NET 11 will be released in November, with preview releases dropping every month until then.


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