Blazor-Entwicklung: Komponenten, die immer passen

Seite 3: Abstraktionsschicht fĂĽr 2-Tier/3-Tier

Inhaltsverzeichnis

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 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 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 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.