zurück zum Artikel

Blazor-Entwicklung: Komponenten, die immer passen

Dr. Holger Schwichtenberg

(Bild: Shutterstock)

Blazor-Anwendungen mit unterschiedlichen Schichtarchitekturen können gemeinsame Razor Components nutzen: Ein Fallbeispiel zeigt den Einsatz in der Praxis.

Entwickler und Entwicklerinnen können mithilfe einer Razor Class Library gemeinsame Razor Components zwischen allen vier Blazor-Varianten (Blazor WebAssembly, Blazor Server, Blazor Desktop und Blazor MAUI) einsetzen, wie schon der Artikel "Eine Blazor-App für alle Plattformen [1]“ gezeigt hat.

Zu den Unterschieden zwischen den vier Blazor-Arten gehört auch, dass sie verschiedene Schichtenarchitekturen (Bild 1 und 2) ermöglichen:

Bild 1: Die Architektur von Blazor WebAssembly und Blazor Server

(Bild: Dr. Holger Schwichtenberg)

Bild 2: Die Architektur von Blazor Desktop und Blazor MAUI

(Bild: Dr. Holger Schwichtenberg)

Wer eine Blazor-Anwendung schreiben will, die in einer Razor Class Library läuft und von allen Blazor-Arten eingebunden werden kann (via Kopf-Projekten, siehe "Eine Blazor-App für alle Plattformen [2]“), muss den Zugriff auf die benötigten Ressourcen in allen Blazor-Arten ermöglichen. Ein erster Gedanke: Der gemeinsame Nenner zwischen allen Blazor-Arten ist eine 3-Tier-Anwendung. Wenn also Razor Components in der Razor Class Library immer per Webservice auf die Datenbank zugreifen, laufen sie in allen Blazor-Arten.

Das stimmt, ist aber die einzige Option. Wer in Blazor Server und Blazor Desktop mit Webservices auf einem Application Server arbeitet, obwohl er das Datenbankmanagementsystem auch direkt ansprechen könnte, handelt sich zusätzlichen Netzwerkverkehr ein, der die Anwendung zunächst verlangsamt. Ein Application Server kann dies gegebenenfalls durch gutes Caching wieder ausgleichen.

Hier soll aber ein Ansatz präsentiert werden, bei dem Blazor Server und Blazor Desktop die Datenbank direkt verwenden, während Blazor WebAssembly und Blazor MAUI per Webservice auf die gleiche Datenbank zugreifen. Dabei verwenden alle vier Blazor-Arten die gleichen Razor Components in einer gemeinsamen Razor Class Library.

Entwickler können dafür den Datenzugriff komplett in die Kopfprojekte verlagern und die Razor Components in der Razor Class Library auf das Rendering reduzieren. In diesem Beispiel können die Razor Components in der Razor Class Library Daten abrufen und speichern. Dazu wird eine Abstraktionsschicht für den Datenzugriff zwischen 2-Tier und 3-Tier eingezogen, die mit einem Trick sehr effizient ist.

MiracleList ist ein praxisnahes Fallbeispiel einer Single-Page-Web-Application zur Aufgabenverwaltung (Bild 3), das der Autor dieses Beitrags in seinen Büchern zu Blazor [3] und Vue.js [4] verwendet. Das Fallbeispiel zeigt viele User Experience-Aspekte moderner Webanwendungen, beispielsweise Responsive Webdesign, modale Dialoge, Drag&Drag , Kontextmenüs, Push-Nachrichten mit Toast-Benachrichtigungen sowie Progressive Web Apps.

MiracleList realisiert eine realitätsnahe Aufgabenverwaltung mit Aufgabenkategorie, Aufgaben und Unteraufgaben sowie Dateizuordnung.

(Bild: Dr. Holger Schwichtenberg)

Es gibt vier Implementierungen des MiracleList-Frontends mit verschiedenen Blazor-Varianten:

Diese vier Blazor-Implementierungen der MiracleList teilen sich eine gemeinsame Benutzeroberfläche, die in einer Razor Class Library realisiert ist. Im Architekturschaubild in Bild 4 zeigt die oberste Reihe die vier Kopfprojekte für die vier Blazor-Arten. Hier befinden sich der Startcode und der Anwendungszustand sowie eine Authentifizierungsklasse. Darunter folgen die gemeinsame Razor Class Library "MLBlazorRCL"sowie die 2-Tier/3-Tier-Abstraktion.

Alle wesentlichen Razor Components der Anwendung stecken in der Razor Class Library MLRazorRCL. Für alle vier Blazor-Arten gibt es zusätzlich jeweils ein Kopfprojekt (siehe Bild 4), das diese Razor Class Library referenziert. In den Kopfprojekten gibt es jeweils folgende Inhalte:

Grundkonzepte für eine Abstraktionsschicht zwischen einer 2-Tier-Architektur und einer 3-Tier-Architektur sind Interfaces und Dependency Injection. Dazu schreibt eine Entwicklerin oder ein Entwickler eine Schnittstellendefinition mit Operationen für alle Lese- und Schreibzugriffen auf die Datenbank beziehungsweise andere Ressourcen. Dann erzeugt sie auf dieser Basis zwei Implementierungen: eine, die direkt die Geschäftslogik verwendet und eine zweite, die die Geschäftslogik über Webservices aufruft. Die Schnittstelle liegt in einem Projekt, das vier Blazor-Frontends referenzieren. Zudem wird die jeweils notwendige Implementierung der Schnittstelle als Projekt referenziert. Anschließend erfolgt die Verbindung von Schnittstelle zur gewünschten Implementierung per Dependency Injection.

Die Implementierung dieser drei Typen (eine Schnittstelle und zwei Klassen) kann aufwendig sein. Hier soll ein Ansatz gezeigt werden, der sich als der einfachste Weg für die Realisierung der Abstraktion erwiesen hat, da dabei zwei der drei oben genannten Typen automatisch generiert werden. Dafür ist es Voraussetzung, dass die Webservices bereits erstellt wurden und Metadaten auf Basis der Open API Specification (OAS), alias Swagger, bereitstellen. Das MiracleList-Backend [5] stellt OAS-Metadaten zur Verfügung. Das Backend basiert auf ASP.NET Core. Dort ist die Bereitstellung von Metadaten sehr einfach möglich und kann bereits beim Erstellen eines WebAPIs-Projekts per Häkchen aktiviert werden.

Mit OAS-Metadaten können Entwickler eine Proxy-.NET-Klasse für den Zugriff auf das Backend generieren. Visual Studio stellt dafür im Projektast "Connected Services" über "Add Connected Service/Service Reference (OpenAPI, gRPC)" einen Codegenerator bereit. Dieser bietet allerdings kaum Einstellmöglichkeiten und damit auch keinen Einfluss auf die Codegenerierung. Daher sei hier das Werkzeug NSwag Studio von Rico Sutter [6] empfohlen (Bild 5). Hier können Entwicklerinnen und Entwickler beispielsweise Shared Contracts nutzen, also gemeinsame Assemblies zwischen Client und Server verwenden, sodass bei der Proxy-Generierung bekannte Typen nicht erneut erzeugt werden.

Ausschnitt aus den vielfältigen Einstellungen in NSwagStudio bei der Generierung von WebAPI-Proxies mit TypeScript oder C#

(Bild: Dr. Holger Schwichtenberg)

Als eine weitere Option bietet NSwagStudio an, eine Schnittstelle direkt für die Proxyklasse zu erzeugen. Im Beispiel der MiracleList erhält ein Programmierer so die im folgenden Listing gezeigte Schnittstellendefinition. Für die bessere Lesbarkeit hat die Redaktion die Listings 1 und 2 mit zusätzlichen Umbrüchen im Vergleich zum GitHub-Repository versehen. Dort auf GitHub [7] finden Leser und Leserinnen dann den kompletten Programmcode.

namespace MiracleList;
 
/// <summary>
/// Diese ist eine aus dem generierten MiracleListProxy 
/// heraus erstellte Schnittstelle zur Abstraktion zwischen
/// MiracleListProxy (3-Tier) und MiracleListNoProxy (2-Tier).
/// </summary>
public interface IMiracleListProxy
{
 Task<LoginInfo> LoginAsync(LoginInfo loginInfo);
 Task<bool> LogoffAsync(string token);
 Task<List<BO.Category>> CategorySetAsync(string mL_AuthToken);
 Task<BO.SubTask> ChangeSubTaskAsync(BO.SubTask st, 
                                     string mL_AuthToken);
 Task<BO.Task> ChangeTaskAsync(BO.Task t, string mL_AuthToken);
 Task<BO.Category> CreateCategoryAsync(string name, 
                                       string mL_AuthToken);
 Task<BO.Task> CreateTaskAsync(BO.Task t, string mL_AuthToken);
 System.Threading.Tasks.Task DeleteCategoryAsync(int id, 
                                          string mL_AuthToken);
…
 Task<bool> RemoveFileAsync(int id, string name, 
                            string mL_AuthToken);
 Task<IDictionary<string, FileInfoDTO>> FilelistAsync(int id, 
                                          string mL_AuthToken);
 Task UploadAsync(int id, string mL_AuthToken, FileParameter file);
}

Listing 1 MiracleListProxy.cs

Die zugehörige generierte Implementierung der Proxyklasse für den Zugriff auf die Webservices ist sehr lang (rund 1600 Zeichen) und daher in nächsten Listing nur in einem kleinen, exemplarischen Ausschnitt für die WebAPI-Operation /CategorySet/{categoryid} wiedergegeben.

[System.CodeDom.Compiler.GeneratedCode("NSwag", …)]
public partial class MiracleListProxy : IMiracleListProxy
{
 private System.Net.Http.HttpClient _httpClient;
 private System.Lazy<Newtonsoft.Json.JsonSerializerSettings>
   _settings;
 
 public MiracleListProxy(System.Net.Http.HttpClient httpClient)
 {
  _httpClient = httpClient;
  _settings = 
    new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
  {
   var settings = new Newtonsoft.Json.JsonSerializerSettings();
   UpdateJsonSerializerSettings(settings);
   return settings;
  });
 }

…

 /// <summary>Liste der Kategorien</summary>
 /// <returns>Success</returns>
 /// <exception cref="ApiException">A server side error occurred.</exception>
 public System.Threading.Tasks.Task<System.Collections.Generic.List<Category>>
   CategorySetAsync(string mL_AuthToken)
 {
  return CategorySetAsync(mL_AuthToken, 
                          System.Threading.CancellationToken.None);
 }
 
 /// <summary>Liste der Kategorien</summary>
 /// <returns>Success</returns>
 /// <param name="cancellationToken">A cancellation token that can be used
 /// by other objects or threads to receive notice of cancellation.</param>
 /// <exception cref="ApiException">A server side error occurred.</exception>
 public async System.Threading.Tasks.Task<System.Collections.Generic.List<Category>>
   CategorySetAsync(string mL_AuthToken, 
                    System.Threading.CancellationToken cancellationToken)
 {
  var urlBuilder_ = new System.Text.StringBuilder();
  urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/v2/CategorySet");
 
  var client_ = _httpClient;
  try
  {
   using (var request_ = new System.Net.Http.HttpRequestMessage())
   {
    if (mL_AuthToken != null)
     request_.Headers.TryAddWithoutValidation(
       "ML-AuthToken", ConvertToString(
         mL_AuthToken, System.Globalization.CultureInfo.InvariantCulture));
    request_.Method = new System.Net.Http.HttpMethod("GET");
    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse
                                ("application/json"));
 
    PrepareRequest(client_, request_, urlBuilder_);
    var url_ = urlBuilder_.ToString();
    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);
    PrepareRequest(client_, request_, url_);
 
    var response_ = await client_.SendAsync(
      request_, 
      System.Net.Http.HttpCompletionOption.ResponseHeadersRead, 
      cancellationToken).ConfigureAwait(false);
    try
    {
     var headers_ = 
       System.Linq.Enumerable.ToDictionary(response_.Headers, 
                                           h_ => h_.Key, h_ => h_.Value);
     if (response_.Content != null && response_.Content.Headers != null)
     {
      foreach (var item_ in response_.Content.Headers)
       headers_[item_.Key] = item_.Value;
     }
     ProcessResponse(client_, response_);
 
     var status_ = ((int)response_.StatusCode).ToString();
     if (status_ == "200")
     {
      var objectResponse_ = await 
        ReadObjectResponseAsync<System.Collections.Generic.List<Category>>
          (response_, headers_).ConfigureAwait(false);
      return objectResponse_.Object;
     }
     else
     if (status_ != "200" && status_ != "204")
     {
      var responseData_ = response_.Content == null ? null : 
        await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
      throw new ApiException(
        "The HTTP status code of the response was not expected (" 
        + (int)response_.StatusCode + ").", 
        (int)response_.StatusCode, responseData_, headers_, null);
     }
 
     return default(System.Collections.Generic.List<Category>);
    }
    finally
    {
     if (response_ != null)
      response_.Dispose();
    }
   }
  }
  finally
  {
  }
 }
…
}

Listing 2. MiracleListProxy.cs ist die generierte Proxyklasse für den Webservicezugriff

Auf dieser Basis kann er dann eine zweite Implementierung der Schnittstelle IMiracleListProxy schaffen. Die Klasse heißt MiracleListNoProxy. Dieser Name drückt aus, dass die Implementierung keinen Proxy für das WebAPI darstellt, sondern direkt die Manager-Klassen der Geschäftslogik im gleichen Prozess verwendet, wie im nächsten Listing zu sehen. Diese Klasse MiracleListNoProxy kann in eine eigene Assembly verpackt werden, kann aber auch Teil der Geschäftslogikschicht sein.

In der Implementierung in diesem Listing wird das übergebene Token ohne vorherige Inhaltsprüfung in eine Zahl konvertiert. Die 2-Tier-Variante braucht kein Authentifizierungstoken. Entwickler und Entwicklerinnen können hier direkt die Ganzzahl-Primärschlüssel der Tabelle mit den Benutzerdaten verwenden. Sollte die Benutzerschnittstellensteuerung etwas anderes als eine Zahl übergeben, wäre das ein klarer Fehler, der zum Laufzeitfehler in der Anwendung führen sollte.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BO;
using MiracleList;
 
namespace BL;
 
public class MiracleListNoProxy : MiracleList.IMiracleListProxy
{
…
 public Task<List<Category>> CategorySetAsync(string mL_AuthToken)
 {
  var bl = new CategoryManager(Int32.Parse(mL_AuthToken));
  var r = bl.GetCategorySet();
  return System.Threading.Tasks.Task.FromResult(r);
 }
 …
}

Listing 3. Dieser Ausschnitt aus MiracleListNoProxy.cs greift direkt auf die Geschäftslogik zu

In den MiracleList-Implementierungen wird jeweils die eine oder andere Implementierung per Dependency Injection injiziert:

services.AddScoped<MiracleListAPI.IMiracleListProxy, MiracleListAPI.MiracleListProxy>();

oder

services.AddScoped<IMiracleListProxy, MiracleListNoProxy>();

Alle Komponenten beziehen dann per Dependency Injection ein Objekt mit diesem Schnittstellentyp, entweder innerhalb der Razor-Datei mit:

@using MiracleList;
@inject IMiracleListProxy proxy oder innerhalb der Code-Behind-Datei mit

[Inject] MiracleList.IMiracleListProxy proxy { get; set; } = null;

Danach können alle Razor Components im Projekt MLBlazorRCL das per Dependency Injection gelieferte Proxy-Objekt verwenden. Sie müssen nichts darüber wissen, ob tatsächlich eine Kommunikation über den Webservice oder ein direkter Datenbankzugriff erfolgt:

var loginResult = await proxy.LoginAsync(loginData);
if (String.IsNullOrEmpty(loginResult.Message)) // OK
{

var categorySet = await proxy.CategorySetAsync(loginResult.Token);…}

Der Programmcode der generierten HTTP-Client-Proxy-Klasse (circa 1600 Zeilen) sowie der "NoProxy"-Implementierung (circa 100 Zeilen) sind hier aufgrund der Länge nicht komplett abgedruckt. Er ist aber komplett auf GitHub zu finden [8].

Ein Authentication State Provider ist ein Mechanismus von Blazor. Für einige Blazor-Mechanismen, wie zum Beispiel die Autorisierungsdirektive @attribute [Authorize] und das rollenabhängige Rendering mit der Komponente <AuthorizeView>, ist eine von AuthenticationStateProvider abgeleitete Klasse notwendig, die in der Startup-Klasse in der Methode ConfigureServices() zu registrieren ist:

services.AddScoped<AuthenticationStateProvider, MeinAuthenticationStateProvider>();

In der Implementierung des Authentication State Provider sind zwei Dinge wichtig: Die Klasse muss die Methode
async Task<AuthenticationState> GetAuthenticationStateAsync() überschreiben und in dem zurückgelieferten AuthenticationState die Blazor-Infrastruktur auf deren Anfrage jederzeit über den aktuellen Status (welche Benutzer angemeldet sind oder dass kein Benutzer angemeldet ist) informieren. Zudem muss die Klasse nach der erfolgreichen Benutzeranmeldung beziehungsweise bei einer Benutzerabmeldung die Blazor-Infrastruktur über einen Aufruf von NotifyAuthenticationStateChanged() unter Angabe eines AuthenticationState-Objekts aktiv benachrichtigen.

In MiracleList wird die eigene Implementierung von Authentication State Provider darüber hinaus ebenfalls für weitere Mechanismen genutzt: An- und Abmeldung von Benutzer und Benutzerinnen, Wechsel des Backends, Prüfung der Verfügbarkeit eines Backends. Dafür gibt es bei MiracleList eine Schnittstelle IMLAuthenticationStateProvider, die zusätzliche Methoden vorsieht; die im folgenden Listing zu sehen sind.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Authorization;
using MiracleList;
 
namespace MiracleList;

public interface IMLAuthenticationStateProvider
{
 /// <summary>
 /// Ermittelt den aktuellen Anmeldezustand
 /// </summary>
 Task<AuthenticationState> GetAuthenticationStateAsync();
 
 /// <summary>
 /// Legt das aktuelle Backend fest
 /// </summary>
 /// <param name="backend">URL oder Connection String</param>
 Task SetCurrentBackend(string backend);
 
 /// <summary>
 /// Prüft, ob das Backend verfügbar ist
 /// </summary>
 /// <param name="backend">URL oder Connection String</param>
 Task<BackendState> CheckBackend(string backend);
 
 /// <summary>
 /// Benutzer anmelden
 /// </summary>
 Task<LoginInfo> LogIn(string username, string password, string backend);
 
 /// <summary>
 /// Benutzer abmelden
 /// </summary>
 Task Logout();

Listing 4. MiracleList_Interfaces/IMLAuthenticationStateProvider.cs

Von dieser Schnittstelle IMLAuthenticationStateProvider gibt es dann zwei Implementierungen:

Beide diese Klassen erben von der in Blazor integrierten Basisklasse AuthenticationStateProvider und implementieren zusätzlich die eigene Schnittstelle IMLAuthenticationStateProvider:

public class MLAuthenticationStateProvider3Tier : AuthenticationStateProvider, IMLAuthenticationStateProvider {

…}

Die Razor Class Library MLBlazorRCL realisiert alle wesentlichen Razor Components für das MiracleList-Fallbeispiel sodass diese Komponenten in allen Blazor-Varianten einsetzbar (Blazor Server, Blazor WebAssembly. Blazor Desktop und Blazor MAUI) sind. Bild 6 zeigt, wie der Hauptbildschirm von MiracleList in verschiedene Komponenten aufgeteilt ist.

Bild 6. Aufbau das Hauptbildschirms am Beispiel der hybriden Apps. Bei den Browser-Varianten mit Blazor WebAssembly und Blazor Server entfällt die untere Leiste.

(Bild: Dr. Holger Schwichtenberg)

Gemeinsame Blazor-Komponenten für alle Ansichten:

Gemeinsame Blazor-Komponenten für die Anmeldeansicht:

Gemeinsame Blazor-Komponenten für die Hauptansicht:

Außerdem beinhaltet die [code]MLBlazorRCL[/CODE] gemeinsame Grafiken und Styles (.css), wie sie in Bild 7 zu sehen sind:

Abbildung 7: Aufbau mit MiracleList-Projektmappe mit aufgeklapptem Inhalt des gemeinsamen Projekts MLBlazorRCL.csproj, das in allen vier Blazor-Kopfprojekten referenziert wird

(Bild: Dr. Holger Schwichtenberg)

Der Beitrag "Eine Blazor-App für alle Plattformen [9]“ hat gezeigt, wie Entwickler und Entwicklerinnen grundsätzlich gemeinsame Razor Components und statische Webartefakte in mehreren Blazor-Anwendungen nutzen können, auch wenn diese auf verschiedenen Blazor-Arten basieren. In diesem Artikel ging es dann um die Abstraktion von Daten- und Ressourcenzugriffen von der Schichtenarchitektur. Mit dem hier gezeigten Weg ist es – mit viel Codegenerierung – auf effiziente Weise möglich, Razor Components zu schreiben, die wahlweise Daten sowohl direkt von einem Datenbankmanagementsystem (2-Tier-Architektur) als auch via Webservice (3-Tier-Architektur).

Dr. Holger Schwichtenberg
ist Chief Technology Expert bei MAXIMAGO, die Innovations- und Experience-getriebener Softwareentwicklung, unter anderem in hochkritischen sicherheitstechnischen Bereichen, anbietet. Zudem ist er Leiter des Expertennetzwerks www.IT-Visions.de, das mit 43 Experten zahlreiche mittlere und große Unternehmen durch Beratung und Schulung bei der Entwicklung sowie dem Betrieb von Software unterstützt.

(fms [10])


URL dieses Artikels:
https://www.heise.de/-9297727

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Web-Frontend-Framework-Eine-Blazor-App-fuer-alle-Plattformen-Teil-1-8987210.html
[2] https://www.heise.de/hintergrund/Web-Frontend-Framework-Eine-Blazor-App-fuer-alle-Plattformen-Teil-1-8987210.html
[3] https://www.it-visions.de/BlazorBuch
[4] https://www.it-visions.de/VueBuch
[5] http://miraclelistbackend.azurewebsites.net/
[6] https://github.com/RSuter/NSwag/wiki/NSwagStudio
[7] https://github.com/HSchwichtenberg/MiracleListNET
[8] https://github.com/HSchwichtenberg/MiracleListNET
[9] https://www.heise.de/hintergrund/Web-Frontend-Framework-Eine-Blazor-App-fuer-alle-Plattformen-Teil-1-8987210.html
[10] mailto:fms@heise.de