Blazor WebAssembly Northwind gRPC Service Testflug

Blazor ist schön. Überraschend einfach ist Blazor als Server Side Variante. Direkter Zugriff auf den SQL Server lässt die Formulare und Listen gerade zu explodieren. Nicht nur das man den Code aus Entity Framework oder meinetwegen auch SQLCommand nach core übernehmen kann, das Binding ist noch einfacher als bei WPF.

Nun ist Blazor 3.2 auch mit der Client/ Browser Seite halbwegs benutzbar. Genannt Blazor Webassembly. Benutzbar weil debugging so lala funktioniert. Allerdings dann nicht mehr in den Prozessgrenzen eines Webservers, sondern strikt getrennt im Browser. Um diese Brücke zu überspringen braucht es einen Service. Reflexartig greift man zum REST basierten Web API VIsual Studio Template. Weil mein Herz rein ist und dem mutigen die Welt gehört wird es aber das neue gRPC.

Ehrlicherweise habe ich zuerst durchaus einige Stunden mit dem Ansatz gRPC + Blazor Webassembly versucht den Prototypen zu bauen.

image

Ohne Erfolg und mit vielen neuen mir unbekannten Fehlermeldungen. Ich erinnere mich an eine alte Geschichte mit einer russischen Rakete bei der sowohl Triebwerk als auch Gerät eine vollkommene Neukonstruktion waren und auf der Rampe explodierte. So wie mein Blazor Projekt. Im Gegensatz zu Plessezk gab es hier keine Toten. Die Lehre daraus, ändere nie mehrere Komponenten gleichzeitig. Also Antrieb oder Rakete. Also zurück zu Blazor-Server-App

Visual Studio Solution einrichten

Da Bildchen bekanntermaßen mehr als Tausend Worte sagen, Visual Studio starten und …

image

Hinzufügen neues Projekt zur Projektmappe

image

gRPC Service erstellen

Wesentlicher Nachteil eines HTTP Rest Services, der Overhead an allen Ecken und Enden. Also hat Google 2015! einen RPC basierten Ansatz mit deutlich höherer Performance entwickelt. Ziel damals die gerade aufkommende Microservice Architektur. Anhand einer Schnittstellenbeschreibung (IDL) werden von geeigneten Tools Proxy (Stub) Klassen gebaut. Wie in den guten alten WCF Zeiten.  Die Beschreibungssprache genannt Protobuf und die Dateien enden auf .proto

Im Unterverzeichnis Protos desGrpcService1 Projekt hat Microsoft netterweise eine Greeter Service angelegt. Für den Einstieg mehr Verwirrung als Erhellung.

Aus diesem Grund bauen wir einen Northwind Customers Service von Anfang an. Die Text Datei Northwind.proto. Im Kopf  Version, der C# generierte Namespace. Package dient wohl dem Protokol selber und muss eindeutig sein, wenn man mehrere Services betreiben will

   1:  syntax = "proto3";
   2:  option csharp_namespace = "GrpcService1";
   3:  package northwind; //??

Ich vermisse an dieser Stelle in Visual Studio helfende Werkzeuge, eine Intellisense oder noch besser einen Generator. Die Protbuf Syntax ist echt schräg.

Nun muss man sich um die verwendeten Datentypen kümmern, Es gibt nichts, fast nichts, nicht mal null oder Nothing. Eigentlich gibt es nicht mal Datentypen sondern Messages die zwischen Client und Server oder anders rum ausgetauscht werden sollen, Die Nummer hinten dran ist die Reihenfolge (hatte ich schon das schräg erwähnt?). Selbst für den Leer Case und für die Suche nach einem bestimmten Kunden mit CustomerID. Die Liste der Customer mit dem Keyword Repeated.

   1:   
   2:  message Customer {
   3:      string CustomerId = 1 ;
   4:      string CompanyName = 2 ;
   5:      string ContactName = 3;
   6:      string ContactTitle = 4;
   7:      string Address = 5;
   8:      string City = 6 ;
   9:      string Region = 7 ;
  10:      string PostalCode = 8;
  11:      string Country = 9 ;
  12:      string Phone = 10 ;
  13:      string Fax = 11 ;
  14:    
  15:  }
  16:  message CustomerFilter {
  17:      string id = 1 ;
  18:      }
  19:   
  20:  message Customers{
  21:     repeated Customer items = 1;
  22:  }
  23:  message Empty {
  24:   
  25:  }

Was jetzt schon auffällt die Duplizierung des Objektes und damit verbundenes Interop Marshalling. Das .NET Customer Objekt wird nicht direkt zu einem gRPC Objekt.

Fehlt nur noch die eigentliche Definition der Prozeduren um die CRUD Funktionalität zu definieren. Eine Funktion ohne Parameter gibt es nicht, deswegen Empty als Typ.

   1:  service NorthwindDB {
   2:    rpc SelectAll (Empty) returns (Customers);
   3:    rpc SelectID (CustomerFilter) returns (Customer);
   4:    rpc Insert (Customer) returns (Empty);
   5:    rpc Update (Customer) returns (Empty);
   6:    rpc Delete (Customer) returns (Empty);
   7:  }

Find ich das gut- Nein, erinnert an längst vergangene Zeiten. Aber es ist wie es ist.

Damit die Stub Klassen überhaupt vom Compiler erzeugt werden, muss die Projekt Datei des gRPCService1 Projektes editiert werden.  Dort findet sich bereits ein Eintrag. Diesen greet.prot kopieren und auf northwind.proto verweisen, in der gleiche Itemgroup als Unterelement.

   1:  <ItemGroup>
   2:      <Protobuf Include="Protos\greet.proto" 
GrpcServices="Server" />
   3:  </ItemGroup>
   4:   
   5:  <ItemGroup>
   6:      <PackageReference Include="Grpc.AspNetCore" Version="2.24.0" />
   7:  </ItemGroup>

 

Diesen Eintrag benötigt man später auch im Client, dann mit =”Client” um  auch dort die passenden Klassen zu erzeugen.

gRPC Service implementieren

Alles beginnt wieder mit einer Klasse. Analog der Vorlage des Greeter Service im Verzeichnis Services in der Notation Postfix Service um später in den Klassen noch den Überblick zu behalten. Diese erbt von dem generierten Stub NorthwindDB aus dem proto File.

   1:  namespace GrpcService1.Services
   2:  {
   3:      public class NorthwindDBService : NorthwindDB.NorthwindDBBase

Die Eigenschaft NorthwindDBBase kommt per Konvention. Das muss Visual Studio fehlerfrei akzeptieren. Der Compiler muss das akzeptieren.

An dieser Stelle muss zuerst ein wenig Fleißarbeit für die Datenbank gemacht werden, Ich  gehe davon aus, das einen Northwind Datenbank im SQL Server angelegt ist.

Installieren sie das Nuget Paket Microsoft.EntityFrameworkCore und empfohlen Microsoft.Extensions.Logging. Letzteres um das Trouble Shooting zu erleichtern indem man Meldungen in das Kestrel “Command” Window schreibt.

Über Scaffold-DbContext kann man sich das Model generieren. Das Paket Microsoft.EntityFrameworkCore.Tools wird dafür benötigt. In meinem Fall wird so eine Klasse NORTHWNDContext : DbContext angelegt, samt dem Model Klassen wie Customers. Dies spiegelt die Tabellenstruktur wieder, wie man von einem OR Mapper erwartet.

Den Connection String wird in der appsettings.json abgelegt.

   1:  {
   2:    "ConnectionStrings": {
   3:      "Northwind": "Data Source=(local);
Initial Catalog=Northwind;Integrated Security=True"
   4:    },

 

In ASP.NET core ist es üblich per DI das Connection Object zu hosten. Dazu die Startup.cs des gRPCService Projektes den dbContext hinzufügen.

   1:  public void ConfigureServices(IServiceCollection services)
   2:  {
   3:      services.AddGrpc();
   4:      services.AddDbContext<NORTHWNDContext>(options =>
   5:  options.UseSqlServer(
Configuration.GetConnectionString("Northwind")));

 

Der folgende Schritt wird aktuell noch nicht benötigt. Zusätzlich wird es aber in der Praxis notwendig sein, CORS zu aktivieren, Gleiche Datei

   1:  services.AddCors(o => o.AddPolicy("AllowAll", builder =>
   2:  {
   3:    builder.AllowAnyOrigin()
   4:     .AllowAnyMethod()
   5:     .AllowAnyHeader()
   6:     .WithExposedHeaders("Grpc-Status", "Grpc-Message");
   7:              }));

und in der Configure Methode

   1:  public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
   2:  ...
   3:   
   4:     app.UseCors();

Zurück in der Service Methode NorthwindDBService lassen sich die Überschreibungen generieren. Schraubenzieher links im Code Editor klicken.

image

Dazu kommt noch der Klassen Konstruktor, über den Logger und vor allem die Referenz auf den DBContext erzeugt wird. In der generierten Überladung von SelectAll wird aus dem EF Objekten eine Liste von Proto Objekten. Da kein Null in Protokoll Buffers Definition zulässig ist, wird ab Zeile 36 die Konvertierung in einen Leerstring eingebaut. Ein wenig fahrlässig, aber passenden zu den ALFKI Daten.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Threading.Tasks;
   5:  using Grpc.Core;
   6:  using GrpcService1.Models;
   7:  using Microsoft.Extensions.Logging;
   8:   
   9:  namespace GrpcService1.Services
  10:  {
  11:  public class NorthwindDBService :
NorthwindDB.NorthwindDBBase
  12:  {
  13:   private readonly ILogger<NorthwindDBService> _logger;
  14:      
  15:   private NORTHWNDContext db;
  16:          public NorthwindDBService(
ILogger<NorthwindDBService> logger, NORTHWNDContext db)
  17:          {
  18:              _logger = logger;
  19:              this.db = db;
  20:              _logger.LogWarning("Grpc Konstruktor");
  21:          }
  22:          
  23:          public override Task<Customers>
SelectAll(Empty empty, ServerCallContext context)
  24:          {
  25:              
  26:              Customers protoCustomer = new Customers();
  27:              foreach (var item in db.Customers)
  28:              {
  29:                  protoCustomer.Items.Add(new Customer()
  30:                  {
  31:                      CustomerId = item.CustomerId,
  32:                      Address = item.Address,
  33:                      City = item.City,
  34:                      CompanyName = 
item.CompanyName,
  35:                      ContactName = 
item.ContactName,
  36:                      ContactTitle = 
item.ContactTitle?.ToString() ?? "",
  37:                      Country = 
item.Country?.ToString() ?? "",
  38:                      Fax = 
item.Fax?.ToString() ?? "",
  39:                      Phone = i
tem.Phone?.ToString() ?? "",
  40:                      PostalCode = 
item.PostalCode?.ToString() ?? "",
  41:                      Region = 
item.Region?.ToString() ?? "" //Kein NUll in Proto
  42:                  });
  43:             }
  44:              return Task.FromResult(protoCustomer);
  45:          }
  46:      }
  47:  }

 

Hab ich nun alles. Glaube nichts vergessen

der Blazor-Server-App Client

Ein Server Client. Naja es sind besondere Zeiten, warum auch nicht. Hier hilft uns Visual Studio mit dem altbewährten Add Service Reference Dialog. Im Blazor App Projekt rechtsclick – auswählen und Dienstverweis hinzufügen.

image

Achtung unbedingt Client auswählen, weil sonst die falschen Stub Klassen generiert werden.

image

Zusätzlich wird auch das fehlende Nuget Paket Grpc.AspNetCore installiert. Das nur als Hinweis wenn man es von Hand machen muss.

Keine Angst: wer falsch geklickt hat, kann in der Blazor Project Datei den Eintrag ItemGroup ProtoBuf auch im XML Code ändern auf Client. Der Compiler baut jedes Mal die Klassen neu.

   1:  <ItemGroup>
   2:      <PackageReference Include="Grpc.AspNetCore" Version="2.28.0-pre2" />
   3:    </ItemGroup>
   4:   
   5:    <ItemGroup>
   6:      <Protobuf Include="..\GrpcService1\Protos\northwind.proto" GrpcServices="Client">
   7:        <Link>Protos\northwind.proto</Link>
   8:      </Protobuf>
   9:    </ItemGroup>

Als View wird noch eine Razor Page benötigt, die ich hier mal Northwind.Razor genannt habe.

image

Auf in den C# Code!  Im Razor View iteriert mand durch die Liste Grpc Customers. Wie vorhin angemerkt ein automatisch anhand Proto erstellter Datentyp.  Im @code Block zeigt der Channel auf den Standard Port eines Kestrel Servers. Dann Instanz des Client, Aufruf der Methode und Zuweisung der Rückgabe. Vermutlich wegen der nicht ganz sauberen async Behandlung muss dann noch dem UI per StateHasChanged mitgeteilt werden sich neu zu zeichnen.

   1:  @page "/nw"
   2:  @using GrpcService1;
   3:  @using Grpc.Net.Client;
   4:   
   5:  <h3>Northwind</h3>
   6:  @if (customers != null)
   7:  {
   8:      @foreach (var item in customers.Items)
   9:      {
  10:          <span>@item.CompanyName</span>
  11:   
  12:      }
  13:  }
  14:   
  15:  <button @onclick="lade">lade</button>
  16:   
  17:  @code {
  18:      public Customers customers { get; set; }
  19:      
  20:      public async void lade()
  21:      {
  22:   
  23:          var channel = GrpcChannel.ForAddress("https://localhost:5001");
  24:          var client = new NorthwindDB.NorthwindDBClient(channel); //GrpcServices="Client"
  25:          customers = await client.SelectAllAsync(new Empty());
  26:          StateHasChanged();
  27:      }
  28:   
  29:  }

 

Natürlich müssen dann beide Projekte beim debuggen gestartet werden, einzustellen per Visual Studio Solution Eigenschaften.

image

Blazor Web Assembly-App fail fast

Ich hatte eingangs geschrieben, das die ersten Versuche mit dem neuen Blazor nicht so tolle waren. Debuggen geht zwar, aber manchmal sieht man die Inhalte von Variablen nicht. Manchmal bleibt alles hängen. Ist wirklich sehr früh und nicht stabil. In jedem Fall sind die Browsertools F12 Command Messages noch halbwegs hilfreich.

So schreite ich nun voran und migriere den Code in ein weiteres, diesmal eben Balzor Server App Projekt um den gRPC Northwind Service zu konsumieren, image

Erste Hürde, ist eine CORS Meldung, die ich wie ganz oben beschrieben ausgebügelt habe.  Trotzdem gibt es unzählige Fehlermeldungen.

image

Am Ende klappt es nicht trotz aller Mühe nicht. Ich bin ehrlich erst ein externer Tipp hat mich zurück auf die Spur gebracht.

Es ist nicht möglich, einen HTTP/2-gRPC-Dienst von einer browserbasierten App aus aufzurufen

Wenn man die Frage stellen kann, ist die Lösung nicht weit. Ein experimentelles Nuget Paket Grpc.AspNetCore.Web. In der Doku sind die nötigen Änderungen am gRPC Service beschrieben,

Im Blazor-Webassembly-App Code muss auch C# Code geändert werden.

   1:  var handler = new GrpcWebHandler(
GrpcWebMode.GrpcWebText, new HttpClientHandler());
   2:  var channel = GrpcChannel.ForAddress(
"https://localhost:5001", new GrpcChannelOptions
   3:     {
   4:      HttpClient = new HttpClient(handler)
   5:     });
   6:   
   7:  var client = new NorthwindDB.NorthwindDBClient(channel);
   8:  customers = await client.SelectAllAsync(new Empty());

Nun noch die Start Optionen in der Solution ändern, damit nicht das Blazor Server Projekt gestartet wird.

Webbrowser kommt hoch, Kestrel wird gestartet

image

Normalerweise schreibe ich kein Fazit. Selbst wenn Blazor 3.2 fertig ist, wird vermutlich ein gutes Stück von Tools fehlen. gRPC fühlt sich an wie Assemblercode auf Endlospapier. Da fühlt sich die Server rendert Variante ein deutliches Stück reifer an und erzeugt am meisten Produktivität.

Kommentare sind geschlossen