.NET 8.0 Preview 3 und 4

Blazor auf dem Weg zur Universalität

Blazor auf dem Weg zur Universalität

.NET 8.0 Preview 3 und 4

Blazor auf dem Weg zur Universalität


In den Previews 3 und 4 gibt es bedeutende Neuerungen für Blazor, auch wenn Blazor United weiterhin nur ein Prototyp ist. Zudem kann man einige ASP.NET-Core-Dienste per Ahead-of-Time-Compiler übersetzen. C# 12.0 kann nun Typaliase und MSBuild liefert übersichtlichere Ausgaben.

Ich berichtete in der Ausgabe 6.23 des Windows Developer bereits über die .NET 8.0 Previews 1 und 2. Bis zum Redaktionsschluss für die Ausgabe, die Sie nun in den Händen halten, sind noch Preview 3 (11. April 2023) und Preview 4 (16. Mai 2023) erschienen. Bis zum Herbst wird es noch drei weitere Previews geben. Parallel zu .NET 8.0 Preview 4 [1] hat Microsoft bei Visual Studio 2022 einen Versionswechsel vollzogen: Version 17.6 ist nun die stabile Version und 17.7 die neue Vorschauversion. Für .NET 8.0 Preview 4 braucht man nun Version 17.7.

Blazor mit reinem Server-side Rendering

Seit .NET 8.0 Preview 3 erlaubt Blazor auch Server-side Rendering (SSR). Jetzt werden sicherlich viele Leser denken: Das ist doch gar nicht neu – Blazor Server gibt es seit September 2019! Nein, Blazor SSR ist nicht das gleiche wie Blazor Server. Aber man muss zugeben, dass die Nähe der Begriffe geeignet ist, viel Verwirrung zu stiften.

Bei dem bisher existierenden Blazor Server werden Razor Components auf dem Server ausgeführt. Das Ergebnis des Rendering, genauer gesagt, der Unterschiede des Rendering zum vorherigen Rendering (im Programmierlatein „Diff“ oder „Change Set“ genannt) wird dann zum Client übertragen (in der Regel per WebSocket-Verbindung) und dort in die Seite eingesetzt (Abb. 1).

Abb. 1: Bisherige Varianten von Blazor für den Browser: Blazor WebAssembly vs. Blazor ServerAbb. 1: Bisherige Varianten von Blazor für den Browser: Blazor WebAssembly vs. Blazor Server

Beim neuen Blazor SSR wird ebenfalls eine Razor-Komponente auf dem Server gerendert. Das Rendering-Ergebnis wird aber in einem Rutsch über eine normale HTTP-Verbindung zum Client gesendet. Blazor SSR besitzt damit grundsätzlich die gleiche 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 Razor-Komponenten 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. 2).

Abb. 2: Blazor SSR mit und ohne Streaming im Vergleich zu ASP.NET Core MVC und ASP.NET Core Razor PagesAbb. 2: Blazor SSR mit und ohne Streaming im Vergleich zu ASP.NET Core MVC und ASP.NET Core Razor Pages

Nun stellt sich die Frage, wofür Microsoft nun Blazor SSR einführt, wenn doch die Architektur grundsätzlich dieselbe ist wie bei ASP.NET Core MVC und ASP.NET Core Razor Pages. Darauf gibt es zwei Antworten:

  1. Ersatz für MVC und Razor Pages: Der Aufbau einer Komponente und auch die Razor-Syntax sind in Razor Components, die Blazor bisher schon in Blazor Server und Blazor WebAssembly verwendet, sind nicht dieselben wie bei MVC und Razor Pages. Indem Microsoft nun das modernere Komponentenmodell aus Blazor auch komplett auf den Server bringt, haben Entwickelnde zukünftig die Wahl, eine Razor-Komponente nicht nur in Blazor Server und Blazor WebAssembly, sondern auch komplett auf dem Server laufen zu lassen. Man kann sicherlich sagen: Das Grab für MVC und Razor Pages ist damit schon ausgehoben, auch wenn Microsoft wohl vorerst nicht verkünden wird, dass die beiden älteren Modelle beerdigt werden sollen.

  2. Grundlage für Blazor United: Ziel von Microsoft ist, mit Blazor United eine Architektur zu erschaffen, die Progressive Enhancements für eine moderne Webanwendung bieten soll. Eine Blazor-United-Webanwendung kann zunächst rein serverseitig gerendert werden, dann im Hintergrund zu Blazor Server mit Interaktivität per WebSocket-Verbindung und schließlich zur rein clientseitigen Ausführung mit Blazor WebAssembly übergehen, sobald die kompletten .NET-Assemblys im Browser geladen sind. Schon die erste Vorführung von Blazor United im Januar 2023 [2] war eindrucksvoll und scheint als Lösung für die bisher schwierige Wahl zwischen Blazor Server und Blazor WebAssembly geeignet. Allerdings gibt es Blazor United weiterhin nur als Prototyp [3], noch nicht als Teil der .NET 8.0 Previews 1 bis 4.

Listing 1 zeigt eine Razor-Komponente, die via Blazor SSR komplett auf dem Server gerendert wird. Der in Blazor bisher unbekannte Zusatz

@implements IRazorComponentApplication<Confirmation> 

ist derzeit notwendig, soll aber später noch entfallen. Listing 2 zeigt die zugehörige Layoutseite auch als Razor-Komponente. Eine Zentralkomponente App.razor wird nicht zwingend benötigt, kann es aber geben und ist sinnvoll, wenn die gesamte Webanwendung auf Razor-Komponenten basiert und man z. B. eine „Leider nicht gefunden“-Seite oder eine „Du kommst hier nicht rein“-Seite realisieren will.

Listing 1: Eine Razor Component für Blazor SSR

@page "/Confirmation/{Name}"
@layout MainLayout
@implements IRazorComponentApplication<Confirmation>
 
<h3>Bestätigung</h3>
<hr />
<p>Guten Tag @Name,</p>
<p>Ihr Zugang zu unserem <a href="https://www.IT-Visions.de/BuchABo">Fachbuch-Abo</a> wird am <b>@Datum</b> freigeschaltet.</p>
<p>Mit freundlichen Grüßen</p>
<p><a href="https://www.IT-Visions.de">www.IT-Visions.de</a></p>
 
@code {
  
  [Parameter]
  public string Name { get; set; } = "Max Mustermann";
  
  public string Datum { get; set; } = DateTime.Now.AddDays(1).ToShortDateString();
  
  protected override void OnInitialized()
  {
    if (String.IsNullOrEmpty(Name)) throw new ArgumentNullException(nameof(Name));
  }
  
  // oder
  // override protected async Task OnInitializedAsync()
  // {
  
  // }
}

Listing 2: Layoutseite mit Blazor SSR

@inherits LayoutComponentBase
@implements IRazorComponentApplication<MainLayout>
 
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
</head>
<body>
  MainLayout
  <hr />
  @Body
</body>
 
</html>

Derzeit gibt es noch keine eigenständige Projektvorlage für ein Projekt, das komplett mit Blazor SSR arbeitet. Man legt bisher ein MVC- oder Razor-Page-Projekt an und ergänzt in der Startdatei Program.cs diese Zeilen für Dependency Injection und Routing:

builder.Services.AddRazorComponents(); 
...
app.MapRazorComponents<Confirmation>();

MapRazorComponents<T> ruft man nur einmal für die Wurzelkomponente auf.

Integration von Blazor SSR mit MVC und Razor Pages

Blazor SSR ermöglicht auch eine Integration mit MVC und Razor Pages. So kann ein MVC-Controller nicht nur eine View, sondern nun auch eine Razor-Komponente rendern, indem er RazorComponentResult<Komponentenname> zurückliefert (Listing 3).

Listing 3: Ein MVC-Controller rendert nicht eine View, sondern eine Razor Component

using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Mvc;
 
namespace NET8_BlazorSSR;
 
public class MVCRCController : Controller
{
  public IResult Index()
  {
    return new RazorComponentResult<Confirmation>(new { Name = "Dr. Holger Schwichtenberg" });
  }
}

Zudem kann man eine solche Razor-Komponente auch in eine Razor Page (.cshtml-Datei) einbetten via Tag Helper <component>:

<component type="typeof(Confirmation)" render-mode="Static" param-name='"Dr. Holger Schwichtenberg"' />

Das funktioniert bisher schon genauso bei Blazor Server.

Formulare mit Blazor SSR

Blazor SSR kann seit Preview 4 auch Formulareinsendungen behandeln. Dazu müssen Entwickelnde in die Seite einen FormDataProvider injizieren:

@inject FormDataProvider FormData

um dann nach einem Formular-Submit per

FormData.Entries.TryGetValue("Feldname", out var feldnameValues)

auf erfolgte Einträge zugreifen zu können. Das ist noch sehr umständlich und fehlerbehaftet; Microsoft will hier noch richtiges Model Binding und Validierung implementieren. Daher verzichte ich an dieser Stelle auf ein Beispiel und berichte erneut darüber, wenn das Feature praxisreif ist.

Streaming bei Blazor SSR

Abbildung 2 zeigt im unteren Teil noch eine weitere, in .NET 8.0 kommende Option: Server-side Rendering mit Streaming. Dabei wird initial eine komplette Seite übertragen, wenn sich aber durch die Ausführung asynchroner Methoden Teile der Seite noch einmal ändern, werden diese Änderungen in der offenen HTTP-Verbindung nachübertragen (siehe einzelne HTML-Blöcke in unteren Teil von Abbildung 2). Dieser Mechanismus funktioniert freilich nicht ohne etwas JavaScript, das aber natürlich von Microsoft kommt. Man integriert für Streaming in die Layoutseite dieses Tag:

<script src="_framework/blazor.web.js" suppress-error="BL9992"></script>

Dabei ist in diesem Fall die sonst in Blazor übliche Fehlermeldung BL9992 zu unterdrücken („Script tags should not be placed inside components because they cannot be updated dynamically. To fix this, move the script tag to the 'index.html' file or another static location.“).

In der Seite, die Streaming machen soll, muss man dann noch

@attribute [StreamRendering(true)]

einfügen. Listing 4 zeigt ein Beispiel, bei dem Benutzende erst „Loading…“ sehen und dann nach Eintreffen der Daten die entsprechende Datentabelle.

Listing 4: Razor Component für Blazor SSR mit Streaming

@page "/weather"
@layout MainLayout
@using NET8_BlazorSSR.BL;
@using NET8_BlazorSSR.BO;
 
@inject WeatherForecastService ForecastService
@implements IRazorComponentApplication<FetchData>
@attribute [StreamRendering(true)] 
 
<PageTitle>Weather forecast</PageTitle>
 
<h1>Weather forecast</h1>
 
<p>This component demonstrates fetching data from a service.</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 WeatherForecast[]? ...