Entwicklung & Code

.NET 10.0 Preview 6 bringt persistierte Circuits und Passkeys für Blazor


.NET 10.0 Preview 6 steht zum Download auf der .NET-Downloadseite bereit. Von Visual Studio 2022 gab es nur ein Bugfixing-Update von Version 17.14.8 auf 17.14.9. Weiterhin ist .NET 10.0 nicht direkt über das Visual-Studio-Setupprogramm installierbar. Wenn das .NET 10.0 SDK getrennt installiert wurde, kann man aber „.NET 10.0 Preview“ in den Auswahlmasken finden.




Dr. Holger Schwichtenberg ist Chief Technology Expert bei der MAXIMAGO-Softwareentwicklung. Mit dem Expertenteam bei www.IT-Visions.de bietet er zudem Beratung und Schulungen im Umfeld von Microsoft-, Java- und Webtechniken an. Er hält Vorträge auf Fachkonferenzen und ist Autor zahlreicher Fachbücher.

Blazor Server besitzt für jeden per Websocket angeschlossenen Browser einen sogenannten Circuit mit dem HTML-Inhalt und den Werten aller Variablen aller aktiven Razor Components. Bisher ging der Circuit komplett verloren, wenn die Verbindung mehrere Sekunden abreißt oder der Browser die Anwendung schlafen legt (z. B. bei mobilen Geräten), denn der Webserver erwartet zum Beibehalten des Circuits eine regelmäßige kurze Keep-Alive-Nachricht über die Websocket-Verbindung.




(Bild: coffeemill/123rf.com)

Verbesserte Klassen in .NET 10.0, Native AOT mit Entity Framework Core 10.0 und mehr: Darüber informieren Dr. Holger Schwichtenberg und weitere Speaker der Online-Konferenz betterCode() .NET 10.0 am 18. November 2025. Nachgelagert gibt es sechs ganztägige Workshops zu Themen wie C# 14.0, KI-Einsatz und Web-APIs.

In .NET 10.0 Preview 6 können Entwicklerinnen und Entwickler nun erstmalig, was das Konkurrenzprodukt Wisej.NET schon lange kann: Zustände über solche Abbrüche hinweg persistieren. Während dies in Wisej.NET jedoch automatisch erfolgt, müssen sie bei Blazor Server selbst etwas implementieren.

Die Persistenz von Circuit-Zuständen bei Blazor Server erfolgt im Standard nur im RAM des Webservers, kann aber optional auch in getrennten Cache-Prozessen (z. B. Redis) oder in einem Datenbankmanagementsystem (z. B. Microsoft SQL Server) erfolgen.

Blazor Server persistiert dabei nicht den kompletten Circuit und auch nicht alle Variablen einer Razor Component, sondern nur diejenigen Werte, die explizit in den Persistent Component State gelegt werden. Den gibt es in Blazor schon länger, um Werte vom Pre-Rendering an das interaktive Rendering zu übergeben. Vor .NET 10.0 mussten Entwicklerinnen und Entwickler den Persistent Component State aufwendig per Code setzen und lesen. Seit .NET 10.0 Preview 3 können sie die zu persistierenden Variablen einfach mit [SupplyParameterFromPersistentComponentState] annotieren.

Lesen Sie auch

Das folgende Listing zeigt ein aussagekräftiges Beispiel. Die Razor Component „State.razor“ persistiert via [SupplyParameterFromPersistentComponentState] eine Instanz der eigenen Klasse PageState. Die Klasse PageState umfasst eine aus einem Datenbankmanagementsystem via Entity Framework Core in OnInitialized() geladene Menge von Flugdaten sowie die Anzahl der Flüge, die angezeigt werden sollen. Zudem gibt es auf der Seite eine laufend aktualisierte Zeitanzeige. Zu Demonstrationszwecken wird jeweils beim Aktualisieren der Zeit die aktuelle Zeit auch in die Browserkonsole geschrieben.

Für .NET 10.0 Preview 7 hat Microsoft angekündigt, [SupplyParameterFromPersistentComponentState] in [PersistentState] umzubenennen. Im gleichen Ankündigungskasten steht auch „Blazor.pauseCircuit will be renamed to Blazor.pause. Blazor.resumeCircuit will be renamed to Blazor.resume.“. Im Praxistest zeigt sich aber, dass die neuen Namen schon in Preview 6 gelten.


@page "/State"
@using BO.WWWings
@using ITVisions
@inject ITVisions.Blazor.BlazorUtil util
 
Counter
 

Aktuelle Uhrzeit: @currentTime.ToString("HH:mm:ss")



Anzahl Flüge: @pageState.CurrentCount

@if (this.pageState?.FlightSet != null) {
    @foreach (Flight f in this.pageState.FlightSet.Take(this.pageState.CurrentCount)) {
  1. Flug #@f.FlightNo @f.FlightDate.ToShortTimeString() @f.Departure @f.Destination
  2. }
} @code { [SupplyParameterFromPersistentComponentState] // NEU ab .NET 10.0 public PageState pageState { get; set; } = null; private DateTime currentTime = DateTime.Now; private System.Timers.Timer? timer; private void IncrementCount() { if (pageState.CurrentCount < maxCount) pageState.CurrentCount++; } private void DecrementCount() { if (pageState.CurrentCount > 0) pageState.CurrentCount--; } int maxCount = 50; // > 58 Führt zum Absturz von Blazor Server (Stand Preview 5) protected override void OnInitialized() { // Uhrzeit-Timer starten timer = new System.Timers.Timer(1000); timer.Elapsed += (s, e) => { currentTime = DateTime.Now; InvokeAsync(StateHasChanged); util.Log(currentTime); }; timer.Start(); if (pageState == null) { // Lade Daten var renderMode = this.RendererInfo.Name; var logMessage = "Lade bis zu " + maxCount + " Flugdatensätze im Blazor-Rendermode=" + renderMode; DA.WWWings.WwwingsV1EnContext ctx = new(); var data = ctx.Flights.Take(maxCount).ToList(); // LINQ --> SQL pageState = new() { FlightSet = data, CurrentCount = 20, Created = DateTime.Now }; } } public void Dispose() { timer?.Dispose(); } /// public class PageState { public DateTime Created { get; set; } public DateTime LastChange { get; set; } public int _CurrentCount; public int CurrentCount { get { return _CurrentCount; } set { _CurrentCount = value; LastChange = DateTime.Now; } } public List FlightSet { get; set; } } }


Listing: State.razor

In App.razor wird zudem der folgende JavaScript-Code hinterlegt, der dafür sorgt, dass Circuits und die zugehörige Websocket-Verbindung beendet werden, wann immer die Webanwendung nicht mehr sichtbar ist und eine Wiederherstellung des Circuits und der Websocket-Verbindung erfolgen, sobald die Webanwendung wieder sichtbar wird. Auf diese Weise kann man bei Blazor Server einige Ressourcen (RAM und Rechenzeit) auf dem Webserver sparen und damit die Skalierbarkeit verbessern.




Listing: Pausieren und Wiederaufnahme von Circuits und zugehöriger Websocket-Verbindung

Die folgende Abbildung zeigt das Verhalten der Razor Component, die für kurze Zeit unsichtbar gemacht wurde (z. B. durch Öffnen eines anderen Browser-Tags oder Minimieren des Browsers). Zunächst gibt es im Sekundentakt eine Ausgabe in die Browserkonsole, jeweils beim Aktualisieren der Uhrzeit auf dem Bildschirm. Sobald die Razor Component nicht mehr sichtbar ist, wird der Circuit-Zustand mit dem JavaScript-Aufruf blazor.pause() persistiert und die Websocket-Verbindung beendet. 23 Sekunden später wird die Webseite wieder sichtbar. Via Blazor.resume() in JavaScript wird ein neuer Circuit erstellt, der persistente Zustand des Circuits geladen und die Websocket-Verbindung wieder aufgebaut. Die Razor Component wird neu gerendert und macht aus Benutzersicht dort weiter, wo sie aufgehört hat: Sie besitzt noch die gleichen Flugdaten wie zuvor (ohne diese neu aus dem Datenbankmanagementsystem laden zu müssen) und zeigt die gleiche Anzahl von Flügen. Die Uhrzeitausgabe in der Webseite und der Browserkonsole läuft weiter. Dass die Uhrzeitausgabe in der Browserkonsole unterbrochen ist, beweist, dass die Webanwendung zwischenzeitlich tatsächlich inaktiv war.



Persistierung eines Blazor-Server-Circuits

Im Startcode der Blazor-Server-Anwendung können Entwicklerinnen und Entwickler die Persistierung konfigurieren. Das folgende Listing zeigt die Festlegung der maximalen Anzahl der persistierten Circuits-Zustände auf 500 (Standard ist 1000), die Dauer der Persistierung im RAM auf 10 Sekunden (der Standard ist zwei Stunden) und die Dauer der Persistierung im Second-Level-Cache auf eine Stunde (der Standard ist zwei Stunden) sowie die Festlegung eines Microsoft SQL Servers als Second-Level-Cache zusätzlich zum RAM-Cache. Einen Hybrid Cache muss man dabei auch hinzufügen, denn Microsoft verwendet die Hybrid-Cache-Bibliothek (eingeführt in .NET 9.0) als Abstraktion.

Der Second-Level-Cache soll eine serverübergreifende Nutzung der persistierten Circuit-Daten erlauben, wenn man dem Tooltip der neuen Eigenschaft HybridPersistenceCache in der Klasse CircuitOptions glauben will: „Gets or sets the HybridCache instance to use for persisting circuit state across servers.“ Mangels Dokumentation zu dieser Aussage wurde die serverübergreifende Nutzung vom Autor dieses Beitrags aber bisher noch nicht getestet.


builder.Services.Configure(options =>
{
 options.PersistedCircuitInMemoryMaxRetained = 500; // The maximum number of circuits to retain. The default is 1,000 circuits.
 options.PersistedCircuitInMemoryRetentionPeriod = TimeSpan.FromSeconds(1); // The maximum retention period as a TimeSpan. The default is two hours. 
 options.PersistedCircuitDistributedRetentionPeriod = TimeSpan.FromSeconds(60); // The maximum retention period for distributed circuits. The default is two hours.
 options.DetailedErrors = true;
});
 
// --------------------- Hybrid Cache konfigurieren
var hyb = builder.Services.AddHybridCache(options => // optionale Einstellungen
{
 options.DefaultEntryOptions = new HybridCacheEntryOptions
 {
  Flags = HybridCacheEntryFlags.DisableCompression // nur als Beispiel für Einstellungen
 };
});
 
// --------------------- Second-Level-Cache konfigurieren
builder.Services.AddDistributedSqlServerCache(options =>
{
 options.ConnectionString = "Data Source=" + DB_SERVERNAME + ";Initial Catalog=NET_Cache;Integrated Security=True;Connect Timeout=30;Encrypt=False;Trust Server Certificate=True;Application Intent=ReadWrite;Multi Subnet Failover=False";
 options.SchemaName = "dbo";
 options.TableName = "Cache2";
});


Listing: Circuit-Zustandspersistenz konfigurieren



Persistierte Circuit-Zustände in einer Microsoft-SQL-Server-Tabelle, deren Aufbau durch den Distributed Cache Provider (SqlServerCache) vorgegeben ist.

Die hier beschriebene Persistenz funktioniert nur bei Blazor Server. Bei einem manuellen Browser-Refresh (Wisej.NET behält dabei den Zustand) oder manuellen Schließen des Browserfensters funktioniert die Persistenz nicht. Diese und weitere Einschränkungen dokumentiert Microsoft in den Release Notes. Dort findet man ebenso Hinweise, wie man Daten in per Dependency Injection injizierten Diensten (sofern es ein „Scoped Service“ ist) persistieren kann.

Die in Blazor eingebauten Eingabesteuerelemente (, , , , usw.) beherrschen seit der ersten Blazor-Version Eingabevalidierung gegen mit Datenvalidierungsannotationen versehene .NET-Objekte via . Allerdings funktionierte das bisher nur mit Objekten der obersten Ebene. In dem nachstehenden Beispiel, bei dem ein Company-Objekt ein Contact-Objekt und dieses wieder ein Address-Objekt enthält, wurden nur die Datenvalidierungsannotationen in der Klasse Company verwendet.


using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Validation;
 
namespace NET10_BlazorServer.Model;
 
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
[ValidatableType]
#pragma warning restore ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
public class Company
{
 public int CompanyID { get; set; }
 [Required(ErrorMessage = "Firmenname ist Pflichtfeld")]
 public string CompanyName { get; set; }
 [Required(ErrorMessage = "Gründungsdatum ist Pflichtfeld")]
 [Range(typeof(DateTime), "01/01/1900", "31/12/2024", ErrorMessage = "Datum muss zwischen 1900 und 2024 liegen.")]
 public DateTime Foundation { get; set; }
 public Contact Contact { get; set; } = new Contact();
}
 
public class Contact
{
 [Required(ErrorMessage = "Website ist Pflichtfeld")]
 public string Website { get; set; }
 
 [Required(ErrorMessage = "E-Mail ist Pflichtfeld")]
 [EmailAddress(ErrorMessage = "Ungültige E-Mail-Adresse")]
 public string Email { get; set; }
 
 public Address Address { get; set; } = new Address();
}
 
public class Address
{
 [Required(ErrorMessage = "Adresse ist Pflichtfeld")]
 public string AddressText { get; set; }
 
}


Listing: Datenmodell Company mit Unterobjekt Contact

In dem im nächsten Listing gezeigten Formular wurde daher bisher nur geprüft, ob die Eingaben bei Firma und Gründungsdatum stimmen, nicht aber bei Website und E-Mail sowie Adresse.


@page "/Validation"
@using BO.WWWings
@using ITVisions
@using System.ComponentModel.DataAnnotations
@using NET10_BlazorServer.Model
@inject ITVisions.Blazor.BlazorUtil util
 

Eingabevalidierung für komplexe Objekte in Blazor 10.0

company.CompanyName)" />

company.Foundation)" />

company.Contact.Website)" />

company.Contact.Email)" />

company.Contact.Address.AddressText)" />

@code { private Company company = new Company(); private EditContext editContext; protected override void OnInitialized() { editContext = new EditContext(company); } void Submit() { if (editContext.Validate()) { // Formular ist gültig util.Log("Formular ist gültig!"); } else { // Formular ist NICHT gültig util.Log("Formular ist NICHT gültig!"); } } }


Listing: Eingabeformular für das obige Datenmodell



Die Validierung findet bei Blazor 9.0 nur auf der obersten Ebene in der Objekthierarchie statt.

Ab .NET 10.0 Preview 6 können nun auch Unterobjekte (komplexe Objekte bzw. Objekthierarchien) validiert werden. Blazor verwendet dabei die gleiche Implementierung wie ASP.NET Core Minimal WebAPIs seit .NET 10.0 Preview 3. Die Implementierung liegt seit Preview 6 im neuen NuGet-Paket „Microsoft.Extensions.Validation„.

Dazu müssen Entwicklerinnen und Entwickler zwei Dinge hinzufügen: erstens einen Aufruf von builder.Services.AddValidation() im Startcode der Anwendung in Program.cs und zweitens müssen sie diese Zeilen in der Datei ergänzen, in der die Modellklassen stehen, wobei dies eine .cs-Datei sein muss. Es funktioniert nicht, wenn die Modellklassen in .razor-Dateien liegen:


#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
[ValidatableType]
#pragma warning restore ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.


Da ein Teil der Funktionen noch als „experimentell“ gilt, braucht man die Deaktivierung der Warnung.

Dies führt dazu, dass zur Entwicklungszeit ein Source Code Generator den Validierungscode erzeugt. Einsehen kann man den generierten Programmcode im Projekt im Ast „Dependencies/Analyzers/Microsoft.Extensions.Validation.ValidationsGenerator“.



In Blazor 10.0 können auch die Benutzereingaben für das Unterobjekt Contact validiert werden.

ASP.NET Core Identity ist eine von Microsoft vordefinierte Benutzerverwaltung mit Weboberfläche und REST-Diensten. Ab .NET 10.0 Preview 6 unterstützt ASP.NET Core Identity nun auch die Web Authentication (WebAuthn) API alias Passkeys.

Der einfachste Weg zur Passkey-Unterstützung führt über das Anlegen eines neuen Blazor-Projekts mit dem Authentifizierungstyp „Individual Accounts“. Man findet dann zusätzliche Dateien wie PasskeyOperation.cs, PasskeyInputModel.cs, Passkeys.razor und PasskeySubmit.razor im Projekt in den Ordnern /Component/Account, /Component/Account/Shared und /Component/Account/Pages/Manage. Diese Dateien kann man (wie bei ASP.NET Core üblich) an eigene Bedürfnisse anpassen. Die Passkey-Daten speichert ASP.NET Core Identity im Standard in seiner Microsoft-SQL-Server-Datenbank in der neuen Tabelle „AspNetUserPasskeys“.

Wie man bestehende Projekte und Datenbankschemata nachrüsten kann, will Microsoft laut „What’s new„-Dokument Mitte August 2025 zusammen mit .NET 10.0 Preview 7 veröffentlichen.

Die Passkey-Implementierung in .NET 10.0 unterstützt Resident Keys (Discoverable Credentials) und Non-Resident Keys, allerdings keine Attestation.



Anlegen eines neuen Blazor-Projekts mit dem Authentifizierungstyp „Individual Accounts“



Anlegen eines Passkeys innerhalb der Benutzerverwaltung



Man muss dem Passkey einen Namen geben.



Der Passkey wurde gespeichert.



Mit dem gespeicherten Passkey kann man sich wieder anmelden.



Tabelle „AspNetUserPasskeys“ im Microsoft SQL Server



Source link

Leave a Reply

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Beliebt

Die mobile Version verlassen