19. Juni 2024 von Daniil Zaonegin
Erstellen von .NET Blazor Hosted App mit dem neuen Blazor Web App Projekt Template (in .NET 8)
Blazor ist ein .NET-Front-End-Framework zum Erstellen eines interaktiven Web-UIs mit C# und nicht mit JavaScript. Das bietet viele Vorteile, wie zum Beispiel, dass man den Code zwischen Backend und Frontend teilen kann. Mit dem Blazor muss man nicht mehr zwei verschiedene Sprachen verwenden, JavaScript für das Frontend und C# für das Backend. Das erleichtert die Wiederverwendung von Code und das Schreiben konsistenter Anwendungen in einem vertrauten Sprachumfeld. Außerdem können viele Fehler bereits beim Kompilieren lösen und nicht mehr dynamisch in der Laufzeit der Anwendung zu bekommen, wie es in JavaScript war.
Die Hosting Modelle von Blazor
Blazor WebAssembly Hosting Modell
Hierbei werden clientseitige Komponenten im Browser mithilfe einer WebAssembly-basierten .NET-Runtime ausgeführt. Alle Razor-Komponenten, deren Abhängigkeiten und die .NET-Runtime werden in den Browser geladen und in diesem ausgeführt. Die Aktualisierung der Benutzeroberfläche und die Ereignisbehandlung erfolgen im selben Prozess.
Vorteile dieses Hostings Modells
- Blazor WebAssembly bietet eine schnellere Benutzererfahrung, denn die UI-Updates sind sofort ausgeführt, ohne dass ein Roundtrip zum Server erforderlich ist.
- Es ermöglicht Offline-Funktionen und kann als Progressive Web-App (PWA) ausgeführt werden.
- Die Skalierung der Anwendung ist einfacher, weil alle User eine Kopie der Anwendung ausführen.
Nachteile dieses Hostings Modells
- Blazor WebAssembly erfordert, dass mehr clientseitige Ressourcen heruntergeladen und ausgeführt werden, was zu einer langsamen Ladezeit beim ersten Anwendung-Start führt.
- Es kann weniger sicher sein, denn sensible Daten und Geschäftslogik auf der Client-Seite heruntergeladen und ausgeführt werden. Auf Client-Code können User zugreifen.
- WebAssembly ist in einer sicheren Umgebung im Browser ausgeführt. Deswegen gibt es viele Limitierungen und man kann nicht alle .NET-Bibliotheken nutzen.
Blazor Server-Hosting Modell
Bei diesem Ansatz erledigt der Browser nur das Rendering, der gesamte Code wird auf dem Server ausgeführt. Dazu wird eine ständige Verbindung zum Server mittels SignalR aufgebaut, einer Open-Source-Bibliothek die Real-time Webanwendungen ermöglicht.
Vorteile dieses Hosting Modells
- Es ist relativ einfach, der Code ist auf dem Server in einem normalen .NET-Prozess ausgeführt und man kann volles .NET mit allen Bibliotheken nutzen.
- Es hat kürzere Ladezeiten beim ersten App-Start, weil weniger clientseitiger Code heruntergeladen und ausgeführt werden muss.
- Es bietet ein besseres Sicherheitsmodell, da sensible Daten und Geschäftslogik auf dem Server gespeichert ist und Client hat darauf keinen Zugriff.
Nachteile dieses Hostings Modells
- Blazor Server erfordert eine ständige Verbindung mit dem Server, was ein Nachteil sein kann, wenn User eine schwache oder unzuverlässige Internetverbindung haben.
- Das Implementieren komplexer UI-Interaktionen kann schwieriger sein, da die UI-Updates langsamer sind als bei Blazor WebAssembly.
- Die Skalierung der Anwendung kann eine große Herausforderung sein, weil das Halten der ständigen Verbindung zwischen Server und Client viele Ressourcen erfordert.
Beide Hostings Modelle haben ihre Vor- und Nachteile. Bis .NET 7 gab es die Möglichkeit, eine Blazor-Anwendung in eine so genannten “Hosted Template” auszuliefern. Dabei war das Template eine Standard-ASP.NET-Core App, die eine Blazor-App an den Client ausgeliefert hat. Dieses Template ermöglichte es, die WebAssembly-Variante zu nutzen und gleichzeitig sensitive Logik auf dem Server auszuführen. Damit profitierte man von den Vorteilen beider Modelle und reduzierte gleichzeitig die Nachteile. Seit .NET 8 existiert dieses Template leider nicht mehr. Es existiert jedoch ein neues Template, welches die gleiche Funktionalität bietet.
Das neue Blazor Web-App Template
In diesem Beitrag werden wir uns das neue Blazor Web-App mit Rendermodus InteractiveAuto genauer ansehen. Dieses Template aktiviert ein automatisches serverseitiges Pre-Rendering der WebApp-Seiten. Zuerst sehen User eine Seite, die auf dem Server gerendert wurde. Die UI-Logik wird auf dem Server ausgeführt und über eine Web-Sockets-Verbindung an das Frontend übergeben. Sobald die Binaries der App vollständig geladen sind, schließt der Client die Web-Socket-Verbindung und nutzt Wasm-Assemblies im Browser.
Eine entsprechende Blazor Web-App kann man mit diesem Cli-Befehl erstellen:
dotnet new blazor --interactivity Auto -n BlazorWeatherApp
Damit bekommen wir folgendes Projekt:
BlazorWeatherApp
Ein Blazor-Serverprojekt, in dem sich die Seiten und Komponenten befinden. Diese können entweder statisch serverseitig gerendert oder interaktiv sein.
- Statisch gerenderte Komponenten werden als einfache HTML-Dateien vom Server an den Client übergeben und haben keine Interaktivität.
- Interaktiv serverseitig gerenderte Komponenten sind interaktiv und werden auf der Serverseite ausgeführt. Sie werden über Web-Sockets-Verbindung an den Client übergeben.
BlazorWeatherApp.Client
Das Blazor-Clientprojekt, in dem sich die Komponenten befinden, die entweder WebAssembly-Rendering oder automatisches Rendering verwenden. Zuerst werden diese Komponenten serverseitig gerendert und dann, wenn die DLL-Dateien im Browser heruntergeladen sind, erfolgt das Rendering clientseitig in WebAssembly.
Dieses Projekttemplate ermöglicht es uns, in einer App sowohl serverseitig als auch clientseitig gerenderte Komponenten zu schreiben. Außerdem löst es das Problem der langen Ladezeiten beim ersten Start. Wenn die Seite Rendering-Modus „Auto“ verwendet, dann nutzt die Anwendung beim Starten serverseitig gerenderte Seiten und wechselt zur Verwendung von WebAssembly mit allen seinen Vorteilen sobald alle benötigten DLLs an den Client übertragen wurden. Sollte die WebAssembly-Version nicht funktionieren, erfolgt automatisch eine Rückkehr zu einer serverseitig gerenderten Seite. Lassen Sie uns ein Bespiel für diese Seite schreiben.
Erstellen einer WebAssembly-Seite im Blazor.Client Projekt
Seiten, die via WebAssembly ausgeführt werden, werden im BlazorWeatherApp.Client-Projekt erstellt. Die folgende Abbildung zeigt das Rohgerüst einer solchen Seite.
@rendermode InteractiveWebAssembly
weist Blazor an, die Seite im Browser zu rendern.
@page
weist Blazor an, welche Route hat die Komponente. Die Komponente mit Route ist eine Webseite. Mehr dazu hier: Seiten, Routing und Layouts - .NET | Microsoft Learn.
Lasst uns eine einfache Seite erstellen, die Daten vom Backend abruft. Dafür werden wir einen Controller auf Serverseite verwenden (in BlazorWebApp).
Controller erstellen im Serverprojekt
Im Serverprojekt erstellen wir einen einfachen WeatherForecastController, der aus dem ASP.NET Core Web API Template stammt.
using BlazorWeatherApp.Shared;
using Microsoft.AspNetCore.Mvc;
namespace BlazorWeatherApp.Controller;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
Das Modell (WeatherForecast) habe ich im BlazorWeatherApp.Shared Projekt erstellt. Dieses Projekt ist eine einfache Klassenbibliothek, die sowohl vom BlazorWeatherApp.Client als auch von der BlazorWeatherApp referenziert wird. Die finale Projektstruktur in der Solution sieht so aus:
Das BlazorWeatherApp.Shared-Projekt enthält Klassen, die sowohl vom Serverprojekt (BlazorWeatherApp) als auch vom Clientprojekt (BlazorWeatherApp.Client) verwendet werden.
Da wir einen Controller verwenden um Daten vom Server abzugreifen, müssen wir die notwendige ASP.NET Core Services mittels AddControllers() Methode zur Dependency Injection hinzufügen.
Daten abrufen auf dem Frontend
Um die Daten aus dem Frontend abzurufen, benötigen wir einen HttpClient. Es gibt verschiedene Möglichkeiten, diesen zu verwenden. An dieser Stelle nutzen wir eine HttpClientFactory. Um diese nutzen und via Dependency Injection einfügen zu können, müssen wir das Paket Microsoft.Extensions.Http zum Projekt hinzufügen und die Factory bzw. den HttpClient via AddHttpClient() verfügbar machen.
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services
.AddHttpClient()
.ConfigureHttpClientDefaults(
d => d.ConfigureHttpClient(c => c.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)));
await builder.Build().RunAsync();
In dem oben gezeigten Codeabschnitt habe ich noch die BaseAddress des HttpClient als BaseAddress der Webanwendung (builder.HostEnvironment.BaseAddress) konfiguriert.
Jetzt können wir in unserer Weather-Seite die Daten abrufen. Dafür müssen wir den HttpClient in die Seite injizieren. Die Daten können beispielsweise während der Initialisierung der Komponente abgerufen werden. Dazu überschreiben wir die Methode OnInitializedAsync() und rufen die Erweiterungsmethode GetFromJson() des HttpClient auf. Den vollständigen Code für die Seite sehen Sie hier:
@page "/weather"
@using BlazorWeatherApp.Shared
@rendermode InteractiveWebAssembly
@inject HttpClient Http
<h3>Weather</h3>
@if (_weatherForecast == null)
{
<div>Loading...</div>
}
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 row in _weatherForecast)
{
<tr>
<td>@row.Date</td>
<td>@row.TemperatureC</td>
<td>@row.TemperatureF</td>
<td>@row.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? _weatherForecast;
protected override async Task OnInitializedAsync()
{
_weatherForecast =
await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
}
Wenn man das Projekt startet, sollte die Seite logischerweise in WebAssembly im Browser interaktiv gerendert sein. Aber wenn man die Weather-Seite aufruft, erhält man stattdessen einen Fehler:
Sehr merkwürdig, wir haben doch den HttpClient zur Abhängigkeitsinjektion hinzugefügt. Der Grund für dieses Verhalten ist an dieser Stelle, dass Prerendering standardmäßig aktiviert ist, das heißt das System versucht nun, die Seiten zuerst auf dem Server zu rendern. Die WebAssembly-App (der wir den HttpClient hinzugefügt haben) startet erst, nachdem alle benötigten Bibliotheken in den Browser geladen wurden. Um dieses Problem zu lösen, können wir das Prerendering deaktivieren. Dazu ändern wir die Zeile mit @rendermode wie folgt: @rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
. Dadurch wird die Seite funktionieren und wir sehen das Ergebnis:
Vorrendern Fix
Um das Problem mit dem Vorrendern zu beheben, müssen wir auf dem Server Weatherforecast-Daten abrufen. Dafür sollen wir ein typisierten HttpClient in dem Clientprojekt (BlazorWeatherApp.Client) verwenden (mehr dazu hier: Use IHttpClientFactory to implement resilient HTTP requests - .NET | Microsoft Learn)
using System.Net.Http.Json;
using BlazorWeatherApp.Shared;
namespace BlazorWeatherApp.Client.Services;
public class ClientWeatherService: IWeatherService
{
private readonly HttpClient _http;
public ClientWeatherService(HttpClient http)
{
_http = http;
}
public Task<WeatherForecast[]?> GetWeatherForecastsAsync()
{
return _http.GetFromJsonAsync<WeatherForecast[]?>("WeatherForecast");
}
}
Die Schnittstelle IWeatherService habe ich in BlazorWeatherApp.Shared erstellt, damit wir sie auch in dem Serverprojekt verwenden können.
namespace BlazorWeatherApp.Shared;
public interface IWeatherService
{
Task<WeatherForecast[]?> GetWeatherForecastsAsync();
}
Jetzt kann man einen typisierten HttpClient registrieren und auf der Weather-Seite nutzen. Hier ist unsere neue Program.cs-Datei. Für die Abhängigkeitsinjektion-Registrierung verwenden wir die Erweiterungsmethode AddHttpClient<>():
using BlazorWeatherApp.Client.Services;
using BlazorWeatherApp.Shared;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services
.AddHttpClient<IWeatherService, ClientWeatherService>(
c => c.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
await builder.Build().RunAsync();
Jetzt können wir die Weather-Seite so umschreiben
@page "/weather"
@using BlazorWeatherApp.Shared
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
@inject IWeatherService WeatherService
@* ... *@
@* Code omitted for brevity *@
@* ... *@
@code {
private WeatherForecast[]? _weatherForecasts;
protected override async Task OnInitializedAsync()
{
_weatherForecasts =
await WeatherService.GetWeatherForecasts();
}
}
Nun wird auf der Seite nicht mehr HttpClient injiziert, sondern IWeatherService. Auf der Client-Seite wird IWeatherService als typisierter HttpClient initialisiert, was bedeutet, dass innerhalb des erstellten Objekts ein HttpClient zur Verfügung steht, mit dem wir auf den Controller im Backend zugreifen können.
Auf der Serverseite hingegen benötigen wir natürlich keinen HttpClient – wir sind auf dem Server und haben daher direkten Zugriff auf die Daten. Daher verwenden wir eine andere Klasse auf der Serverseite, die allerdings ebenfalls IWeatherService implementiert:
using BlazorWeatherApp.Shared;
namespace BlazorWeatherApp.Services;
public class ServerWeatherService : IWeatherService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public Task<WeatherForecast[]?> GetWeatherForecastsAsync()
{
return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
}).ToArray())!;
}
}
Den Code habe ich vom WeatherController kopiert. Jetzt können wir den Service in der Server-App registrieren (natürlich nicht als HttpClient, sondern einfach nur als scoped Objekt). Der Vorteil: Wir können diese Klasse jetzt auch im Controller injizieren und dadurch duplizierten Code vermeiden
BlazorWeatherApp/Program.cs:
//…
//Code omitted for brevity
//…
builder.Services.AddControllers();
builder.Services.AddScoped<IWeatherService, ServerWaetherService>();
WeatherController.cs:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IWeatherService _weatherService;
public WeatherForecastController(ILogger<WeatherForecastController> logger,
IWeatherService weatherService)
{
_logger = logger;
_weatherService = weatherService;
}
[HttpGet(Name = "GetWeatherForecast")]
public Task<WeatherForecast[]?> Get()
{
return _weatherService.GetWeatherForecastsAsync();
}
}
Jetzt kann man das Prerendering im Client wieder aktivieren, sinnvollerweise mit „Interactive Auto“ um die Webassembly-Seite automatisch zu laden, sobald sie im Browser verfügbar ist. Davor ist die Seite auf dem Server gerendert.
Beim Laden werden zunächst die Daten angezeigt, die vom Server ermittelt wurden. Sobald alle DLLs für die Client-App übertragen wurden, ändern sich diese Daten – weil jetzt die WebAssembly-App startet und ihre Daten neu vom Server abruft.
Speichern des Renderzustands
Nicht immer ist es erwünscht, dass die Daten neu geladen werden, je nach Applikation möchte man den aktuellen Zustand, der aus dem Prerendering stammt, an die Client-App übergeben. Das verhindert dann, dass die Daten neu geladen werden müssen. Das funktioniert über den PersistentComponentState. Diese Klasse wird über die DependencyInjection übergeben und kann wie folgt verwendet werden:
@page "/weather"
@* Code omitted for brevity *@
@implements IDisposable
@inject PersistentComponentState ApplicationState
@* Code omitted for brevity *@
@code {
private WeatherForecast[]? _weatherForecasts;
private PersistingComponentStateSubscription _subscription;
protected override async Task OnInitializedAsync()
{
_subscription = ApplicationState.RegisterOnPersisting(Persist);
var foundInState =
ApplicationState.TryTakeFromJson<WeatherForecast[]>("weather", out var forecasts);
_weatherForecasts = foundInState
? forecasts
: await WeatherService.GetWeatherForecastsAsync();
}
private Task Persist()
{
ApplicationState.PersistAsJson("weather", _weatherForecasts);
return Task.CompletedTask;
}
public void Dispose()
{
_subscription.Dispose();
}
}
Mit der Methode ApplicationState.RegisterOnPersisting() registriert man die Callback-Methode, die aufgerufen wird, wenn die Serveranwendung gestoppt wird und die Client-Anwendung startet. Mit der Methode ApplicationState.PersistAsJson() kann man die Daten speichern, und mit der Methode ApplicationState.TryTakeFromJson() aus dem Persistant State abrufen. Der Zustand wird als JSON vom Server an die Client-App übergeben und kann nur einmal genutzt werden (danach gibt die Methode ApplicationState.TryTakeFromJson() false zurück). Damit werden die Daten nicht mehr neu geladen.
Zusammenfassung
Wir haben eine Blazor-Anwendung erstellt, die ähnliche Eigenschaften aufweist, wie die Blazor ASP.NET Hosted App aus .NET 7. Diese Anwendung besteht aus einem WebAssembly-Projekt, in dem man WebAssembly-Komponenten erstellen kann, und einem Serverprojekt, in dem man APIs schreiben kann, mit einer Datenbank arbeiten kann und alles tun, was in WebAssembly im Browser nicht möglich ist. Weitere Möglichkeiten sind:
- 1. Man kann serverseitig gerenderte Seiten erstellen, bei denen die gesamte Logik auf dem Server ausgeführt und dann an den Client übergeben wird.
- 2. Man kann auch die Seiten mit Auto-Rendering Modus schreiben. Der Code von diesen Seiten wird zuerst auf dem Server ausgeführt (wenn die DLL-Dateien der Anwendung im Browser noch nicht heruntergeladen sind) und dann über eine WebSocket-Verbindung an den Browser übergeben. Sobald die DLL-Dateien geladen sind, startet die WebAssembly-Anwendung, die auch offline funktionieren kann (sofern die Daten vom Server geladen sind). Allerdings erfordert dies das Schreiben von Code sowohl auf dem Server als auch auf dem Client, in der WebAssembly-Version.
Den Quellcode der App findest du auf Github: daniilzaonegin/BlazorWeatherApp: Example of new Blazor Web App in .net 8.0 (github.com).
Ihr möchtet gern mehr über spannende Themen aus der adesso-Welt erfahren? Dann werft auch einen Blick in unsere bisher erschienenen Blog-Beiträge.