Das Tempo bleibt ordentlich
Neuerungen in Blazor 10.0, TEIL 2
Im ersten Teil dieses Beitrags [1] wurden bereits die Neuerungen insbesondere in den Bereichen Zustandsverwaltung, Navigation, Validierung und Sicherheit vorgestellt. Hier geht es um die übrigen Neuigkeiten.
Metriken und Tracing
Auf der Agenda steht die Verbesserung der Überwachung von Blazor-Anwendungen bei Microsoft bereits seit ASP.NET Core in .NET 8.0, erschienen im November 2023. Dort hat ASP.NET Core diverse OpenTelemetry-Metriken und -Traces erhalten. Seit Preview 5 von .NET 10.0 gibt es nun spezielle Metriken für Blazor-Anwendungen.
Listing 1 zeigt, wie diese neuen Überwachungsfunktionen zunächst im Blazor-Startcode in der Datei Program.cs zu aktivieren sind. Danach stehen die in Tabelle 1 aufgeführten Metriken und die in Tabelle 2 genannten Traces zur Verfügung.
Listing 1: Aktivierung der Metriken und Traces für Blazor in der Startdatei einer Blazor-Server-Anwendung
using ITVisions.Blazor;using NET10_BlazorServer.Components;using OpenTelemetry.Metrics;using OpenTelemetry.Trace; var builder = WebApplication.CreateBuilder(args); // Add services to the container.builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); builder.Services.AddBlazorUtilForBlazorServer(); #region Metriken und Tracesbuilder.Services.AddOpenTelemetry().WithMetrics(metrics =>{ metrics.AddAspNetCoreInstrumentation(); metrics.AddRuntimeInstrumentation(); metrics.AddMeter("Microsoft.AspNetCore.Components");
// Neu in ASP.NET Core 10.0 ab Preview 5 metrics.AddMeter(
"Microsoft.AspNetCore.Components.Lifecycle");
// Neu in ASP.NET Core 10.0 ab Preview 5 metrics.AddMeter(
"Microsoft.AspNetCore.Components.Server.Circuits");
// Neu in ASP.NET Core 10.0 ab Preview 5 metrics.AddConsoleExporter(); metrics.AddPrometheusExporter();
// OTLP für Prometheus}).WithTracing(tracing =>{ tracing.AddAspNetCoreInstrumentation(); tracing.AddHttpClientInstrumentation(); tracing.AddSource(
"Microsoft.AspNetCore.Components");
// Neu in ASP.NET Core 10.0 ab Preview 5 tracing.AddConsoleExporter(); tracing.AddOtlpExporter();
// Für Grafana Tempo, Jaeger etc.});#endregion var app = builder.Build(); // Configure the HTTP request pipeline.if (!app.Environment.IsDevelopment()){ app.UseExceptionHandler(
"/Error", createScopeForErrors: true); app.UseHsts();} app.UseHttpsRedirection(); app.UseAntiforgery(); app.MapStaticAssets();app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();app.MapPrometheusScrapingEndpoint();
// Metriken bereitstellen unter
// https://localhost:7119/metrics app.Run();
Tabelle 1: Metriken in Blazor 10.0
| Meter | Metrik | Beschreibung (Deutsch) |
| Microsoft.AspNetCore.Components | Aspnetcore.components.navigation | Zählt die Gesamtanzahl der Routenwechsel in deiner Blazor-Anwendung. |
| Microsoft.AspNetCore.Components | aspnetcore.components.event_handler | Misst die Dauer der Verarbeitung von Browser-Ereignissen, einschließlich eigener Logik. |
| Microsoft.AspNetCore.Components.Lifecycle | aspnetcore.components.update_parameters | Misst die Dauer der Verarbeitung von Komponentenparametern, einschließlich eigener Logik. |
| Microsoft.AspNetCore.Components.Lifecycle | aspnetcore.components.render_diff | Misst die Dauer der Rendering-Batches. |
| Microsoft.AspNetCore.Components.Server.Circuits | aspnetcore.components.circuit.active | Zeigt die Anzahl der aktuell im Speicher befindlichen aktiven Circuits. |
| Microsoft.AspNetCore.Components.Server.Circuits | aspnetcore.components.circuit.connected | Zählt die Anzahl der mit Clients verbundenen Circuits. |
| Microsoft.AspNetCore.Components.Server.Circuits | aspnetcore.components.circuit.duration | Misst die Lebensdauer eines Circuits und zählt deren Gesamtanzahl. |
Tabelle 2: Traces in Blazor 10.0
| Bereich | Tracing-Ereignis | Beschreibung (Deutsch) | Tags | Verknüpfungen |
| Circuit-Lebenszyklus | Microsoft.AspNetCore.Components.CircuitStart | Protokolliert die Initialisierung eines Circuits im Format Circuit {circuitId} | aspnetcore.components.circuit.id | HTTP-Aktivität |
| Navigation | Microsoft.AspNetCore.Components.RouteChange | Verfolgt Routenwechsel im Format Route {route} -> {componentType} | aspnetcore.components.circuit.id, aspnetcore.components.type, aspnetcore.components.route | HTTP-Trace, Circuit-Trace |
| Event-Verarbeitung | Microsoft.AspNetCore.Components.HandleEvent | Protokolliert Ereignisverarbeitung im Format Event {attributeName} -> {componentType}.{methodName} | aspnetcore.components.circuit.id, aspnetcore.components.type, aspnetcore.components.method, aspnetcore.components.attribute.name, error.type | HTTP-Trace, Circuit-Trace, Router-Trace |
Die Metriken in Microsoft.AspNetCore.Components.Server.Circuits und die Traces in Microsoft.AspNetCore.Components.CircuitStart sind dabei nur in Blazor-Server-Anwendungen vorhanden, denn nur dort gibt es Circuits.
Bild 1 zeigt, wie auf Basis der neuen OpenTelemetry-Metriken in Blazor das Open-Source-Monitoring- und -Alerting-System Prometheus der Cloud Native Computing Foundation (CNCF) [2] die aktiven Circuits einer Blazor Server-Anwendung visualisiert.
Bild 2 zeigt die Darstellung von drei Blazor-Metriken aus Prometheus in einem Dashboard in der Datenvisualisierungsanwendung Grafana [3].
Grafische Darstellung der Anzahl der aktiven Circuits einer Blazor-Server-Anwendung in
Prometheus (Bild 1)
Grafana visualisiert die Anzahl der Navigationen zu einzelnen Seiten, der Anzahl der aktiven Circuits und die Circuit-Lebensdauer nach Größenklassen (Bucket) (Bild 2)
AutorDiagnosedaten in Blazor WebAssembly
Eine größere neue Diagnose-Funktion in Blazor 10.0 bekam die WebAssembly-basierte Variante von Blazor, die als Single-Page-App innerhalb des Webbrowsers läuft: Entwicklerinnen und Entwickler können damit im Webbrowser zur Laufzeit Diagnosedaten über Performance und Speicherinhalt sammeln.
Um die neuen Funktionen nutzen zu können, müssen Entwicklerinnen und Entwickler die .NET WebAssembly Build Tools (wasm-tools) zunächst in der aktuellen .NET-10.0-Version als .NET-SDK-Workload installieren:
dotnet workload install wasm-tools
In den .NET WebAssembly Build Tools gibt es zwei neue Funktionen:
- Performance-Analyse in den Browser-Entwicklerwerkzeugen („F12-Tools“)
- Sammeln und Herunterladen von Trace-Dateien und Memory Dumps aus dem Browser
Für die Nutzung der Performance-Analyse in den Browser-Entwickler-Werkzeugen („F12-Tools“) stehen die in Tabelle 3 gezeigten Einstellungen in der Projektdatei zur Verfügung.
Tabelle 3: Projekteinstellungen für Blazor-WebAssembly-Diagnosedaten
| Eigenschaft | Standardwert | Setze auf … | Beschreibung |
| <WasmProfilers> | Kein Wert | browser | Legt fest, welche Profiler verwendet werden sollen. Der Browser-Profiler ermöglicht die Integration mit den Entwicklerwerkzeugen des Browsers. |
| <WasmNativeStrip> | true | false | Deaktiviert das Entfernen (Stripping) der nativen ausführbaren Datei. |
| <WasmNativeDebugSymbols> | true | true | Aktiviert das Erstellen mit nativen Debug-Symbolen. |
(Quelle: Microsoft)
Mit den Einstellungen aus Listing 2 erhält man in den Browser-Entwicklerwerkzeugen in der Registerkarte Performance eine Ansicht Timings mit der Ablauffolge der aufgerufenen .NET-Methoden (siehe Bild 3).
Listing 2: Einstellungen im Blazor WebAssembly-Client-Projekt
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> ... <!-- NEU: Blazor WebAssembly Runtime Diagnostics --> <PropertyGroup> <!-- Für Anzeige von "Timings" im Performance-Tab der Browser-Developer-Tools--> <WasmProfilers>browser</WasmProfilers> <WasmNativeStrip>false</WasmNativeStrip> <WasmNativeDebugSymbols>true </WasmNativeDebugSymbols> </PropertyGroup> </Project>
Aufrufhierarchie der Methoden im Browser mit Angabe des Zeitverbrauchs in den Browser-Entwicklerwerkzeugen unter „Performance | Timings“ (Bild 3)
AutorMit den Einstellungen aus Listing 3 (siehe auch Tabelle 4) in der Projektdatei erlaubt man das Sammeln von Trace-Daten und Memory-Dumps einer Blazor-WebAssembly-Anwendung im Webbrowser. Die resultierenden Daten werden als .nettrace-Datei im Browser heruntergeladen.
Listing 3: Einstellungen im Blazor WebAssembly-Client-Projekt
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> ... <!-- NEU: Blazor WebAssembly Runtime Diagnostics --> <PropertyGroup> <!-- Für Trace- und Memory-Dump-Files --> <WasmPerfTracing>true</WasmPerfTracing> <WasmPerfInstrumentation>all </WasmPerfInstrumentation> <EventSourceSupport>true</EventSourceSupport> <MetricsSupport>true</MetricsSupport> </PropertyGroup> </Project>
Tabelle 4: Projekteinstellungen für Blazor-WebAssembly-Diagnosedaten
| Eigenschaft | Standardwert | Setze auf … | Beschreibung |
| <WasmPerfTracing> | false | true | Aktiviert die Unterstützung für WebAssembly-Leistungstracing. |
| <WasmPerfInstrumentation> | none | all | Aktiviert die Instrumentierung, die für den Sampling-Profiler erforderlich ist. |
| <EventSourceSupport> | false | true | Aktiviert die Unterstützung für EventPipe. |
| <MetricsSupport> | false | true | Aktiviert die Unterstützung für System.Diagnostics.Metrics. |
Quelle: Microsoft
Im Anschluss daran kann können Entwicklerinnen und Entwickler mittels JavaScript-APIs auf die Diagnosedaten zugreifen. Man gibt dafür die folgenden Befehle in der Browser-Konsole in den Browser-Entwicklerwerkzeugen („F12-Tools“) ein:
- CPU-Nutzung sammeln für die nächsten fünf Sekunden: globalThis.getDotnetRuntime(0).collectCpuSamples({durationSeconds: 5});
- Metriken sammeln für die nächsten fünf Sekunden: globalThis.getDotnetRuntime(0).collectPerfCounters({durationSeconds: 5});
- Speicherabbildung erstellen: globalThis.getDotnetRuntime(0).collectGcDump();
Entwicklerinnen und Entwickler erhalten die gesammelten Daten in einer .nettrace-Datei im Downloads-Ordner des Webbrowsers, siehe Bild 4.
Die .nettrace-Dateien mit CPU- und Performance-Counter-Dateien kann man ohne weitere Schritte in Visual Studio öffnen, indem man die Datei via File | Open öffnet oder einfach per Drag and Drop auf den Dateibereich von Visual Studio zieht, siehe Bild 5.
Während man .nettrace-Dateien mit CPU- und Performance-Counter-Dateien direkt in Visual Studio öffnen kann, gilt es eine Speicherabbilddatei zunächst mit dem .NET-CLI-Werkzeug dotnet-gcdump [4] in eine .gcdump-Datei für Visual Studio zu konvertieren.
Die Installation der aktuellen Version des Werkzeugs dotnet-gcdump erfolgt mit diesem Kommandozeilenbefehl:
dotnet tool install --global dotnet-gcdump
Die Konvertierung einer .nettrace-Datei in eine .gcdump-Datei geschieht dann so:
dotnet-gcdump convert Dateiname.nettrace
Die erzeugte .gcdump-Datei kann man anschließend in Visual Studio betrachten (Bild 6). Man sieht, welche Typen wie oft instanziert wurden und wie viele Bytes sie verbrauchen. Die Spalte Size (Bytes) bezeichnet dabei die Bytes, die der Typ selbst verbraucht. Die Spalte Inclusive Size (Bytes) umfasst zusätzlich auch die von Unterobjekten verbrauchten Speicherplätze.
Herunterladen der Memory-Dump-Datei im Format .nettrace (Bild 4)
Autor
Nach Klick auf System.String.Join() sieht man die Anzahl der Aufrufe und die CPU-Nutzungszeit (Bild 5)
Autor
Betrachten der Memory-Dump-Datei (.gcdump) in Visual Studio (Bild 6)
AutorFingerprinting für systemeigene JavaScript-Datei
Die bei Blazor mitgelieferte JavaScript-Datei (je nach verwendeter Projektvorlage: blazor.web.js, blazor.server.js beziehungsweise blazor.webassembly.js) erhält in .NET 10.0 die bereits in .NET 9.0 eingeführte Möglichkeit zur Komprimierung (Gzip zur Entwicklungszeit, Brotli nach dem Publishing) sowie einen Fingerabdruck des Inhalts im Dateinamen, der verhindert, dass der Webbrowser alte Versionen aus dem Browser-Cache verwendet. Daher steht in der aktualisierten Projektvorlage Blazor Web App in App.razor nun ein @Assets im entsprechenden <script>-Tag.
<script src="@Assets["_framework/blazor.web.js"]"> </script>
Das Fingerprinting funktioniert in .NET 10.0 auch für die in Blazor eingebaute JavaScript-Datei in den sogenannten Standalone-Blazor-WebAssembly-Projekten (kurz: „blazorwasm“), welche nicht in ASP.NET Core betrieben werden, sondern stattdessen auf einem statischen Webserver gehostet sein können.
dotnet new blazorwasm -n ITVisionsDemo
Hier muss man aber mehr tun. Man verwendet in den Projekteinstellungen
<OverrideHtmlAssetPlaceholders>true </OverrideHtmlAssetPlaceholders>
und im HTML-Kopfbereich in index.html:
<link rel=“preload“ id=“webassembly“ /> <script type=“importmap“></script>
Das <script>-Tag ist dann:
<script src=“_framework/blazor.webassembly#[
.{fingerprint}].js“></script
>
Wichtig: In bestehenden Projekten müssen Entwicklerinnen und Entwickler die Änderungen manuell vornehmen, um die Optimierungen zu nutzen. Der Build-Prozess sorgt dafür, dass einige Ersetzungen stattfinden; siehe die grünen Rahmen in Bild 7 und Bild 8.
Ersetzungen in Index.html durch den Build-Prozess (Teil 1) (Bild 7)
Autor
Ersetzungen in Index.html durch den Build-Prozess (Teil 1) (Bild 7)
AutorUmgebungen bei Blazor WebAssembly
In Blazor-WebAssembly-Anwendungen mussten Entwicklerinnen und Entwickler die Umgebungsdefinition (Development, Staging, Production) bisher per HTTP-Header vornehmen (siehe [5]). Nun nimmt Microsoft beim Kompilieren von Blazor WebAssembly-Anwendungen automatisch folgende Umgebungen an:
- Bei dotnet build: Development
- Bei dotnet publish: Production
Entwicklerinnen und Entwickler können die Umgebungsart auch per Projekteinstellung in der Projektdatei (.csproj) explizit setzen, zum Beispiel für die Umgebung Staging:
<WasmApplicationEnvironmentName>Staging </WasmApplicationEnvironmentName>
Individuelle Verbindungsproblemdialoge bei Blazor Server
Blazor Server stellt schon immer einen modalen Dialog dar, falls ein Abbruch der WebSockets-Verbindung zwischen Webbrowser und Webserver erfolgt ist und eine Wiederaufnahme versucht wird. Diese Darstellung war allerdings bisher von Microsoft vorgegeben und von Entwicklerinnen und Entwicklern nicht anpassbar, siehe Bild 9.
In der Projektvorlage Blazor Web App findet man seit .NET 10.0 Preview 2 im Ordner Layout eine neue Razor Component mit Namen ReconnectModal.razor (siehe Listing 5).
Verbindungsproblemdialog für Blazor Server. Mit diesem Aussehen gibt es ihn seit Blazor 9.0 (Bild 9)
AutorListing 5: Standardinhalt von ReconnectModal.razor
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script> <dialog id="components-reconnect-modal" data-nosnippet> <div class="components-reconnect-container"> <div class="components-rejoining-animation" aria-hidden="true"> <div></div> <div></div> </div> <p class= "components-reconnect-first-attempt-visible"> Rejoining the server... </p> <p class= "components-reconnect-repeated-attempt-visible"> Rejoin failed... trying again in <span id= "components-seconds-to-next-attempt"></span> seconds. </p> <p class="components-reconnect-failed-visible"> Failed to rejoin.<br />Please retry or reload the page. </p> <button id="components-reconnect-button" class= "components-reconnect-failed-visible"> Retry </button> </div> </dialog>
Diese Komponente enthält die Standarddarstellung des modalen Fensters bei Verbindungsproblemen. Mit dieser neuen Razor Component können Entwicklerinnen und Entwickler in Blazor-10.0-basierten Webanwendungen also in den folgenden drei Fällen Einfluss auf die Texte und die Darstellung nehmen:
- Wiederherstellung wird erstmals versucht
- Wiederherstellung ist fehlgeschlagen und wird erneut versucht
- Wiederherstellung ist endgültig fehlgeschlagen nach Ablauf des Timeouts
Zu der Razor Component ReconnectModal.razor gehören auch eine CSS-Datei ReconnectModal.razor.css sowie eine JavaScript-Datei ReconnectModal.razor.js. Auch diese Dateien sind beliebig anpassbar. ReconnectModal.razor wird am Ende der Datei MainLayout.razor eingebunden via <ReconnectModal /> (siehe Listing 6) und kann auch komplett ersetzt werden.
Listing 6: Ausschnitt aus App.razor
... <body> <Routes @rendermode="InteractiveServer" /> <ReconnectModal /> <script src="@Assets["_framework/blazor.web.js"]"> </script> </body> ...
Listing 7 und das zugehörige Bild 10 zeigen eine Anpassung des Reconnect-Dialogs.
Listing 7: Eigene Version von ReconnectModal.razor
<script type="module" src="@Assets["Components/Layout/ ReconnectModal.razor.js"]"></script> <dialog id="components-reconnect-modal" data-nosnippet> <div class="components-reconnect-container"> <div class="components-rejoining-animation" aria-hidden="true"> <div></div> <div></div> </div> <p class= "components-reconnect-first-attempt-visible"> @* Rejoining the server... *@ Blazor Server hat derzeit keinen Kontakt zum Webserver. Es wird versucht, die Verbindung wiederherzustellen... </p> <p class="components-reconnect-repeated-attempt- visible"> @* Rejoin failed... trying again in <span id= "components-seconds-to-next-attempt"></span> seconds. *@ Verbindungswiederherstellung: Erneuter Versuch in <span id="components-seconds-to-next-attempt"> </span> Sekunden. </p> <p class="components-reconnect-failed-visible"> @* Failed to rejoin.<br />Please retry or reload the page. *@ Blazor Server den Kontakt zum Webserver nicht wiederherstellen können. </p> <button id="components-reconnect-button"> Erneut verbinden </button> oder <button onclick="location.reload();"> Neustart </button> </div> </dialog>
Response Streaming beim HttpClient in Blazor WebAssembly
In Blazor WebAssembly-basierten Anwendungen hat Microsoft in der Klasse System.Net.Http.HttpClient im Standard nun das Response Streaming aktiviert, um die Leistung zu erhöhen und den Speicherbedarf zu verringen (vergleiche Listing 8).
Angepasster Verbindungsproblemdialog (Bild 10)
AutorListing 8: HttpClient mit optionalem Response Streaming
@page "/HttpStreaming"
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using System.Net.Http
@using System.Diagnostics
@inject HttpClient HttpClient
<h1>HTTP Response Streaming</h1>
<button type="button" class="btn btn-primary"
@onclick="Get">Laden</button>
<button type="button" class="btn btn-warning"
@onclick="Stop">Abbrechen</button>
<input type="checkbox" @bind="streamingEnabled" />
Response Streaming aktivieren
<hr />
<p>Status: @status</p>
<p>Response Streaming aktiv: @streamingEnabled</p>
<p>Stream-Type: @streamType</p>
<p>Bytes gelesen: @byteCount</p>
<p>Dauer: @sw.ElapsedMilliseconds ms</p>
@code {
int byteCount;
string status = "";
string streamType = "Unknown";
bool streamingEnabled = false;
// Set to true to enable streaming
Stopwatch sw = new Stopwatch();
CancellationTokenSource cts;
// Laden
async Task Get()
{
status = "Lade...";
sw.Reset();
sw.Start();
cts = new CancellationTokenSource();
using var request = new HttpRequestMessage(
HttpMethod.Get, "https://www.it-visions.de/
produkte/pdf/www.IT-Visions.de_Firmenbrosch%
C3%BCre.pdf");
if (streamingEnabled)
request.SetBrowserResponseStreamingEnabled(true);
else request.SetBrowserResponseStreamingEnabled(
false);
// Beim Streaming; HttpCompletionOption.Response
// HeadersRead / Der Standardwert ist
// ResponseContentRead, was bedeutet, dass der
// Vorgang erst abgeschlossen werden soll, nachdem
// die gesamte Antwort, einschließlich des
// Inhalts, aus dem Socket gelesen wurde.
using var response = await HttpClient.SendAsync(
request, streamingEnabled ?
HttpCompletionOption.ResponseHeadersRead :
HttpCompletionOption.ResponseContentRead);
using Stream stream =
await response.Content.ReadAsStreamAsync();
streamType = stream.GetType().FullName;
// Get the type of the stream:
// System.IO.MemoryStream oder System.Net.Http.
// HttpConnection+ContentLengthReadStream
// Blockweises Lesen des Streams
var bytes = new byte[10000];
while (!cts.Token.IsCancellationRequested)
{
var read = await stream.ReadAsync(
bytes, cts.Token);
if (read == 0) // Ende des Streams erreicht
{
sw.Stop();
status = "Fertig!";
return;
}
byteCount += read;
// UI-Update
StateHasChanged();
await Task.Delay(1);
}
}
// Abbrechen
void Stop()
{
sw.Stop();
cts?.Cancel();
}
}
Listing 8: HttpClient mit optionalem Response Streaming
@page "/HttpStreaming"
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using System.Net.Http
@using System.Diagnostics
@inject HttpClient HttpClient
<h1>HTTP Response Streaming</h1>
<button type="button" class="btn btn-primary"
@onclick="Get">Laden</button>
<button type="button" class="btn btn-warning"
@onclick="Stop">Abbrechen</button>
<input type="checkbox" @bind="streamingEnabled" />
Response Streaming aktivieren
<hr />
<p>Status: @status</p>
<p>Response Streaming aktiv: @streamingEnabled</p>
<p>Stream-Type: @streamType</p>
<p>Bytes gelesen: @byteCount</p>
<p>Dauer: @sw.ElapsedMilliseconds ms</p>
@code {
int byteCount;
string status = "";
string streamType = "Unknown";
bool streamingEnabled = false;
// Set to true to enable streaming
Stopwatch sw = new Stopwatch();
CancellationTokenSource cts;
// Laden
async Task Get()
{
status = "Lade...";
sw.Reset();
sw.Start();
cts = new CancellationTokenSource();
using var request = new HttpRequestMessage(
HttpMethod.Get, "https://www.it-visions.de/
produkte/pdf/www.IT-Visions.de_Firmenbrosch%
C3%BCre.pdf");
if (streamingEnabled)
request.SetBrowserResponseStreamingEnabled(true);
else request.SetBrowserResponseStreamingEnabled(
false);
// Beim Streaming; HttpCompletionOption.Response
// HeadersRead / Der Standardwert ist
// ResponseContentRead, was bedeutet, dass der
// Vorgang erst abgeschlossen werden soll, nachdem
// die gesamte Antwort, einschließlich des
// Inhalts, aus dem Socket gelesen wurde.
using var response = await HttpClient.SendAsync(
request, streamingEnabled ?
HttpCompletionOption.ResponseHeadersRead :
HttpCompletionOption.ResponseContentRead);
using Stream stream =
await response.Content.ReadAsStreamAsync();
streamType = stream.GetType().FullName;
// Get the type of the stream:
// System.IO.MemoryStream oder System.Net.Http.
// HttpConnection+ContentLengthReadStream
// Blockweises Lesen des Streams
var bytes = new byte[10000];
while (!cts.Token.IsCancellationRequested)
{
var read = await stream.ReadAsync(
bytes, cts.Token);
if (read == 0) // Ende des Streams erreicht
{
sw.Stop();
status = "Fertig!";
return;
}
byteCount += read;
// UI-Update
StateHasChanged();
await Task.Delay(1);
}
}
// Abbrechen
void Stop()
{
sw.Stop();
cts?.Cancel();
}
}
Zuvor musste man das Response Streaming manuell aktivieren. Dies geschah mit
request.SetBrowserResponseStreamingEnabled(true);
In .NET 10.0 ist nun das Response Streaming automatisch aktiv, und der Stream-Typ ist damit ein anderer.
Durch das im Standard aktive Response Streaming sind allerdings nur noch asynchrone Operationen und keine synchronen Operationen mehr möglich. Ein Aufruf der synchronen Send()-Methode HttpClient.Send(request) führt dann zum Laufzeitfehler „Operation is not supported on this platform.“
Diese Verhaltensänderung gehört zu den Breaking Changes in .NET 10.0, die unter [6] dokumentiert sind.
Das Response Streaming im HttpClient ist in .NET 10.0 aber auf Wunsch auch deaktivierbar mit:
request.SetBrowserResponseStreamingEnabled(false);
Alternativ ist die globale Deaktivierung in der Projektdatei möglich:
<WasmEnableStreamingResponse>false </WasmEnableStreamingResponse>
Individuelle Zeilenformatierung im QuickGrid
Im Tabellensteuerelement <QuickGrid> können Entwicklerinnen und Entwickler nun eine Methode beim neuen Attribut RowClass angeben, welche bei jeder erzeugten Tabellenzeile aufgerufen wird, um die CSS-Klasse der Zeile individuell auf Basis der Daten zu setzen.
Die Eigenschaft RowClass erwartet als Rückgabewert von der Methode den Namen einer CSS-Klasse, die Sie selbst definieren müssen.
In Listing 9 wird die Methode ApplyRowClass() aufgerufen, die Flüge mit weniger als zehn freien Plätzen orange markiert und bei ausgebucht (0) oder Überbuchung (<0) rot (Bild 11).
Listing 9: Neue RowClass-Eigenschaft im Steuerelemente QuickGrid
<style>
.red {
color: red;
}
.orange {
color: orange;
}
.ok {
color: black;
}
.scrollable {
overflow-y: scroll;
max-height: 400px; /* Höhe nach Bedarf anpassen */
}
</style>
<div class="row">
<div class="col scrollable">
<QuickGrid @ref="flightGrid" RowClass=
"ApplyRowClass" ItemsProvider="@itemsProvider"
TGridItem="BO.WWWings.Flight" Virtualize="true"
OverscanCount="@overscanCount">
<PropertyColumn Property="@(p => p.FlightNo)"
Title="FlugNr" Sortable="true" />
<PropertyColumn Property="@(p => p.Departure)"
Title="Abflugort" Sortable="true">
<ColumnOptions>
<input type="search" @bind="departureFilter"
placeholder="Filter by Departure"
@bind:after="@(() => flightGrid
.HideColumnOptionsAsync())" />
</ColumnOptions>
</PropertyColumn>
<PropertyColumn Property="@(p => p.Destination)"
Title="Zielort" Sortable="true" />
<PropertyColumn Property="@(p => p.FlightDate)"
Title="Datum" Format="dd.MM.yyyy"
Sortable="true" />
<PropertyColumn Property="@(p => p.FreeSeats)"
Title="Freie Plätze" />
</QuickGrid>
@code {
private string ApplyRowClass(BO.WWWings.Flight f)
{
if (f.FreeSeats <= 0) return "red";
if (f.FreeSeats <= 10) return "orange";
return "ok";
}
}
</div>
Datenbasierte Formatierung im QuickGrid mit der Eigenschaft
RowClass (Bild 11)
Der Aufruf HideColumnOptionsAsync() sorgt dafür, dass die geöffnete Filtereingabe sich automatisch wieder schließt, wenn die Eingabetaste gedrückt wird (Bild 12)
AutorAutomatisches Schließen des Filters im QuickGrid
Zusätzlich gibt es im <QuickGrid>-Steuerelement nun eine neue Methode HideColumnOptionsAsync(), die Entwicklerinnen und Entwickler aufrufen können, damit sich die Eingabe von Filterkriterien schließt, wenn der Filter aktiviert wird (siehe Listing 10 und Bild 12). Diese Methode hieß in den Preview-Versionen 2 und 3 noch anders: CloseColumnOptionsAsync().
Listing 10: Einsatz von HideColumnOptionsAsync() im Steuerelement <QuickGrid>
<QuickGrid @ref="flightGrid" RowClass="ApplyRowClass" ItemsProvider="@itemsProvider" TGridItem="BO.WWWings.Flight" Virtualize="true" OverscanCount="@overscanCount"> <PropertyColumn Property="@(p => p.FlightNo)" Title="FlugNr" Sortable="true" /> <PropertyColumn Property="@(p => p.Departure)" Title="Abflugort" Sortable="true"> <ColumnOptions> <input type="search" @bind="departureFilter" placeholder="Filter by Departure" @bind:after="@(() => flightGrid .HideColumnOptionsAsync())" /> </ColumnOptions> </PropertyColumn> <PropertyColumn Property="@(p => p.Destination)" Title="Zielort" Sortable="true" /> <PropertyColumn Property="@(p => p.FlightDate)" Title="Datum" Format="dd.MM.yyyy" Sortable="true" /> </QuickGrid>
Erweiterte C#-JavaScript-Interoperabilität
In Blazor 10.0 erweitert Microsoft die Schnittstellen für die Interoperabilität zwischen C# und JavaScript (IJSRuntime, IJSInProcessRuntime, IJSObjectReference und IJSInProcessObjectReference) um neue Mitglieder.
Mit den neuen Mitgliedern können Entwicklerinnen und Entwickler
- eine Instanz von einem JavaScript-Objekt erzeugen und dabei Konstruktorparameter übergeben (InvokeNew() und InvokeNewAsync()) sowie
- Variablen oder Objekteigenschaften lesen (GetValue() und GetValueAsync()) beziehungsweise
- Objekteigenschaften setzen (SetValue() sowie SetValueAsync()).
Bisher konnte man von C# aus lediglich Funktionen in JavaScript mit Invoke(), InvokeVoid(), InvokeAsync() und InvokeVoidAsync() aufrufen. Die neuen Interoperabilitätsmethoden vermeiden in einigen Fällen, dass eine JavaScript-Wrapper-Funktion geschrieben werden muss; stattdessen kann man nun direkt JavaScript-Objekte erzeugen und Werte lesen/schreiben (Bild 13). Ein Beispiel sieht man in den Listings 11 und 12.
Ausgabe des Beispiels (Bild 13)
AutorListing 11: Eine JavaScript-Klasse in der Datei /wwwroot/JSUtil.js
window.JSUtil = class {
constructor(text) {
// zwei Eigenschaften der JavaScript-Klasse
this.text = text;
this.caseSensitive = false;
}
// Hole Textlänge
getTextLength() {
return this.text.length;
}
// Prüfe, ob Text im aktuellen URL vorkommt
containsTextInUrl() {
const url = window.location.href;
if (this.caseSensitive) return url.includes(
this.text);
else return url.toLowerCase().includes(
this.text.toLowerCase());
}
}
Listing 12: Einsatz der neuen Interoperabilitätsmethoden von C# nach JavaScript
@inject IJSRuntime JSRuntime
@page "/interop"
@using ITVisions.Blazor
@inject BlazorUtil util
<script src="JSUtil.js"></script>
<h3>Blazor 10.0 C#/JavaScript-Interop</h3>
<hr noshade />
<button @onclick="Aktion">JavaScript aufrufen</button>
<br />
<ul>
@((MarkupString)Ausgabe)
</ul>
@code {
protected override Task OnInitializedAsync()
{
return base.OnInitializedAsync();
}
public string Ausgabe { get; set; } = "";
async Task Aktion()
{
// Eine einfache Variable setzen im Browser
await JSRuntime.SetValueAsync<Guid>("Token",
Guid.NewGuid());
var token = await JSRuntime.GetValueAsync<string>(
"Token");
Ausgabe = "<li>Aktuelles Token: " + token +
"</li>";
// Eine Eigenschaft des vorhandenen
// document-Objekts ändern
var titleOld = await JSRuntime.GetValueAsync<string>(
"document.title");
var titleNew = "Interop-Demo: " +
DateTime.Now.ToLongTimeString();
await JSRuntime.SetValueAsync<string>(
"document.title", titleNew);
Ausgabe += "<li>Title geändert von '" + titleOld +
"' auf '" + titleNew + "'" + "</li>";
// Ein neues JavaScript-Objekt erstellen und damit
// arbeiten
var jsObj = await JSRuntime.InvokeNewAsync(
"JSUtil", "Localhost");
var text = await jsObj.GetValueAsync<string>(
"text");
var textLength = await jsObj.InvokeAsync<int>(
"getTextLength");
await jsObj.SetValueAsync<bool>("caseSensitive",
true);
var caseSensitive = await jsObj.GetValueAsync<bool>(
"caseSensitive");
var containsTextInUrl = await
jsObj.InvokeAsync<bool>("containsTextInUrl");
Ausgabe += $"<li>Die Zeichenkette {text} hat
{textLength} Zeichen und kommt{(containsTextInUrl
? "" : " NICHT")} in der aktuellen URL vor. Der
Vergleich war '{(caseSensitive ? "casesensitive"
: "caseinsensitive")}'.</li>";
}
}
Weitere Pläne für Blazor
Wenn man auf GitHub stöbert [7], findet man in der Road-
map für ASP.NET Core nur noch wenige weitere Pläne von Microsoft für Blazor 10.0. Microsoft will beispielsweise Scaffolding-Werkzeuge einführen für Authentifizierung in Visual Studio und bei dotnet scaffold zur Erstellung von ASP.NET Core Identity Web API Endpoints und Entra-ID-Authentifizierung sowie Authentifizierung in Blazor Hybrid.
Man findet in der Liste (wie immer) allerdings auch Ideen, die leider auf die nächste .NET-Version verschoben wurden, wie zum Beispiel, dass es eine bessere Integration des <QuickGrid>-Steuerelements mit Entity Framework Core geben soll.
[1] Holger Schwichtenberg, Weiterhin mit ordentlich Tempo, dotnetpro 10-11/2025, Seite 19 ff.,
[2] Prometheus
[3] Grafana
[4] Heapanalysetool dotnet-gcdump
[5] Microsoft Learn, ASP.NET Core Blazor environments, Set the client-side environment via header
[6] Breaking Changes in .NET 10.0
[7] Microsoft ASP.NET Core Roadmap for .NET 10