Ich treffe immer wieder Entwicklerinnen und Entwickler, die meinen, Blazor sei schon wieder tot. Diesen Gedanken kann ich schwer nachvollziehen, insbesondere nicht beim Blick auf die vielen Neuerungen in .NET 11.0 Preview 1 (erschienen am 10.02.2026) und Preview 2 (erschienen am 10.03.2026), die den größten Teil aller bisher vorgestellten Neuerungen in ASP.NET Core 11.0 ausmachen.
Für Blazor 11.0 muss man das .NET 11.0 SDK installieren [1] und die Visual Studio 2026 Insider Edition verwenden.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Containeroption in der Projektvorlage „Blazor Web App“
Die Projektvorlage Blazor Web App erlaubt nun das Anlegen eines Blazor-Projekts direkt in einem Docker-Container (Abb. 1).

Abb. 1: Neue Option in der Projektvorlage
Neue Projektvorlage für Web Worker
Für die Erstellung von Web-Worker-Projekten gibt es nun eine eigene Projektvorlage: .NET Web Worker in Visual Studio bzw. auf der Kommandozeile dotnet new webworker. Dabei entsteht eine DLL, die in Blazor-WebAssembly-basierten Anwendungen genutzt werden kann, um beliebigen Code als Web Worker im Hintergrund aufzurufen, ohne den UI-Thread zu blockieren. Das erzeugte Web-Worker-Projekt enthält eine C#-Datei und zwei JavaScript-Dateien:
-
WebWorkerClient.cs mit Klasse WebWorkerClient
-
dotnet-web-worker.js: JavaScript-Funktionen erzeugen eine eigene Instanz der .NET-Laufzeitumgebung im Webbrowser
-
dotnet-web-worker-client.js: JavaScript-Funktionen zum Aufruf der Arbeitsmethode
In diesen Dateien muss man erstmal nichts ändern. Man referenziert das Web-Worker-Projekt im Blazor-WebAssembly-Projekt und erlaubt unsichere Blöcke:
<!--Notwendig für WebWorker-->
<ItemGroup>
<ProjectReference Include="..\WebWorker\NET11_WebWorker.csproj" />
</ItemGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
In einer Razor-Komponente ruft man dann die CreateAsync()-Methode der in der Projektvorlage enthaltenen Klasse WebWorkerClient auf, um eine Instanz zu erhalten:
workerClient = await WebWorkerClient.CreateAsync(JSRuntime);
Um den Web Worker zu starten, verwendet man InvokeAsync() unter Angabe einer statischen .NET-Arbeitsmethode (als Zeichenkette), die die Arbeit ausführen soll, sowie der von der Arbeitsmethode erwarteten Parameter und des Rückgabetyps der Methode als Typparameter:
private SalesAnalysis? result;
...
result = await workerClient!.InvokeAsync<SalesAnalysis>(
$"{nameof(Services)}.{nameof(DataWorker)}." + nameof(DataWorker.AnalyzeSalesData_WebWorker), [recordCount]);
Wenn man den Web Worker vorzeitig beenden will, kann man DisposeAsync() aufrufen:
await workerClient.DisposeAsync();
Allerdings löst das einen Laufzeitfehler in InvokeAsync() aus. Daher muss man dort ein try … catch ergänzen.
Das Beispiel in Abbildung 2 zeigt: Ein Web Worker braucht ein wenig mehr Rechenzeit als die Verarbeitung auf dem Main Thread, aber während der Verarbeitung bleibt die Benutzeroberfläche reaktiv. Leider kann aus Platzgründen der Programmcode zu diesem Beispiel hier nicht abgedruckt werden.

Abb. 2: Verarbeitung auf Main Thread vs. Verarbeitung via Web Worker
Umgebungsabhängiges Rendering
Die neue Komponente <EnvironmentBoundary> erlaubt HTML-Ausgaben abhängig von der Hostumgebung (Development, Staging und Production). Über die Parameter Include und Exclude steuern Entwicklerinnen und Entwickler, in welchen Umgebungen eine Ausgabe stattfindet (Listing 1). In ASP.NET MVC und Razor Pages gab es dafür bisher schon einen Tag Helper. In Blazor mussten Entwicklerinnen und Entwickler mit @if-Bedingungen arbeiten.
</pre>
<EnvironmentBoundary Include="Development,Staging">
<div class="alert alert-warning">
<p>Debug oder Staging</p>
</div>
</EnvironmentBoundary>
<EnvironmentBoundary Exclude="Production">
<p>@DateTime.Now</p>
</EnvironmentBoundary>
Zugriff auf Umgebungsvariablen mit IConfiguration
Blazor-WebAssembly-Anwendungen erhalten automatisch von der Laufzeitumgebung einige Umgebungsvariablen. Das sind freilich nicht die Umgebungsvariablen des Clientbetriebssystems, auf dem der Webbrowser läuft. Ein Zugriff darauf ist aus Sicherheitsgründen durch den Webbrowser unterbunden.
Neu in .NET 11.0 ist, dass einige Umgebungsvariablen nicht nur via System.Environment, sondern auch per Configuration-Objekt zur Verfügung stehen, das automatisch in den Dependency-Injection-Container eingefügt wird und welches man überall in Razor Components und anderen Klassen per Dependency Injection bekommen kann.
Auch Umgebungsvariablen, die vor dem Aufruf von WebAssemblyHostBuilder.CreateDefault() per Environment.SetEnvironmentVariable() gesetzt werden, erscheinen im Configuration-Objekt (Listing 2, Abb. 3).
</pre>
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
namespace NET11_BlazorWASMStandalone;
public class Program
{
public static async Task Main(string[] args)
{
// Diese Umgebungsvariable erscheint im Configuration-Objekt
Environment.SetEnvironmentVariable("API_ENDPOINT", "https://miraclelistbackend.azurewebsites.net/");
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
// Diese Umgebungsvariable erscheint NICHT im Configuration-Objekt
Environment.SetEnvironmentVariable("ENABLE_PRO_FEATURES", "true");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
}

Abb. 3: Vergleich des Inhalts des Configuration-Objekts mit den manuell hinzugefügten Umgebungsvariablen
Formularbeschriftungen mit <Label> und <DisplayName>
In Blazor 11.0 gibt es neue Formularsteuerelemente mit den Namen <Label> und <DisplayName>. Auch diese sind Pendants zu bestehenden Funktionen in den älteren Webframeworks. Bei beiden Komponenten geht es um die Beschriftung von Formularfeldern. In Blazor-Formularen mussten Entwicklerinnen und Entwickler bisher die Beschriftungen manuell per HTML-Tags erfassen:
<div>
<label for="companyName">Firma</label>
<InputText class="form-control" id="customerFullName" @bind-Value="company.CompanyName" />
<ValidationMessage For="@(() => company.CompanyName)" />
</div>
auch wenn es Datenannotationen im Objektmodell gab, z. B.:
public class Company
{
[DisplayName("Firma")] // oder: [Display(Name = "Firma")]
public string CompanyName { get; set; }
...
}
Nun kann sich Blazor mit <DisplayName> den passenden Anzeigenamen aus dem Objektmodell holen:
<label for="companyName"> <DisplayName For="() => company.CompanyName" /></label>
Die Komponente <Label> erlaubt eine weitere Verkürzung auf
<Label For="() => company.CompanyName" />
Auch das Einbetten eines Eingabesteuerelements in <Label> ist möglich:
<Label For="() => company.CompanyName" >
<InputText class="form-control" id="customerFullName" @bind-Value="company.CompanyName" />
<ValidationMessage For="@(() => company.CompanyName)" />
</Label>
Rendering von MathML
Blazor 11.0 kann nun auch die Mathematical Markup Language (MathML) zur Darstellung mathematischer Formeln im Browser interaktiv rendern (Listing 3). Zuvor konnte man mit Blazor nur HTML und SVG erzeugen.
</pre>
@page "/mathml"
@code {
private int xValue = 4;
}
<h1>MathML Rendering in Blazor</h1>
<h2>
Quadratische Formel (a-b-c-Formel oder Mitternachtsformel)
</h2>
<div class="mb-3">
<label class="form-label">Wert für x:</label>
<input type="number" @bind="xValue" class="form-control" style="max-width: 200px" />
</div>
<div class="p-4 bg-light rounded">
<math display="block">
<mrow>
<mi>x</mi>
<mo>=</mo>
<mfrac>
<mrow>
<mo>−</mo>
<mi>b</mi>
<mo>±</mo>
<msqrt>
<mrow>
<msup><mi>b</mi><mn>2</mn></msup>
<mo>−</mo>
<mn>@xValue</mn>
<mi>a</mi>
<mi>c</mi>
</mrow>
</msqrt>
</mrow>
<mrow>
<mn>@(xValue/2)</mn>
<mi>a</mi>
</mrow>
</mfrac>
</mrow>
</math>
</div>

Abb. 4: Gerenderte Webseite aus Listing 3
Neue Blazor-Komponente <BasePath>
In der App.razor-Datei einer Blazor-Web-App-Anwendung fand man bisher
<base href="">
bzw. für Unterpfade wie /apps/appname
<base href="/apps/appname/">
Das kann man nun ersetzen durch
<BasePath />
Diese neue Komponente ermittelt den href-Wert zur Laufzeit aus der Eigenschaft NavigationManager.BaseUri und verwendet andernfalls /, falls der Basis-URI nicht analysiert werden kann. Dies vereinfacht das Hosting von Blazor-Anwendungen in Unterpfaden wie /apps/appname (Listing 4). Der Einsatz von <BasePath> ist in Blazor-WebAssembly-Standalone-Apps nicht möglich.
</pre>
@using Microsoft.AspNetCore.Components.Endpoints
<!DOCTYPE html>
<html>
<head>
<BasePath />
<link rel="stylesheet" href="css/app.css" />
</head>
<body>
@RenderBody()
</body>
</html>
OnRowClick() im Steuerelement <QuickGrid>
Das Blazor-Tabellensteuerelement <QuickGrid> erhielt in .NET 11.0 Preview 1 ein neues Ereignis OnRowClick():
<QuickGrid
OnRowClick="@HandleRowClick"
ItemsProvider="@itemsProvider"
TGridItem="BO.WWWings.Flight" ...>
Dieses Ereignis kann man per synchroner oder asynchroner Methode behandeln:
void HandleRowClick(BO.WWWings.Flight f) { ... }
async Task HandleRowClick(BO.WWWings.Flight f) { ... }
OnRowClick() sorgt auch dafür, dass beim Überfahren der Zeilen mit der Maus der Cursor-Pointer (in der Regel eine Hand) angezeigt wird.
Relative URLs bei <NavLink> und NavigationManager
Die eingebaute Komponente <NavLink> und die Hilfsklasse NavigationManager bieten nun neben der absoluten auch eine relative Navigation vom aktuellen Standort aus:
<NavLink href="ziel" RelativeToCurrentUri="true">
Zum Ziel
</NavLink>
und
void NavRelative()
{
// Navigate to a sibling page
NavigationManager.NavigateTo("ziel", new NavigationOptions
{
RelativeToCurrentUri = true
});
}
Die relative Navigation von /navigation/start zu /navigation/ziel über Zeichenkette “ziel” (s. o.) funktioniert jedoch nur, wenn die Seite mit /navigation/start aufgerufen wurde, nicht wenn die Seite über eine verkürzte Route wie /navigation aufgerufen wird.
Fragmentbezeichner in URLs bei NavigationManager
Die ebenfalls neue Methode GetUriWithHash() hängt einen Fragmentbezeichner an den aktuellen URL an, z. B. NavigationManager.GetUriWithHash(“Kapitel2”) ergänzt #Kapitel2.
Vereinfachte SignalR-Konfiguration für Blazor Server
Bei Blazor Server können Entwicklerinnen und Entwickler die Eigenschaften der SignalR-Verbindung nun im Startcode Program.cs einfacher setzen als bisher:
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode(options =>
{
options.ConfigureConnection = dispatcherOptions =>
{
dispatcherOptions.CloseOnAuthenticationExpiration = true;
dispatcherOptions.AllowStatefulReconnects = true;
dispatcherOptions.ApplicationMaxBufferSize = 1024 * 1024;
};
});
Angleichung der JavaScript-basierten Konfiguration
Auch bei dem JavaScript-basierten Teil der Blazor-Konfiguration gibt es eine Verbesserung für ältere Projekte, die noch blazor.server.js und blazor.webassembly.js als Clientbibliothek nutzen. Hier können Entwicklerinnen und Entwickler nun die gleiche Syntax wie bei der in .NET 8.0 eingeführten universellen Blazor-Clientbibliothek blazor.web.js verwenden, siehe „Unified startup options format for Blazor scripts“ in den Release Notes [2].
Hintergrunddienste für Blazor WebAssembly mit IHostedService
Blazor WebAssembly unterstützt nun auch die Schnittstelle IHostedService aus dem NuGet-Paket „Microsoft.Extensions.Hosting.Abstractions“. Damit kann man nun Hintergrunddienste im Webbrowser erstellen (Listing 5).
</pre>
using Microsoft.Extensions.Hosting;
namespace Services;
public class DataRefreshService : IHostedService, IDisposable
{
private Timer? _timer;
private int _refreshCount = 0;
private byte _refreshIntervall = 1;
public int RefreshCount => _refreshCount;
public DateTime? LastRefresh = null;
public event Action? OnRefresh;
public Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("[DataRefreshService] Start...");
_timer = new Timer(RefreshData, null, TimeSpan.Zero, TimeSpan.FromSeconds(_refreshIntervall));
return Task.CompletedTask;
}
private void RefreshData(object? state)
{
_refreshCount++;
LastRefresh = DateTime.Now;
Console.WriteLine($"[DataRefreshService] #{_refreshCount} -> {DateTime.Now:HH:mm:ss}");
OnRefresh?.Invoke();
}
public Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("[DataRefreshService] Stop...");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
Der Hintergrunddienst muss in Program.cs vor dem Start registriert werden:
builder.Services.AddSingleton<DataRefreshService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<DataRefreshService>());
Diese Hintergrund-Service-Klasse kann nun überall konsumiert werden. Wichtig ist, dass StateHasChanged() aufgerufen wird, wenn die Hintergrund-Service-Klasse ihre Daten geändert hat (Listing 6, Abb. 5). Blazor kann in solchen Fällen die Änderung nicht automatisch erkennen!
</pre>
@using Services
@inject DataRefreshService RefreshService
@code {
private int refreshCount = 0;
protected override void OnInitialized()
{
RefreshService.OnRefresh += HandleRefresh;
}
private void HandleRefresh()
{
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
RefreshService.OnRefresh -= HandleRefresh;
}
}
<div class="card mb-4">
<div class="card-header">
<h2>Hintergrunddienste mit IHostedService </h2>
</div>
<div class="card-body">
<p>
Ein <code>IHostedService</code> mit Namen <code>DataRefreshService</code> läuft im Hintergrund
</p>
<div class="alert alert-info">
<strong>Anzahl Aktualisierungen:</strong> @RefreshService.RefreshCount<br />
<strong>Letzte Aktualisierung:</strong> @RefreshService.LastRefresh
</div>
</div>
</div>

Abb. 5: Ausgabe des Beispiels aus Listing 6 nach fünf Sekunden
Neuer Analyzer für InvokeAsync() bei JSRuntime
Entwicklerinnen und Entwickler werden nun gewarnt, wenn sie InvokeAsync<object>() einsetzen und aufgefordert, InvokeAsyncVoid() zu verwenden (Abb. 6).

Abb. 6: Neuer Analyzer für InvokeAsync() bei JSRuntime
Datenübergabe zwischen HTTP-Anfragen mit TempData bei Blazor Static SSR
Blazor Static Server-side Rendering (Static SSR), die in .NET 8.0 neu eingeführte Blazor-Variante für Multi-Page-Apps, ist den vorherigen Ansätzen ASP.NET Core MVC und ASP.NET Core Razor Pages überlegen hinsichtlich des Komponentenmodells, der Razor-Syntax und des partiellen Seitenaustauschs. Allerdings gab es bis dato auch einige Funktionen in MVC und Razor Pages, die Blazor Static SSR nicht beherrschte. Dazu gehören das Caching von Seitenteilen, erweiterbare Bedingungen für Routenparameter und die Datenübergabe zwischen HTTP-Anfragen mit dem TempData-Objekt. Letzteres ist nun seit .NET 11.0 Preview 2 möglich. Die Datenspeicherung erfolgt wahlweise als Cookie (CookieTempDataProvider) oder im Session Storage (SessionStorageTempDataProvider) des Webbrowsers.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Anders als bei MVC und Razor Pages ist TempData aber keine Property der Basisklasse, sondern muss als Cascading Parameter explizit konsumiert werden (Listing 7 und 8). Dabei ist zu beachten, dass TempData wie bei MVC und Razor Pages nur Zeichenketten abspeichern kann, d. h., komplexe Objekte müssen Entwicklerinnen und Entwickler selbst serialisieren.
</pre>
@page "/Registration"
@using BlazorSSRSamples
@using Newtonsoft.Json
@inject NavigationManager NavigationManager
<EditForm FormName="Registration" Model="reg" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<p>
Ihr Name: <InputText @bind-Value="reg.Name" />
<ValidationMessage For="@(() => reg.Name)" />
</p>
<p>
Ihre E-Mail-Adresse: <InputText @bind-Value="reg.EMail" />
<ValidationMessage For="@(() => reg.EMail)" />
</p>
<button class="btn btn-primary" type="submit">Bestellen</button>
</EditForm>
@code {
[SupplyParameterFromForm]
RegistrationData reg { get; set; } = new();
[CascadingParameter]
public ITempData? TempData { get; set; }
private string? message;
private void HandleSubmit()
{
TempData!["Message"] = "Registrierung erfolgreich übermittelt!";
TempData!["Reg"] = System.Text.Json.JsonSerializer.Serialize(reg); // speichert nur Strings
TempData!["RegInfo"] = DateTime.Now.ToString();
NavigationManager.NavigateTo("RegistrationConfirm", new NavigationOptions() { ForceLoad = true });
}
}
</pre>
@page "/RegistrationConfirm"
@using BlazorSSRSamples
@inject NavigationManager NavigationManager
<p class="mb-2 alert alert-success">@message</p>
@if (reg.HasValue)
{
<p>Ihre Daten:<br />
@reg.Name<br />
@reg.EMail</p>
}
@if (regInfo.HasValue)
{
<p>Registriert am:<br />
@regInfo</p>
}
@code {
[CascadingParameter]
public ITempData? TempData { get; set; }
private string? message;
private string? regInfo;
BlazorSSRSamples.RegistrationData? reg { get; set; } = new();
protected override void OnInitialized()
{
message = TempData?.Get("Message") as string ?? "No message";
reg = System.Text.Json.JsonSerializer.Deserialize<BlazorSSRSamples.RegistrationData>(TempData?.Get("Reg").ToString());
regInfo = TempData?.Get("regInfo") as string;
}
}
Für kommende Preview-Versionen ist geplant, dass das Auslesen von Daten aus TempData über eine Annotation [SupplyParameterFromTempData], mit der man ein Property annotieren kann, vereinfacht wird [3].
Ausblick
In den kommenden Monaten wird es weitere Beiträge geben, in denen auch Neuerungen in .NET 11.0 abseits von Blazor besprochen werden.
Links & Literatur
Author
🔍 Frequently Asked Questions (FAQ)
1. Was ist neu für Blazor in .NET 11 Preview 1 und 2?
.NET 11 Preview 1 und 2 enthalten zahlreiche Neuerungen für Blazor, darunter Web Worker, umgebungsabhängiges Rendering, MathML-Rendering, BasePath, relative Navigation, IHostedService-Unterstützung und TempData für Blazor Static SSR.
2. Wie funktionieren Web Worker in Blazor 11?
Blazor 11 bietet eine eigene Projektvorlage für Web-Worker-Projekte. Damit kann Code in Blazor-WebAssembly-Anwendungen im Hintergrund ausgeführt werden, ohne den UI-Thread zu blockieren.
3. Was macht die neue Blazor-Komponente EnvironmentBoundary?
4. Wofür ist BasePath in Blazor 11 gedacht?
5. Was bringt TempData für Blazor Static SSR?
Seit .NET 11 Preview 2 kann Blazor Static SSR Daten zwischen HTTP-Anfragen mit TempData übergeben. Die Speicherung erfolgt wahlweise per Cookie oder im Session Storage des Browsers.




