Ich berichtete in Windows Developer bereits über vorherige Vorschauversionen von .NET 8.0:
- über die Previews 1 und 2 in Ausgabe 6.2023 sowie
- über die Previews 3 und 4 in Ausgabe 8.2023.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Bis Einreichung dieses Artikels sind noch Preview 5 (13. Juni 2023), Preview 6 (11. Juli 2023) und Preview 7 (8. August 2023) erschienen.
Als geplanter Erscheinungstermin für die fertige Version von .NET 8.0 wurde inzwischen der 14. November 2023 verkündet. Bis dahin wird es noch zwei weitere Vorschauversionen mit dem Titel „Release Candidate“ geben. Üblich ist, dass Microsoft mit den Release-Candidate-Versionen eine Go-live-Lizenz verbindet, das heißt ab dann ist der produktive Einsatz von .NET 8.0 erlaubt. Das heißt aber nicht, dass es in der Release-Candidate-Phase keine Änderungen mehr geben wird.
Blazor United: Ein flexibles Blazor für alle Webarchitekturen
Microsoft hatte am 24. Januar 2023 auf YouTube [1] die Integration von Blazor Server und Blazor WebAssembly zu Blazor United verkündet, sodass aus Benutzerinnen- und Benutzersicht zur Laufzeit ein nahtloser Übergang der Rendering-Arten entsteht.
Blazor United umfasst neben den bisher verfügbaren Blazor-Architekturen für den Webbrowser (Blazor Server und Blazor WebAssembly) die folgenden fünf neuen Architekturen:
- Blazor Server-side Rendering (Blazor SSR)
- Blazor Server-side Rendering mit Streaming (Blazor SSR mit Streaming)
- Hosting von Blazor Server innerhalb von Blazor SSR (anstelle des bisherigen Hostings von Blazor Server in ASP.NET Core Razor Pages)
- Hosting von Blazor WebAssembly innerhalb von Blazor SSR (anstelle des bisherigen Hostings von Blazor Server in ASP.NET Core Razor Pages)
- Wechsel einer Komponente von Blazor Server zu Blazor WebAssembly
Spannend ist, dass bei Blazor United diese Architekturmodelle innerhalb eines einzigen Projekts mischbar sind und zwar nicht nur pro Seite, sondern pro einzelner Komponente. Das bedeutet zum Beispiel, dass man eine Webanwendung erschaffen kann, in der alle statischen Inhalte auf dem Server gerendert werden und nur die Teile, die tatsächlich Interaktivität benötigen (z. B. Eingabeformulare, Suchdialoge) dann mit Blazor Server oder Blazor WebAssembly arbeiten. So wird Blazor in Version 8.0 zu einem deutlich universelleren Ansatz für Webanwendungen. Blazor eignet damit auch für öffentliche Websites (z. B. Firmen- und Produktpräsentationen, Webshops) und nicht nur für Intra- und Extranetanwendungen.
Blazor SSR als Nachfolger für MVC und Razor Pages
Der Begriff Server-side Rendering (Blazor SSR) kann verwirren. Erste Erfahrungen in der Praxis zeigen, dass viele Entwickler:innen denken: „Das gibt es doch schon mit Blazor Server“. Aber nein, Blazor Server und Blazor SSR sind nicht das Gleiche.
Das schon seit .NET Core 3.1 verfügbare Blazor Server rendert zwar auf dem Server, es entsteht aber dennoch eine Singel Page Application (SPA). Über eine WebSockets-Verbindung werden Unterschiede des Rendering zum vorherigen Rendering (im Programmiererlatein „Diff“ oder „Change Set“ genannt) zum Client gesendet und dort per JavaScript ausgetauscht. Die Benutzer:innen bemerken daher nicht, dass es ein Server-Rendering gab. Die Seite ist genauso interaktiv wie beim Einsatz von Blazor WebAssembly oder eines JavaScript-basierten Webfrontendframeworks wie Angular, React oder VueJS.
Mit dem neuen Blazor Server-side Rendering (Blazor SSR) bietet Microsoft im Rahmen von Blazor nun auch eine rein serverseitige, aus der Clientsicht statische HTML-Erzeugung zu einer Multi Page Application (MPA). Das Rendering-Ergebnis wird bei Blazor SSR in einem Rutsch über eine normale HTTP-Verbindung zum Client gesendet und es werden immer ganze Seiten ausgetauscht (oberer Teil in Abb. 1), sodass der Benutzer ein typisches Flackern der Darstellung beim Seitenwechsel wahrnimmt. Neue HTTP-Anfragen beim Server werden nur durch Hyperlinks oder Formulareinsendungen ausgelöst. .NET- oder JavaScript-Code im Browser sind bei Blazor SSR nicht notwendig. Optional kann man aber per JavaScript Streaming ermöglichen, das Benutzer:innen schon während des Renderings Anzeigen bietet.
Blazor SSR ist schon seit .NET 8.0 Preview 3 verfügbar. Bei Blazor SSR können (mit einigen Abstrichen) die gleichen Razor Components, die bisher bei Blazor Server, Blazor WebAssembly, Blazor MAUI und Blazor Desktop eingesetzt wurden, nun für reines Server-side Rendering verwendet werden. Blazor SSR besitzt grundsätzlich die gleiche Multi-Page-Application-Architektur wie ASP.NET Core Model View Controller (MVC), das es seit .NET Core 1.0 gibt, und ASP.NET Core Razor Pages, eingeführt in .NET Core 2.0 im Jahr 2017.
Der Unterschied zu MVC und Razor Pages ist, dass bei Blazor SSR auf dem Webserver sogenannte Razor Components mit der Blazor-Variante der Razor-Template-Syntax arbeiten, anstelle von Controller plus Razor Views (bei MVC) bzw. Page Model plus Razor Pages (bei Razor Pages). Genau wie bei den älteren Modellen muss auf dem Webserver für Blazor SSR natürlich ASP.NET Core laufen.
Abb. 1: Blazor Server-side Rendering ohne Streaming und mit Streaming sowie Blazor Server und Blazor WebAssembly
Neue Projektvorlage Blazor Web App
Während man in .NET 8.0 Preview 3 und 4 als Ausgangspunkt für Blazor SSR noch ein ASP.NET-Core-MVC- oder Razor-Pages-Projekt anlegen musste, gibt es seit .NET 8.0 Preview 5 sowie Visual Studio 2022 17.7 Preview 2 eine neue Projektvorlage Blazor Web App (Abb. 2). Alternativ kann man so ein Projekt über die Kommandozeile anlegen:
dotnet new blazor --use-server -o Projektname
Abb. 2: Einstellungen der Projektvorlage Blazor Web App
Die neue Projektvorlage legt die in Blazor schon übliche Webanwendung auf Basis des CSS-Frameworks Bootstrap in Version 5 an. Die Webanwendung zeigt links (Abb. 3 und 4) eine seitliche Navigationsleiste, die sich bei kleinem Browserfenster auf ein Toastmenü rechts oben reduziert. Wenn das Häkchen Use interactive Server components nicht aktiviert wird (auf der Kommandozeile das –use-server weglassen), entstehen nur zwei Seiten und Menüeinträge: Index.razor mit einer Willkommensnachricht und Weather.razor mit der Anzeige zufälliger Wettervorhersagedaten (diese Seite hieß in vorherigen Blazor-Versionen FetchData.razor). Im Ordner /Shared gibt es die Rahmenseite MainLayout.razor und NavMenu.razor für die Navigationsleiste. Den Wurzelcode der HTML-Seite findet man in App.razor. Dort findet man ebenso die Einstellungen für den Blazor-Router (<Found>, <NotFound>, optional auch <Navigating>), der nun sowohl für das Routing auf dem Client als auch auf dem Server zuständig ist. _Imports.razor beinhaltet wie bisher Namensraumimporte, die für alle .razor-Dateien gelten sollen.
Abb. 3: Wetterdatenseite Weather.razor während des Ladens
Abb. 4: Wetterdatenseite Weather.razor nach dem Laden
Während die Startseite Index.razor nur statischen Text zeigt, sieht man im Menüpunkt Weather zunächst eine Ladenachricht („Loading…“, Abb. 3) und dann etwa eine Sekunde später die zufällig generierte Wettervorhersage (Abb. 4). Während man das Datenobjekt als Klasse WeatherForecast in einer eigenen Datei /Models/WeatherForecast.cs findet, steckt der Generierungscode für die zufälligen Wettervorhersagen direkt in Weather.razor (Listing 1).
SIE LIEBEN C#?
Entdecken Sie die BASTA! Tracks
Listing 1: Zufällig generierte Wettervorhersage mit absichtlicher Verzögerung von einer Sekunde in Weather.razor
@page "/weather" @attribute [StreamRendering(true)] <PageTitle>Weather</PageTitle> <h1>Weather</h1> <p>This component demonstrates showing data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private WeatherForecast[]? forecasts; protected override async Task OnInitializedAsync() { // Simulate retrieving the data asynchronously. await Task.Delay(1000); var startDate = DateOnly.FromDateTime(DateTime.Now); 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(); } }
Die Razor Components kommen in der Projektvorlage leider (wie immer bei Microsoft) als Single-File-Komponenten mit Inline-Code (als Blöcke @code { … }) vor. Die Trennung von Programmcode und Layout ist dennoch bei Blazor SSR, wie bei allen anderen Blazor-Arten, möglich. Entwicklerinnen und Entwickler haben die Wahl, Code-Behind-Dateien als partielle Klassen und via Vererbung anzulegen.
Die Verwendung der Schnittstelle IRazorComponentApplication zu Seitenbeginn, die bei Blazor SSR in Preview 3 und 4 noch notwendig war, entfällt seit Preview 5.
Die Zeile @implements IRazorComponentApplication<Confirmation> muss aus älteren Komponenten ersatzlos gestrichen werden.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Verhältnis von Blazor SSR zu MVC und Razor Pages
Blazor SSR lässt die Vorgänger MVC und Razor Pages zu Auslaufmodellen werden, denn die Razor Components von Blazor bieten eine einfachere Syntax, insbesondere für die Formulardatenbehandlung, Datenbindung und das Einbetten von Komponenten ineinander. Zudem bietet Razor Components bei Blazor SSR auch Streaming, was es bei MVC und Razor Pages nicht gibt.
Für eine Migration von MVC oder Razor Pages zu Blazor SSR bietet Microsoft eine Integration an: So kann ein MVC-Controller nicht nur eine View, sondern nun auch eine moderne Razor-Komponente rendern, indem er RazorComponentResult<Komponentenname> zurückliefert (Listing 2).
Listing 2
public class MVCRCController : Controller { public IResult Index() { return new RazorComponentResult<Confirmation>(new { Name = "Dr. Holger Schwichtenberg" }); } }
Zudem kann man eine solche moderne Razor-Komponente via Tag Helper <component> auch in eine Razor Page (.cshtml-Datei) einbetten:
<component type="typeof(Confirmation)" render-mode="Static" param-name='"Dr. Holger Schwichtenberg"' />
Das funktioniert bisher schon genauso bei Blazor Server.
Streaming beim Server-side Rendering
Streaming ist eine optionale Funktion von Blazor SSR. Beim Streaming wird initial eine komplette Seite vom Webserver zum Browser übertragen, sobald das erste Rendering der Seite auf dem Webserver stattgefunden hat. Anders als beim normalen SSR-Rendering wird die HTTP-Verbindung aber nicht nach dem Ende des gerenderten Inhalts geschlossen. Falls sich dann nach Ende der Ausführung asynchroner Methoden Teile der gerenderten Seite noch mal ändern, werden diese Änderungen in der offen gehaltenen HTTP-Verbindung noch nachübertragen und die Inhalte per JavaScript in der schon angezeigten Seite ausgetauscht. Indem man ein this.StateHasChanged() in den Programmcode einbaut, werden auch Zwischenstände übertragen, während die asynchrone Methode noch läuft. Man kann also über die offene HTTP-Verbindung auch mehrmals übertragen – aber nur bis zum Ende des Laufs aller asynchronen Methoden auf dem Webserver.
Abbildung 1 zeigt die Architektur von Blazor SSR ohne Streaming und Blazor SSR mit Streaming sowie Blazor Server und Blazor WebAssembly im Vergleich. Wie das Bild zeigt: JavaScript muss der Browser nur für Blazor Server, Blazor WebAssembly und Blazor SSR mit Streaming können, denn nur in diesen Fällen wird eine von Microsoft gelieferte JavaScript-Datei (blazor.server.js, blazor.webassembly.js bzw. blazor.web.js) in den Webbrowser geladen. Bei Blazor SSR wird kein .NET-Code in den Browser geladen und daher ist auch kein WebAssembly im Browser notwendig.
Streaming zeigt die Projektvorlage Blazor Server App, wenn das Häkchen Use interactive Server components (Abb. 2) gesetzt oder auf der Kommandozeile das –use-server verwendet wurde in der Komponente Weather.razor. Die Benutzersicht auf /Weather zeigen Abbildung 3 und 4.
OnInitializedAsync() in Listing 1 (Weather.razor aus der Projektvorlage von Microsoft) implementiert als erste Codezeile eine absichtliche Verzögerung um eine Sekunde, die das Laden von Daten aus einer Datenbank oder einem Webservice simulieren soll. Dass der Benutzer der Webanwendung in der Zwischenzeit „Loading…“ sieht, liegt an der Direktive @attribute [StreamRendering(true)] am Beginn von Weather.razor in Verbindung mit der @if-Abfrage in Listing 3.
Listing 3
@if (forecasts == null) { <p><em>Loading...</em></p> } else { ... }
Wie sich das Streaming aus der Sicht des Webbrowsers darstellt, sieht man in Abbildung 5: Zunächst wird eine komplette HTML-Seite innerhalb des Tags <html>…</html> übertragen. Die Datei mit dem „Loading…“ stellt der Browser dar. Die verzögert eingehenden Render-Ergebnisse aller asynchronen Operationen hängt ASP.NET Core in der gleichen HTML-Antwort noch der Seite an (siehe <template> innerhalb von <blazor-ssr> in Zeilen 42 und 45 in Abbildung 6). Die JavaScript-Datei blazor.web.js, die am Ende der ursprünglichen Seite geladen wurde (Zeile 39 in Abb. 6), baut die in den Tags <template> enthaltenen HTML-Fragmente dann in die ursprünglich dargestellte Seite ein, sodass die Benutzer:innen mit etwas Verzögerung die Wetterdatentabelle statt des Ladehinweises sehen.
Die in Preview 4 eingeführten Sektionen für Blazor (<SectionOutlet> und <SectionContent>) funktionieren seit Preview 7 auch beim Streaming-Rendering sowie in Verbindung mit kaskadierenden Werten und Error Boundaries.
Abb. 5: Counter.razor ist zunächst interaktiv über eine WebSocket-Verbindung zum Blazor-Server-Prozess; die Blazor-WebAssembly-Laufzeitumgebung wird im Hintergrund nachgeladen
Abb. 6: Serverstreaming bei Blazor SSR: Nach dem </html> kommen noch später eingehende Fragmente in der gleichen HTML-Antwort, die blazor.web.js in die Seite einsetzt
Listing 4 zeigt eine Erweiterung des Programmcodeblocks aus Listing 1. Dieses Mal wird zusätzlich zweimal die Überschrift geändert. Der Benutzer sieht (jeweils nach einer Sekunde künstlicher Wartezeit) drei Aktualisierungen der Seite:
- Die Überschrift wird das erste Mal geändert und zeigt eine Ladenachricht.
- Die Daten werden geladen und als Tabelle dargestellt.
- Die Überschrift wird das zweite Mal geändert, ebenso die Anzahl der geladenen Datensätze.
Wichtig ist nach jedem Schritt, die Methode this.StateHasChanged() auszuführen, sonst sieht der Benutzer keine Aktualisierung der Seite.
Listing 4: Erweiterter Code von Weather.razor mit mehreren Bildschirmaktualisierungen
@page "/showdata" @attribute [StreamRendering(true)] <PageTitle>@title</PageTitle> <h1>@title</h1> <p>This component demonstrates showing data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private WeatherForecast[]? forecasts; string title = "Weather forecast"; protected override async Task OnInitializedAsync() { // 1. UI-Update: Nur Titel await Task.Delay(1000); title = "Loading Weather forecasts..."; this.StateHasChanged(); // 2. UI-Update: Tabelle await Task.Delay(1000); var startDate = DateOnly.FromDateTime(DateTime.Now); 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(); this.StateHasChanged(); // 3. UI-Update: Nochmal Titel await Task.Delay(1000); title = forecasts.Length + " Weather forecasts loaded"; } }
Komponenten als eigenständige Seiten oder eingebettet
Sowohl Index.razor als auch Weather.razor besitzen eine @page-Direktive:
@page "/"
bzw.
@page "/weather"
Beide Komponenten sind also über die relativen URLs “/” bzw. “/Weather” direkt im Browser aufrufbar. Wie in Blazor üblich, kann man auch bei Blazor Server-side Rendering jede Komponente auch per Tag in eine andere Komponente einbetten. Wenn man also die Wetterkomponente bereits auf der Startseite sehen will, erweitert man die Index.razor-Datei einfach um das Tag <Weather>:
@page "/" <PageTitle>Home</PageTitle> <h1>Home</h1> Welcome to your new app. <Weather></Weather>
Auch die Kurzschreibweise <Weather/> anstelle von <Weather></Weather> ist möglich.
Auch serverseitig gerenderte Blazor-Komponenten können Parameter besitzen. Wie in Blazor üblich, deklariert man sie in der Komponente mit einer C#-Property mit Annotation [Parameter]:
[Parameter] public int Days { get; set; } = 5;
Die Property nutzt man dann im Komponentencode, z. B.:
forecasts = Enumerable.Range(1, Days).Select(…)
Nun können Entwicklerinnen und Entwickler den Parameter auf Nutzerseite auf einen anderen Wert als den Standardwert setzen:
<Weather Days=”10″></Weather>
Die Einbettung von Blazor-Komponenten ineinander ist damit deutlich einfacher als die Arbeit mit Partial Views bei ASP.NET Core MVC und ASP.NET Core Razor Pages [2]. Blazor bietet im Gegensatz zu den Vorgängern ein echtes Komponentenmodell.
Möchte man, dass der Parameter auch per URL gefüllt werden kann, verwendet man [SupplyParameterFromQuery] statt [Parameter]:
[SupplyParameterFromQuery] public int Days { get; set; } = 5;
Jetzt kann man die Seite über diesen URL dazu bringen, die Wettervorhersage für 10 Tage zu erzeugen:
https://localhost:7009/weather?days=10
Auch Razor Components, die keine @page-Direktive haben und daher nur als Teil einer anderen Seite verwendbar sind, können seit Preview 6 [SupplyParameterFromQuery] nutzen. Falls sowohl [SupplyParameterFromQuery] als auch [Parameter] vor einem Property stehen und die Hostkomponente einen Wert per Attribut vorgibt, wiegt das schwerer als eine Angabe eines Parameters im URL.
Keine Interaktivität im Client
Bei Blazor SSR ist zu beachten, dass alle Clientereignisbehandlungen (z. B. @onclick, @onmousemove, @onkeydown) nicht funktionieren, denn im Browser ist kein Code, der sie behandeln könnte. Wenn man so ein Ereignis behandelt, sieht man gar nichts, also weder im Browser noch in Visual Studio eine Fehlermeldung. Auch die Ausführung von eigenem JavaScript-Code, z. B.:
await js.InvokeVoidAsync("alert", "Hello World");
funktioniert nicht. Immerhin kassiert man hier einen Laufzeitfehler: „System.InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered.“ Visual Studio warnt bislang leider nicht, wenn man in einer Razor-Komponente bei Blazor SSR solche Ereignisse behandelt oder JavaScript-Code ausführt.
Mögliche Clientinteraktionen in Blazor SSR sind, wie in Multi Page Applications üblich:
- Hyperlinks (<a href=”…”>)
- Formulare, die mit HTTP-POST gesendet werden
Zudem ist eine serverseitige Navigation mit dem aus Blazor schon bekannten NavigationManager möglich:
@inject NavigationManager NavigationManager ... NavigationManager.NavigateTo("/Weather");
Wenn man das zum Beispiel im Rahmen des Lebenszyklusereignisses OnInitialized() oder nach dem Einsenden eines Formular ausführt, sendet der Webserver eine HTTP-Antwort mit Statuscode 302 und der neuen Adresse im Headerfeld “Location”.
Einfachere Formulare bei Blazor SSR
Die in .NET 8.0 Preview 4 und 5 umständliche Handhabung von Formulareingaben beim Server-side Rendering in Blazor via CascadingModelBinder und FormData.Entries.TryGetValue() ist seit .NET 8.0 Preview 6 einfacher, indem Entwicklerinnen und Entwickler nun einfach ein Property mit [SupplyParameterFromForm] annotieren können (Listing 5). Alle in der HTTP-Anfrage gelieferten Name-Wert-Paare werden in die dem Namen entsprechenden Properties abgelegt. Die annotierten Properties können einfache Datentypen, komplexe Typen (Klassen, Strukturen, Records) oder Mengentypen sein. Vorhandene Validierungsannotationen werden berücksichtigt und Validierungsfehlerausgaben sind mit den eingebauten Blazor-Komponenten <ValidationMessage> und <ValidationSummary> möglich. Seit Preview 7 werden auch die Annotationen [DataMember] und [IgnoreDataMember] berücksichtigt, mit denen man die abzubildenden Namen ändern kann.
Seit Preview 7 ist Voraussetzung für die Formularbindung, dass das Tag <EditForm> via Tagattribut FormName einen Namen deklariert:
<EditForm method="POST" FormName="Registration" Model="reg" OnValidSubmit="HandleSubmit">
Ohne diese Angabe kommt es zum Laufzeitfehler: „The POST request does not specify which form is being submitted. To fix this, ensure <form> elements have a @formname attribute with any unique value, or pass a FormName parameter if using <EditForm>.“ Es ist richtig, was diese Fehlermeldung ebenfalls suggeriert: Seit Preview 7 ist die serverseitige Formularbehandlung auch ohne die eingebaute Blazor-Komponente <EditForm> mit dem Standard-HTML-Tag <form> und dem Zusatz @formname möglich.
Listing 5: Vereinfachte Formulardatenbindung einschließlich Validierung bei Blazor Server-side Rendering
@page "/Registration/" @using NET8_BlazorSSR; @layout MainLayout @inject NavigationManager nav <h3>Bestellung des Fachbuchabos</h3> <hr /> @if (!reg.Success) { <EditForm method="POST" FormName="Registration" Model="reg" OnValidSubmit="HandleSubmit"> <DataAnnotationsValidator /> <p>Ihr Name: <InputText @bind-Value="reg.Name" /> <ValidationMessage For="@(()=>reg.Name)" /></p> <p>Ihr E-Mail-Adresse: <InputText @bind-Value="reg.EMail" /> <ValidationMessage For="@(()=>reg.EMail)" /></p> <button type="submit">Bestellen</button> </EditForm> } @if (reg.Success) { <div> <p>Liebe(r) @reg.Name,</p> <p>vielen Dank für Ihre Registrierung zum Fachbuchabo!</p> <p><a href="/confirmation/@reg.Name">Bestätigung ausdrucken</a></p> </div> } @code { [SupplyParameterFromForm] BookSubscriptionRegistration? reg { get; set; } protected override void OnInitialized() { reg ??= new(); } void HandleSubmit() { reg.Save(); reg.Success = true; } }
SPA-Inseln mit Blazor Server
Sofern man Use interactive Server components bei der Projektvorlage Blazor Web App wählt, erhält man im Blazor-Projekt eine dritte Seite namens Counter.razor (Quellcode in Listing 6), die man als Counter im Navigationsmenü sieht (Abb. 3 und 4). Der Zähler auf der Seite funktioniert interaktiv auch ohne Verzögerung und Flackern der Seite, das heißt, dass die Seite nicht komplett neu geladen wird. Möglich wird das durch folgende Direktive zu Beginn der Datei Counter.razor:
@attribute [RenderModeServer]
Das führt zu einer Single-Page-App-Insel innerhalb des Server-side Rendering: Counter.razor wird mit Blazor Server gerendert – wie üblich unter Verwendung einer WebSockets-Verbindung, die aber erst beim ersten Aufruf von Counter.razor aufgebaut wird. Die Projektvorlage zeigt nur eine einzelne solcher Single-Page-App-Inseln und es stellt sich natürlich die Frage, ob es bei mehreren solchen SPA-Inseln auch mehrere WebSockets-Verbindungen gibt. Ein Test mit einer Erweiterung der Projektvorlage zeigt: Nein. Es wird nur eine WebSockets-Verbindung aufgebaut, die dann für alle SPA-Inseln innerhalb der gleichen Blazor-Anwendung zum Einsatz kommt.
Was die Projektvorlage auch nicht zeigt: Eine SPA-Insel kann nicht nur eine Seite mit eigenem URL sein, sondern auch Teil einer anderen Seite. Wenn man zum Beispiel die Counter.razor-Komponente in die Index.razor-Seite einbettet via Tag <Counter></Counter> (oder kurz <Counter/>) funktioniert die Interaktivität der Insel via Blazor Server und WebSockets-Verbindung ebenso.
Neben der Deklaration einer SPA-Insel per Direktive innerhalb der Razor Component selbst, gibt es auch die Möglichkeit, dass der Nutzer einer Komponente den RenderMode vorgibt:
<Counter @rendermode="@RenderMode.Server" />
Sie sollten sich nicht wundern, wenn Visual Studio nicht bei der Eingabe mithilft: Diese Syntax ist korrekt, aber es gibt in Visual Studio 2022 (bis einschließlich der zum Redaktionsschluss für diesen Beitrag aktuellen Version 17.8 Preview 1) keinerlei IntelliSense-Eingabeunterstützung dafür.
Es ist übrigens nicht möglich, gleichzeitig den Render-Modus in der Komponente mit @attribute [RenderModeServer] und nochmals beim Aufrufer mit @rendermode=”@RenderMode.Server” festzulegen. Ein solchen Versuch quittiert Blazor mit dem Laufzeitfehler: „The component type ‘Counter’ has a fixed rendermode of ‘Microsoft.AspNetCore.Components.Web.ServerRenderMode’, so it is not valid to specify any rendermode when using this component.“
Listing 6: Counter.razor mit dem RenderModeServer @page “/counter”
@attribute [RenderModeServer] <PageTitle>Counter</PageTitle> <h1>Counter</h1> <p role="status">Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } }
Startdateien bei Blazor SSR
Die Voraussetzungen für Blazor SSR, Streaming und SPA-Inseln legt die Projektvorlage Blazor Web App automatisch an. Dazu gehört:
- Für Blazor SSR braucht man in der cs einen Aufruf von AddRazorComponents() und MapRazorComponents<App>(): builder.Services.AddRazorComponents() und app.MapRazorComponents<App>()
- Für SPA-Inseln mit Blazor Server braucht man in der cs zusätzlich einen Aufruf von AddServerComponents() und .AddServerRenderMode(): builder.Services.AddRazorComponents().AddServerComponents() und app.MapRazorComponents<App>().AddServerRenderMode()
- Für das Streaming braucht man in der razor dieses Tag zum Laden der JavaScript-Datei:
<script src="_framework/blazor.web.js" suppress-error="BL9992"></script>
SPA-Inseln mit Blazor WebAssembly
SPA-Inseln mit Blazor Server hat Microsoft in .NET 8.0 Preview 5 eingeführt, wobei das @rendermode in der Hostseite erst mit Preview 6 kam. Während in .NET 8.0 Preview 5 die SPA-Inseln ausschließlich mit Blazor Server möglich waren, bietet Microsoft seit .NET 8.0 Preview 6 an dieser Stelle alternativ nun auch Blazor WebAssembly an.
Bisher gibt es für diese Konstellation keine Projektvorlage; man kann aber die Projektvorlage Blazor Web App dazu selbst manuell umbauen. Wesentliche Voraussetzung ist, dass sich alle mit Blazor WebAssembly zu rendernden Razor-Komponenten in einer separaten, referenzierten Razor Class Library (also einer eigenen DLL) befinden, die auf dem SDK Microsoft.NET.Sdk.BlazorWebAssembly basiert:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
Diese Abtrennung ist notwendig, um den in den Browser zu ladenden Code von dem Servercode abzugrenzen.
Auch das Hauptprojekt (nun nur noch mit dem Code, der nur auf dem Server laufen soll) muss man wie folgt umbauen:
- Man muss das Paket AspNetCore.Components.WebAssembly.Server referenzieren (hier mit der Versionsnummer für die .NET 8.0 Preview 7):
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.0-preview.7.23375.9" />
- Dann muss man in der Startdatei cs die Methode AddWebAssemblyComponents()nach AddRazorComponents() aufrufen:
builder.Services.AddRazorComponents().AddWebAssemblyComponents();
- Bei MapRazorComponents<App>() muss man die Methode AddWebAssemblyRenderMode() nutzen:
app.MapRazorComponents<App>().AddWebAssemblyRenderMode();
Nun ist es möglich, eine Komponente aus dieser DLL mit dem Render-Modus WebAssembly zu nutzen:
<Counter @rendermode="@RenderMode.WebAssembly" />
Alternativ kann diesen Render-Modus auch die Komponente selbst deklarieren:
@attribute [RenderModeWebAssembly]
Im Standard werden solche WebAssembly-Komponenten serverseitig vorgerendert. Das können Entwicklerinnen und Entwickler bei Bedarf ausschalten:
@attribute [RenderModeWebAssembly(prerender: false)]
bzw.
<Counter @rendermode="@(new WebAssemblyRenderMode(prerender: false))
Ein funktionierendes Beispiel für die Integration von Blazor WebAssembly in eine Anwendung mit Blazor Sever-side Rendering findet man unter [3].
Modus für das Blazor-Rendering
In .NET 8.0 Preview 7 kam dann zum RenderMode.Server und RenderMode.WebAssembly auch noch der RenderMode.Auto hinzu, z. B.:
<Counter IncrementAmount="1" @rendermode="@RenderMode.Auto" />
Alternativ kann eine Blazor-Komponente den Automodus statisch in ihrer Definition selbst festlegen:
@attribute [RenderModeAuto]
Dazu aktiviert man in der Startdatei beide Render-Modi (also Blazor Server und Blazor WebAssembly) nacheinander:
builder.Services .AddRazorComponents() .AddWebAssemblyComponents() .AddServerComponents();
und
app.MapRazorComponents<App>() .AddWebAssemblyRenderMode() .AddServerRenderMode();
Falls die Blazor-WebAssembly-Laufzeitumgebung innerhalb von 100 Millisekunden geladen werden kann, verwendet der Automodus immer Blazor WebAssembly. Eine so kurze Zeit kann allerdings nur in sehr schnellen Netzwerken erreicht werden oder wenn die Laufzeitumgebung schon im Cache des Browsers liegt. Falls ASP.NET auf dem Webserver diese Zeitspanne als nicht erreichbar ansieht, wird die Komponente zunächst per Blazor Server gerendert und eine WebSockets-Verbindung zwischen Browser und Webserver für die Interaktivität aufgebaut.
Die Blazor-WebAssembly-Laufzeitumgebung lädt dann im Hintergrund nach (Das Laden der .wasm-Dateien nach dem Aufbau der WebSockets-Verbindung ist in Abbildung 6 zu sehen.). Ein Wechsel zu Blazor WebAssembly erfolgt dann aber nicht im laufenden Betrieb der Komponente, sondern erst, wenn die einzelne Komponente neu initialisiert wird, zum Beispiel durch einen zwischenzeitlichen Wechsel zu einer anderen Seite. Es muss aber nicht die ganze Anwendung neu geladen werden.
Der neue Automodus bietet den Vorteil einer schnellen ersten Sicht auf die Inhalte für die Benutzer:innen, die aber dennoch via WebSockets interaktiv ist. Bei der weiteren Nutzung kann dann das im Hintergrund geladene Blazor WebAssembly zum Einsatz kommen, wodurch die Nutzung der Komponente auch bei instabiler Netzwerkverbindung möglich wird. Hier wird man noch Konzepte brauchen, wie man Benutzer:innen so lenkt, dass die Komponente ein weiteres Mal initialisiert wird, denn sonst vollzieht sich der Wechsel auf Blazor WebAssembly ja nicht.
Bisher gibt es für den Automodus keine Projektvorlage. Interessierte Entwickler:innen finden ein Beispiel mit einer einzigen Komponente im Automodus (Counter.razor) auf GitHub [4]. Man sieht darin, dass die Komponente, die im Automodus laufen soll, in einer getrennten Assembly liegen muss. Die Tatsache, dass es für die Komponente Counter.razor im Projekt Client auch noch eine korrespondierende Datei Counter.razor im Projekt Server gibt, ist ein temporärer Workaround, weil die Routen aus Razor Class Libraries derzeit nicht im Serverprojekt erfasst werden. Microsoft arbeitet daran, das via .AddComponentAssemblies() im Startcode zu erledigen [5].
Was das Beispielprojekt nicht zeigt: Falls die Komponente Daten aus Datenbanken abruft, muss sie so gestaltet sein, dass das nicht nur bei Blazor Server, sondern auch Blazor WebAssembly funktioniert. Das bedeutet: Es darf nicht generell ein direkter Datenbankzugriff erfolgen, sondern man muss entweder immer die Daten per Web-Service/Web-API laden oder aber man muss eine Abstraktion einbauen, die die Daten bei Blazor Server per Direktzugriff lädt und bei Blazor WebAssembly per Web-Service/Web-API.
Mischung der Render Modes
Das Mischen von SPA-Inseln mit Blazor WebAssembly, Blazor Server und dem Automodus in einer Anwendung und sogar in seiner Seite ist möglich (Listing 7), sofern beide Render-Modi in der Startdatei aktiviert wurden:
builder.Services .AddRazorComponents() .AddWebAssemblyComponents() .AddServerComponents();
und
app.MapRazorComponents<App>() .AddWebAssemblyRenderMode() .AddServerRenderMode();
Listing 7 zeigt eine Startseite Index.razor, die eine Komponente Counter.razor einmal per Blazor Server und einmal per Blazor WebAssembly einbindet. Abbildung 7 zeigt die Auswirkung auf den Webbrowser: Der Browser baut für Blazor Server eine WebSockets-Verbindung auf und lädt danach die WebAssembly-Dateien für Blazor WebAssembly. Die Rahmenkomponente Home.razor entsteht rein per Server-side Rendering.
Die Ausgabe, mit welchem Render-Modus das Rendering erfolgt ist, geschieht auf Basis des Typs, den Blazor für IJSRuntime injiziert. Das ist Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSRuntime bei Blazor Server und Microsoft.AspNetCore.Components.WebAssembly.Services.DefaultWebAssemblyJSRuntime bei Blazor WebAssembly. Leider gibt es für den aktuellen Render-Modus noch keine direkte Abfrage auf Komponentenebene.
Listing 7: Home.razor nutzt eine explizite Mischung von Blazor Server und Blazor WebAssembly in einer Seite
@page "/" <PageTitle>Mischung von Blazor Server und Blazor WebAssembly</PageTitle> <hr /> <h3>1. Counter</h3> <hr /> <div> <Net8BlazorAuto.Client.Pages.Counter CurrentCount="42" @rendermode="@RenderMode.Server" /> </div> <hr /> <h3>2. Counter</h3> <hr /> <div> <Net8BlazorAuto.Client.Pages.Counter CurrentCount="42" @rendermode="@RenderMode.WebAssembly" /> </div> <hr />
Abb. 7: Der Browser baut für Blazor Server eine WebSockets-Verbindung auf und lädt für Blazor WebAssembly die WebAssembly-Dateien
Abbildung 7 zeigt auf der linken Seite auch, dass die Counter.razor-Komponente jeweils nun auch eine Unterkomponente mit gelblichem Kasten bekommen hat. Diese Unterkomponenten (Unterkomponente.razor) verwenden automatisch den gleichen Render-Modus wie die Host-Komponente.
Nun könnte man auf die Idee kommen, diesen Unterkomponenten wieder einen expliziten Render-Modus zu verpassen. Das funktioniert aber nicht, denn wenn man versucht, in einer per Blazor Server gerenderten Komponente eine Blazor-WebAssembly-Komponente zu laden, erhält man die Laufzeitfehlermeldung:
"Cannot create a component of type 'Net8Blazor.Client.Pages.Unterkomponente' because its render mode 'Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode' is not supported by interactive server-side rendering."
Umgekehrt, wenn man in einer per Blazor WebAssembly gerenderten Komponente eine Komponente explizit per Blazor Server rendert, heißt der Fehler: „Cannot create a component of type ‘Net8Blazor.Client.Pages.Unterkomponente’ because its render mode ‘Microsoft.AspNetCore.Components.Web.ServerRenderMode’ is not supported by WebAssembly rendering.“ Möglich ist aber, Unterkomponente.razor auf den Automodus zu setzen und damit als Unterkomponente in beiden Render-Modi zu verwenden.
Hinsichtlich der Mischung von Render-Modi bedeutet das also: Innerhalb einer per Blazor SSR gerenderten Komponente kann man über Blazor Server und Blazor WebAssembly gerenderte Komponenten beliebig mischen. Wenn allerdings einer dieser beiden Render-Modi für eine Komponente vorgegeben ist, kann man in untergeordneten Komponenten den Render-Modus nicht mehr wechseln.
Andere Blazor-Projektvorlagen
Wenn man sich die Projektvorlagen mit dem Wort „Blazor“ in Visual Studio oder per Kommandozeile (Abb. 8) ansieht, findet man dort neben der neuen Blazor Web App auch die altbekannten Vorlagen für Blazor WebAssembly, Blazor Server und Blazor MAUI. Bei der Nutzung der Projektvorlagen für Blazor Server stellt man aber fest, dass im Visual-Studio-Dialog .NET 8.0 für Blazor Server nicht angeboten wird. Wenn man versucht, .NET 8.0 an der Kommandozeile zu erzwingen
dotnet new blazorserver --framework net8.0
sieht man die Fehlermeldung in Abbildung 9. Tatsächlich will Microsoft die Projektvorlage Blazor Server nicht mehr anbieten, denn Blazor Server ist nun ein Teil der allgemeinen Vorlage Blazor Web App. An die Stelle der bisherigen Rahmencodes in _Host.cshtml und der Fehlerbehandlung in Error.cshtml (beides sind Razor Pages!) tritt nun eine komplette Lösung mit Razor Components (also Blazor-Komponenten!).
Eine Projektvorlage für Blazor WebAssembly gibt es noch im .NET 8.0 SDK und Visual Studio 2022 mit Auswahl .NET 8.0, aber die Option ASP.NET Core hosted ist verschwunden.
Diese Änderungen begründet Microsoft im Blogeintrag zu .NET 8.0 Preview 6 [6]: „Im Rahmen der Vereinheitlichung der verschiedenen Blazor-Hostingmodelle in einem einzigen Modell in .NET 8 konsolidieren wir auch die Anzahl der Blazor-Projektvorlagen. In dieser Vorschauversion haben wir die Blazor-Server-Vorlage und die Option ‚ASP.NET Core hosted‘ aus der Blazor-WebAssembly-Vorlage entfernt. Beide Szenarien werden durch Optionen dargestellt, wenn die neue Blazor-Web-App-Vorlage verwendet wird.“.
Möglicherweise wird Microsoft die Produktbezeichnungen Blazor Server und Blazor WebAssembly in Zukunft auch gar nicht mehr verwenden, sondern nur noch allgemein von „Blazor“ und den Render-Modi „server-side“ (gleich Blazor SSR), „interactive server-side“ (gleich Blazor Server) und „WebAssembly“ (gleich Blazor WebAssembly) sprechen.
Abb. 8: Blazor-Projektvorlagen im .NET 8.0 SDK
Abb. 9: Fehlermeldung beim Versuch, ein Projekt mit der Projektvorlage Blazor Server für .NET 8.0 anzulegen
Globale kaskadierende Blazor-Werte
Kaskadierende Werte, die man bisher nur in Razor-Komponenten definieren konnte, können Entwicklerinnen und Entwickler in .NET 8.0 im Startcode einer Blazor-Anwendung innerhalb der Program.cs registrieren, um diese Werte als Zustand für alle Komponenten in einer Komponente verfügbar zu machen. Dazu registriert man ein Objekt wahlweise ohne expliziten Namen:
builder.Services.AddCascadingValue(sp => new Autor { Name = "Dr. Holger Schwichtenberg1", Url = "www.dotnet-doktor.de" });
oder mit Namen:
builder.Services.AddCascadingValue("autor2", sp => new Autor { Name = "Dr. Holger Schwichtenberg", Url = "www.dotnet-doktor.de" });
oder via CascadingValueSource:
builder.Services.AddCascadingValue(sp => { var a = new Autor { Name = "Holger Schwichtenberg", Url = "www.dotnet-doktor.de" }; var source = new CascadingValueSource<Autor>("autor3",a, isFixed: false); return source; });
Dann kann jede Razor-Komponente innerhalb der Blazor-Anwendung dieses Objekt konsumieren und auch verändern (Listing 8).
Listing 8
@code { [CascadingParameter] Autor autor1 { get; set; } [CascadingParameter(Name = "autor2")] Autor autor2 { get; set; } [CascadingParameter(Name = "autor3")] Autor autor3 { get; set; } [ParameterAttribute] public int id { get; set; } protected override void OnInitialized() { autor3.Name = "Dr. " + autor3.Name; } }
Antiforgery-Token zum Schutz gegen Cross-Site Request Forgery
Für den Schutz gegen Angriffe nach dem Prinzip der Cross-Site Request Forgery (CSRF/XSRF) liefert Microsoft in ASP.NET Core 8.0 ab Preview 7 eine neue Middleware, die Entwicklerinnen und Entwickler im Startcode einer ASP.NET-Core-Anwendung via builder.Services.AddAntiforgery(); integrieren können. Dieser Aufruf aktiviert zunächst nur in der Verarbeitungs-Pipeline das IAntiforgeryValidationFeature. Ein auf ASP.NET Core aufbauendes Webframework (z. B. Blazor, WebAPI, MVC, Razor Pages) muss sodann ein Antiforgery-Token im Programmcode berücksichtigen. Der Blogeintrag [7] zeigt Implementierungsbeispiele für ASP.NET Core Minimal APIs [8] und Blazor (bei Verwendung von <EditForm>) [9], lässt aber offen, wie der Implementierungsstatus für andere ASP.NET-Core-basierte Webframeworks wie MVC und Razor Pages ist.
WebCIL ist nun der Standard bei Blazor WebAssembly
Seit .NET 8.0 Preview 4 gibt es das WebCIL-Format für Blazor WebAssembly; seit Preview 7 ist es nun im Standard aktiv. Die Implementierung von WebCIL zusammen mit WebAssembly-Modulen zielt darauf ab, die Blockierung von Blazor WebAssembly durch Firewalls und Antivirensoftware zu verhindern. Microsoft hat das Format inzwischen so angepasst, dass Standard-WebAssembly-Module mit der Dateinamenserweiterung .wasm generiert werden (in Preview 4 war es noch .webcil).
Entwicklerinnen und Entwickler haben die Option, WebCIL zu deaktivieren, indem man den Schalter <WasmEnableWebcil>false</WasmEnableWebcil> in der Projektdatei verwendet. In Blogeintrag [10] gibt Microsoft jedoch keine expliziten Hinweise darauf, in welchen Szenarien das sinnvoll sein könnte. Bisher waren die Blazor-WebAssembly-Dateien standardmäßig mit der Erweiterung .dll versehen.
Identity Server ist raus
Gemäß der Ankündigung auf der Build-Konferenz im Mai 2023 hat Microsoft den Duende Identity Server nun aus seinen Projektvorlagen für React- und Angular-Projekte mit einem ASP.NET-Core-Backend entfernt. Dieser Schritt wurde vom .NET-Entwicklungsteam unternommen, da der Identity Server, der in den Versionen 1 bis 4 als Open-Source-Software und Projekt der gemeinnützigen .NET Foundation verfügbar war, mittlerweile zu einem kommerziellen Produkt geworden ist. Ausgenommen von den Lizenzgebühren sind lediglich Open-Source-Projekte. Microsoft hat sich entschieden, die besagten Projektvorlagen nun auf das in ASP.NET Core integrierte Identity-System zu stützen. Für Entwicklerinnen und Entwickler, die den Identity Server weiterhin nutzen möchten, stellt Microsoft die Dokumentation von Duende zur Verfügung [11].
Weitere WebAPIs für ASP.NET Core Identity
In .NET 8.0 Preview 4 hatte Microsoft für ASP.NET Core Identity, das integrierte Benutzerverwaltungssystem von ASP.NET Core, erstmals WebAPI-Endpunkte eingeführt. Diese Ergänzung erfolgte neben der bereits vorhandenen Weboberfläche, die auf serverseitigem Rendering basiert. Dies geschah, um ASP.NET Core Identity effektiver in Single-Page-Webanwendungen (unter Verwendung von JavaScript oder Blazor) zu integrieren. Zuvor beschränkten sich die verfügbaren WebAPI-Endpunkte lediglich auf die Benutzerregistrierung (/register) und die Benutzeranmeldung (/login).
In Preview-Version 7 wurden weitere Endpunkte hinzugefügt. Diese umfassen die Bestätigung von Benutzerkonten per E-Mail (/confirmEmail und /resendConfirmationEmail), das Zurücksetzen von Passwörtern (/resetPassword), die Aktualisierung von Tokens (/refresh) sowie die Unterstützung für die 2-Faktor-Authentifizierung (/account/2fa) sowie das Lesen und Aktualisieren von Profildaten (/account/info).
Die Aktivierung dieser WebAPI-Endpunkte für ASP.NET Core Identity erfolgt in der Datei Program.cs, indem nach AddIdentityCore<T>() die Funktion AddApiEndpoints() aufgerufen wird, wie in Listing 9 dargestellt.
Listing 9: Startcode einer ASP.NET-Core-Anwendung, die eine Benutzerverwaltung via ASP.NET Core Identity via WebAPI bereitstellt (Quelle: [12])
using System.Security.Claims; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme); builder.Services.AddAuthorizationBuilder(); builder.Services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("AppDb")); builder.Services.AddIdentityCore<MyUser>() .AddEntityFrameworkStores<AppDbContext>() .AddApiEndpoints(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Adds /register, /login and /refresh endpoints app.MapIdentityApi<MyUser>(); app.MapGet("/", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); if (app.Environment.IsDevelopment() { app.UseSwagger(); app.UseSwaggerUI(); } app.Run(); class MyUser : IdentityUser { } class AppDbContext : IdentityDbContext<MyUser> { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } }
Fehlertoleranz bei ASP.NET Core SignalR
Der Benachrichtigungsdienst ASP.NET Core SignalR kann seine Verbindung nun nach kurzen Verbindungsabbrüchen automatisch nahtlos wiederherstellen (Seamless Reconnect). Seit .NET 8.0 Preview 5 funktioniert das aber vorerst nur mit .NET-Clients. Ein .NET-Client muss die Verbindung mit dem Zusatz UseAcks = true aufbauen:
var hubConnection = new HubConnectionBuilder() .WithUrl("https://ServerName/Hub">", options => { options.UseAcks = true; }) .Build();
Es gibt noch keinerlei Konfiguration dieser Fehlertoleranz. Das ist aber in Arbeit [13]. Von der neuen Fehlertoleranz wird auch Blazor Server profitieren, weil dort ASP.NET Core SignalR für die Übertragung der Seitenänderungen zum Einsatz kommt.
Neuerungen in C# 12.0
Für C# 12.0 gibt es vier neue Sprachfeatures in den Previews 5 bis 7. Die wesentliche Neuerung sind dabei die Collection Literals. Mit dieser neuen Syntaxform kann man die bisher sehr heterogenen Initialisierungsformen von Objektmengen im Stil von JavaScript stark vereinheitlichen, also mit den Werten in eckigen Klammern, getrennt durch Kommata (Tabelle 1).
Bisherige Initialisierung | Nun auch möglich |
int[] a = new int[3] { 1, 2, 3 }; | int[] a = [1,2,3]; |
Span<int> b = stackalloc[] { 1, 2, 3 }; | Span<int> b = [1,2,3]; |
ImmutableArray<int> c = ImmutableArray.Create(1, 2, 3); | ImmutableArray<int> c = [1,2,3]; |
List<int> d = new() { 1, 2, 3 }; | List<int> d = [1,2,3]; |
IEnumerable<int> e = new List<int>() { 1, 2, 3 }; | IEnumerable<int> e = [1,2,3]; |
Tabelle 1: Collection Literals in C# 12.0
Weitere Neuerungen in C# 12.0 sind:
- Der Operator nameof funktioniert jetzt auch mit Mitgliedsnamen, einschließlich Initialisierern, bei statischen Mitgliedern und in Attributen (Listing 10).
- Ein Interceptor erlaubt, einen Methodenaufruf abzufangen und umzulenken. Das will Microsoft vor allem einsetzen, um mehr Code kompatibel zum Ahead-of-Timer-Compiler zu machen [14].
- Zur Optimierung gibt es jetzt Inline-Arrays [14].
Listing 7: Erweiterungen für nameof() in C# 12.0
/// <summary> /// nameof-Erweiterung in C# 12.0, siehe auch: /// https://github.com/dotnet/csharplang/issues/4037 /// </summary> [Description($"{nameof(StringLength)} liefert die Eigenschaft {nameof(Name.Length)}")] public struct Person { public string Name; public static string MemberName1() => nameof(Name); // bisher schon möglich public static string MemberName2() => nameof(Name.Length); // bisher: error CS0120: An object reference is required for the non-static field, method, or property 'Name.Length' public void PrintMemberInfo() { Console.WriteLine($"Die Struktur {nameof(Person)} hat ein Mitglied {nameof(Name)}, welches eine Eigenschaft {nameof(Name.Length)} besitzt!"); // Die Struktur Person hat ein Mitglied Name das eine Eigenschaft Length besitzt! } [Description($"{nameof(StringLength)} liefert die Eigenschaft {nameof(Name.Length)}")] public int StringLength(string s) { return s.Length; } }
Verbesserungen beim Native-AOT-Compiler
Die seit .NET 8.0 Preview 3 verfügbare Ahead-of-Time-Kompilierung für ASP.NET Core Minimal WebAPIs kommt nun auch mit komplexen Objekten klar, die mit [AsParameters] annotiert sind [15].
Microsoft stellt seit .NET 8.0 Preview 6 die Möglichkeit zur Verfügung, .NET-Anwendungen für iOS, Mac Catalyst und tvOS mit Hilfe des neuen .NET-Native-AOT-Compilers zu kompilieren. Diese Möglichkeit steht sowohl für Apps, die auf diese Plattformen beschränkt sind („.NET for iOS“), als auch für das .NET Multi-Platform App UI (.NET MAUI) zur Verfügung. Dadurch laufen die Anwendungen nicht mehr auf Mono, und die App-Pakete für „.NET for iOS“ werden spürbar kompakter. Hingegen verzeichnen die App-Pakete für .NET MAUI eine Zunahme in ihrer Größe (Abb. 10). In einem Blogeintrag [16] hat Microsoft bestätigt, dass dieses Problem erkannt wurde und aktiv daran gearbeitet wird, eine Lösung zu finden, die zu einem Größenvorteil von etwa 30 Prozent führt.
Abb. 10: Verkleinerte App-Pakete durch Native AOT (Bildquelle: Microsoft [16])
Verbesserungen für System.Text.Json
In .NET 8.0 Preview 6 und 7 gab es wieder einmal Verbesserungen für den JSON-Serialisierer/-Deserialisierer im NuGet-Paket System.Text.Json.
System.Text.Json vermag nun neuere Zahlentypen wie Half, Int128 und UInt128 sowie die Speichertypen Memory<T> und ReadOnlyMemory<T> zu serialisieren. Bei den Letzteren entstehen, wenn es sich um Memory<Byte> und ReadOnlyMemory<Byte> handelt, Base64-kodierte Zeichenketten. Andere Datentypen werden als JSON-Arrays serialisiert.
- Beispiel 1:
JsonSerializer.Serialize<ReadOnlyMemory<byte>>(new byte[] { 42, 43, 44 }); wird zu "Kiss"
- Beispiel 2:
JsonSerializer.Serialize<Memory<Int128>>(new Int128[] { 42, 43, 44 }); wird zu [42,43,44]
- Beispiel 3:
JsonSerializer.Serialize<Memory<string>>(new string[] { "42", "43", "44" }); wird zu ["42","43","44"]
Zusätzlich dazu ermöglichen die Annotationen [JsonInclude] und [JsonConstructor] es Entwicklerinnen und Entwicklern, die Serialisierung nichtöffentlicher Klassen-Member zu erzwingen. Für jedes nichtöffentliche Mitglied, das mit [JsonInclude] annotiert ist, muss im Konstruktor, der mit [JsonConstructor] annotiert ist, ein Parameter vorhanden sein, um den Wert während der Deserialisierung setzen zu können. Ein aussagekräftiges Beispiel dazu zeigt Listing 10.
Listing 10: Serialisierung und Deserialisierung mit [JsonInclude] und [JsonConstructor]
public class Person { [JsonConstructor] // ohne dies: 'Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. internal Person(int id, string name, string website) { ID = id; Name = name; Website = website; } [JsonInclude] // ohne dies: 'Each parameter in the deserialization constructor on type 'FCL_JSON+Person' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. Fields are only considered when 'JsonSerializerOptions.IncludeFields' is enabled. The match can be case-insensitive.' internal int ID { get; } public string Name { get; set; } [JsonInclude] // ohne dies: 'Each parameter in the deserialization constructor on type 'FCL_JSON+Person' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. Fields are only considered when 'JsonSerializerOptions.IncludeFields' is enabled. The match can be case-insensitive.' private string Website { get; set; } public override string ToString() { return $"{this.ID}: {this.Name} ({this.Website})"; } } ... // Serialisierung var p1 = new Person(42, "Dr. Holger Schwichtenberg", "www.dotnet-doktor.de"); string json4 = JsonSerializer.Serialize(p1); // {"X":42} Console.WriteLine(json4); // Deserialisierung var p2 = JsonSerializer.Deserialize<Person>(json4); Console.WriteLine(p2);
Die bereits vorhandene Klasse JsonNode hat neue Methoden wie DeepClone() und DeepEquals() erhalten. Außerdem wird bei JsonArray nun IEnumerable angeboten, was Aufzählung mit foreach und Language Integrated Query (LINQ) ermöglicht:
JsonArray jsonArray = new JsonArray(40, 42, 43, 42); IEnumerable<int> values = jsonArray.GetValues<int>().Where(i => i == 42); foreach (var v in values) { Console.WriteLine(v); }
Zu den Neuerungen in System.Text.Json in Version 8.0 gehört auch, dass die auf zu serialisierende Klassen anwendbare Annotation [JsonSourceGenerationOptions] nun alle Optionen bietet, die auch die Klasse JsonSerializerOptions beim imperativen Programmieren mit der Klasse System.Text.Json. JsonSerializer erlaubt. Für System.Text.Json in Verbindung Native AOT gibt es eine neue Annotation [JsonConverter]:
[JsonConverter(typeof(JsonStringEnumConverter<MyEnum>))] public enum MyEnum { Value1, Value2, Value3 }
Source Generator für COM
In .NET 7.0 Preview 6 präsentierte Microsoft den Source Generator für native API-Aufrufe über die [LibraryImport]-Annotation [17] auf allen Betriebssystemplattformen. In .NET 8.0 Preview 6 wurde nun eine vergleichbare Option für die Nutzung des Component Object Models (COM), das ausschließlich unter Windows verfügbar ist, eingeführt. Um die COM-Schnittstelle zu nutzen, muss der entsprechende Wrapper die Annotation [GeneratedComInterface] tragen. Klassen, die diese Schnittstellen implementieren, werden mit [GeneratedComClass] annotiert. Zusätzlich zu der bereits in .NET 7.0 eingeführten [LibraryImport]-Annotation können Entwickler nun auch COM-Schnittstellen als Parameter- und Rückgabetypen verwenden. Diese Annotationen ermöglichen es dem C#-Compiler, den normalerweise zur Laufzeit generierten COM-Zugriffscode bereits zur Entwicklungszeit zu erzeugen. Der generierte Code findet sich im Projekt unter /Dependencies/Analyzers/Microsoft.Interop.ComInterfaceGenerator.
Für bestehende Schnittstellen, die [ComImport]-Annotationen aufweisen, schlägt Visual Studio die Konvertierung in [GeneratedComInterface] vor. Analog dazu wird für Klassen, die diese Schnittstellen implementieren, von Visual Studio vorgeschlagen, sie mit [GeneratedComClass] zu annotieren.
Wie schon in der Vergangenheit vorgekommen, hat Microsoft auch in diesem Ankündigungsblogeintrag [18] Fehler eingebaut. Bei allen im Zusammenhang mit COM behandelten Beispielen fehlt das Schlüsselwort partial. Microsoft schreibt zum Beispiel:
[GeneratedComInterface] [Guid("5401c312-ab23-4dd3-aa40-3cb4b3a4683e")] interface IComInterface { void DoWork(); void RegisterCallbacks(ICallbacks callbacks); }
Dieser Code führt aber in Visual Studio 2022 (sowohl in der mit Preview 6 erschienenen Version 17.7 Preview 3.0 als auch in der zum Redaktionsschluss aktuellen Version 17.8 Preview 1) zu diesem Compilerfehler: „The interface ‘IComInterface’ or one of its containing types is missing the ‘partial’ keyword. Code will not be generated for ‘IComInterface’.“ Korrekt ist:
[GeneratedComInterface] [Guid("5401c312-ab23-4dd3-aa40-3cb4b3a4683e")] partial interface IComInterface { void DoWork(); void RegisterCallbacks(ICallbacks callbacks); }
Microsoft hat zumindest einige der Begrenzungen des Source Generators für COM dokumentiert [19]. Das schließt ein, dass der Generator nicht für Schnittstellen funktioniert, die auf IDispatch und IInspectable basieren. Des Weiteren werden weder COM-Properties noch COM-Events unterstützt. Es ist auch wichtig zu beachten, dass Entwickler:innen nicht in der Lage sind, eine COM-Klasse mit dem Schlüsselwort new zu aktivieren; das ist lediglich durch den Aufruf von CoCreateInstance() möglich. Diese Beschränkungen sollen auch in der finalen Version von .NET 8.0 bestehen bleiben, die am 14. November 2023 veröffentlicht werden soll. Eventuelle Verbesserungen sind möglicherweise erst in einer späteren Hauptversion zu erwarten.
Neues Steuerelement OpenFolderDialog für WPF
Viele Jahre lang gab es keine neuen Steuerelemente für die Windows Presentation Foundation (WPF). In .NET 8.0 Preview 7 liefert Microsoft nun einen neuen Dialog für das Auswählen von Ordnern im Dateisystem (Listing 8). Es öffnet sich der Standarddialog des Windows-Betriebssystems. Realisiert wurde die Klasse Microsoft.Win32.OpenFolderDialog aber nicht von Microsoft selbst, sondern dem Communitymitglied Jan Kučera [20].
Listing 8: Einsatzbeispiel für das neue WPF-Steuerelement OpenFolderDialog
OpenFolderDialog openFolderDialog = new OpenFolderDialog() { Title = "Select folder to open ...", InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), }; string folderName = ""; if (openFolderDialog.ShowDialog() == true) { folderName = openFolderDialog.FolderName; }
Tastaturshortcuts für .NET-MAUI-Menüs
Wie angekündigt konzentriert sich das Entwicklungsteam von .NET MAUI in .NET 8.0 auf Fehlerbehebungen, siehe die Blogeinträge zu Preview 5 [21], Preview 6 [22] und Preview 7 [23].
Ein wesentliches neues Feature in diesen drei Previews sind Tastaturshortcuts, die Entwicklerinnen und Entwickler nun per SetAccelerator() einem Menüeintrag zuweisen können:
<MenuFlyoutItem x:Name="AddProductMenu" Text="Add Product" Command="{Binding AddProductCommand}" /> ... MenuItem.SetAccelerator(AddProductMenu, Accelerator.FromString("Ctrl+A"));
Im Hauptblogeintrag zu .NET 8.0 Preview 7 [24] ist noch eine Verbesserung für internationalisierte Mobile-Anwendungen auf Apple-Betriebssystemen (iOS, tvOS und MacCatalyst) erwähnt: Hier kann die Größe der länderspezifischen Daten mit einer neuen Einstellung um 34 Prozent reduziert werden:
<HybridGlobalization>true</HybridGlobalization>
Einige APIs ändern aber durch HybridGlobalization ihr Verhalten [25]. HybridGlobalization ist auch für .NET-Anwendungen möglich, die auf WebAssembly laufen [26]. Die Einstellung lädt dann die Datei icudt_hybrid.dat, die 46 Prozent kleiner sein soll als die bisherige icudt.dat.
Außerdem gibt es nun eine .NET-MAUI-Erweiterung für Visual Studio Code [27].
Funkstille bei Entity Framework Core
Beim Entwicklungsteam von Entity Framework Core ist es derzeit recht still. Für die Previews 5, 6 und 7 gab es zwar jeweils eine neue Version auf NuGet [28], aber nicht den sonst zu allen Vorschauversionen üblichen eigenständigen Blogeintrag des Entity-Framework-Core-Entwicklungsteams. Auch das GitHub Issue [29], in dem Entity-Framework-Core-Engineering-Manager Arthur Vickers [30] bisher im Abstand von zwei Wochen über den Fortschritt berichtete, lieferte zuletzt nur am 22. Juni und 6. Juli einen Eintrag mit einigen kleineren Verbesserungen.
Laut einer Tabelle in oben genanntem Issue hat das Entwicklungsteam die meisten der kleineren Neuerungen in Entity Framework Core in den ersten Preview-Versionen bereits erledigt und arbeitet nun an den größeren Projekten. Dazu gehören:
- Tree Shaking/Trimming sowie Ahead-of-Time-Kompilation für ADO.NET und Entity Framework Core
- der neue, schnellere Microsoft-SQL-Server-Datenbanktreiber für ADO.NET und Entity Framework Core mit dem Codenamen „Woodstar“
- das Mapping von Value Objects für Domain-Driven Design
- verbesserte Werkzeuge für Database-First-Entwicklung (alias Reverse Engineering) in Visual Studio
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Fazit
.NET 8.0 bietet in den Previews 6, 7 und 8 spannende Neuerungen. Es wird nun deutlich, dass Microsoft Blazor United tatsächlich umsetzt. Bis zum Erscheinungstermin am 14. November 2023 im Rahmen der .NET Conf 2023 sind aber noch einige Abrundungen notwendig. Das Entity-Framework-Core-Entwicklungsteam hat noch einiges zu liefern.
Vermischte weitere Neuerungen
Neben dem im Haupttext besprochenen Neuerungen sollen weitere Verbesserungen in den .NET 8.0 Previews 5, 6 und 7 kurz erwähnt werden:
- Im .NET 8.0 SDK führt beim Publishing einer .NET-8.0-Anwendung mit dotnet publish die Angabe eines Runtime Identifiers (mit –runtime oder kurz -r) nicht mehr automatisch dazu, dass eine Self-contained-App (SCA) entsteht. Bisher musste man, wenn man das nicht wollte, –no-self-contained Das gehört zu den inzwischen zahlreichen Breaking Changes in .NET 8.0, die Microsoft unter [31] dokumentiert.
- In .NET 8.0 Preview 5 gab es eine Verbesserung des Metrics API: Per Dependency Injection kann man nun ein Objekt mit der Schnittstelle IMeterFactory beziehen, wenn man zuvor AddMetrics() aufgerufen hat.
- Die Klasse IO.Compression.ZipFile, die es seit dem klassischen .NET Framework 4.5 und im modernen .NET seit Version .NET Core 1.0 gibt, erhält zwei neue statische Methoden CreateFromDirectory() und ExtractToDirectory(). Diese ermöglichen das direkte Erstellen eines ZIP-Archivs aus einem Dateisystemordner bzw. das Entpacken in einen Zielordner.
- Bei ASP.NET Core gibt es in der Klasse WebApplication eine neue Methode CreateEmptyBuilder(), die einen WebApplicationBuilder ohne vordefiniertes Verhalten erzeugt. Der Aufrufer muss also alle Middleware und Dienste selbst hinzufügen. Dafür ist das Anwendungs-Bundle kleiner.
- Bei ASP.NET Core WebAPI gibt es seit Preview 5 generische Annotationen für Fälle, in denen man bisher einen Parameter vom Typ Type übergeben musste: [ProducesResponseType<T>], [Produces<T>], [MiddlewareFilter<T>], [ModelBinder<T>], [ModelMetadataType<T>], [ServiceFilter<T>] und [TypeFilter<T>].
- Die Klasse HttpClient bietet nun auch Unterstützung für HTTPS-basierte Proxies [32].
- Wenn sys als Webserver unter Windows verwendet wird, haben Entwickler und Entwicklerinnen, die Option, Response Buffering im Windows-Kernel zu aktivieren. Microsoft behauptet unter [33]: „In betroffenen Szenarien kann dies die Reaktionszeiten drastisch von Minuten (oder völligem Ausfall) auf Sekunden verkürzen.“
- Redis konnte bisher schon für Distributed Caching in ASP.NET Core verwendet werden [34]. Microsoft ermöglicht in .NET 8.0 nun auch die Nutzung von Redis für das in ASP.NET Core 7.0 eingeführte Webserverausgabencaching mit dem NuGet-Paket Extensions.Caching.StackExchangeRedis und dem Startcodeaufruf services.AddStackExchangeRedisOutputCache().
- Bei den Hashing-Klassen in Security wird nun auch SHA-3 angeboten. Die neuen Klassen heißen SDA3_256, SHA3_386 und SHA3_512 in Ergänzung zu den bisherigen Klassen SDA_256, SHA_386 und SHA_512.
- Microsoft hat in .NET 8.0 Preview 5 die Debugger-Ansicht einiger ASP.NET Core-Klassen verbessert, sodass diese nun statt dem Klassennamen einige der wesentlichen Daten anzeigen (11).
Abb. 11: Verbesserte Debugger-Ansicht in ASP.NET-Core-Projekten (Bildquelle: Microsoft)
Dr. Holger Schwichtenberg (20-maliger MVP) – alias „Der DOTNET-DOKTOR“ – gehört zu den bekanntesten .NET- und Webexperten in Deutschland. Er ist Chief Technology Expert bei der Softwaremanufaktur MAXIMAGO. Mit dem 43-köpfigen Expertenteam bei www.IT-Visions.de bietet er zudem Beratung und Schulungen zu über 950 Entwicklerthemen an. Seit 1998 ist er ununterbrochen Sprecher auf sämtlichen BASTA!-Konferenzen und Autor von mehr als 90 Fachbüchern sowie über 1 500 Fachartikeln.
E-Mail: [email protected]
Twitter: @dotnetdoktor
Web: www.dotnet-doktor.de
Links & Literatur
[1] https://www.youtube.com/watch?v=48G_CEGXZZM
[2] https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial
[3] https://github.com/mkArtakMSFT/BlazorWasmClientInteractivity
[4] https://github.com/danroth27/Net8BlazorAuto
[5] https://github.com/dotnet/aspnetcore/issues/49756
[6] https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-6
[7] https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-7
[8] https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-7/#antiforgery-integration-for-minimal-apis
[9] https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-7/#antiforgery-integration
[10] https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-5/#improved-packaging-of-webcil-files
[11] https://docs.duendesoftware.com/identityserver/v5/quickstarts/5_aspnetid
[12] https://github.com/davidfowl/IdentityEndpointsSample
[13] https://github.com/dotnet/aspnetcore/issues/46691
[14] https://devblogs.microsoft.com/dotnet/new-csharp-12-preview-features
[15] https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-5/#native-aot
[16] https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-6/#support-for-targeting-ios-platforms-with-nativeaot
[17] https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.libraryimportattribute
[18] https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-6
[19] https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-6/#limitations
[20] https://github.com/miloush
[21] https://devblogs.microsoft.com/dotnet/announcing-dotnet-maui-in-dotnet-8-preview-5
[22] https://devblogs.microsoft.com/dotnet/announcing-dotnet-maui-in-dotnet-8-preview-6
[23] https://devblogs.microsoft.com/dotnet/announcing-dotnet-maui-in-dotnet-8-preview-7
[24] https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-7/#hybridglobalization-mode-on-ios-tvos-maccatalyst-platforms
[25] https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-hybrid-mode.md
[26] https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-6/#hybridglobalization-mode-on-wasm
[27] https://devblogs.microsoft.com/visualstudio/announcing-the-dotnet-maui-extension-for-visual-studio-code
[28] https://www.nuget.org/packages/Microsoft.EntityFrameworkCore
[29] https://github.com/dotnet/efcore/issues/29989
[30] https://github.com/ajcvickers
[31] https://learn.microsoft.com/en-us/dotnet/core/compatibility/8.0
[32] https://github.com/dotnet/runtime/issues/31113
[33] https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-6/#http-sys-kernel-response-buffering
[34] https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed