Hallo zusammen,
ich habe mich mit gRPC beschäftigt und dabei über eine Idee nachgedacht:
Ist ein nahtloser Wechsel zwischen Client-Server und Monolith ohne großem zusätzlichen Aufwand möglich?
Die Idee ist, eine Channel-Implementierung zu entwickeln, die in der Lage ist, Client-Aufrufe im selben Prozess zu verarbeiten, möglicherweise mit einem DI-Scope. Je nach Anwendungs-Konfiguration wird dann der originale Channel oder der InProcess-Channel verwendet.
Die Anwendung wäre weiterhin strikt in zwei Bereiche unterteilt: Client und Server. Der Client kommuniziert mit dem für gRPC generierten Client und je nach Konfiguration läuft die Kommunikation entweder über das Netzwerk, oder im selben Prozess.
Diese Idee richtet sich natürlich an eine sehr spezifische Zielgruppe, da sie nur für Anwendungen sinnvoll ist, die diese Art des Wechsels benötigen. Denkbar wäre z.B. eine Anwendung, die entweder als eigenständige Desktop-Anwendung oder im Team mit einem zentralen Server genutzt werden soll. Bei einer Desktop-Anwendung möchte man natürlich keinen separaten Server starten müssen.
Für Tests könnte es auch nützlich sein, ich halte es aber für besser, die Business-Logik vernünftig zu abstrahieren und dann direkt ohne gRPC zu testen. Außerdem kann ein InProcess-Channel niemals das Verhalten einer echten Netzwerk-Kommunikation vollständig abbilden.
Warum ich nun dieses Thema auf mache:
Was denkt ihr über diese Idee?
Welche Funktionen haltet ihr für wichtig?
Welche Risiken sollten explizit getestet werden?
Besten Dank 😃
NuGet Packages im Code auslesen
lock Alternative für async/await
Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.
Hallo Palladin007,
In-Process gRPC ist nur unnötiger Overhead (auch wenn Sockets od. Named Pipes als Transport verwendet werden).
Da würde ich eher die Abstraktion so ziehen, dass entweder der Kommunikations-Layer (gRPC) verwendet wird od. alles In-Process läuft (Methoden-Aufrufe).
Die via proto
-Files generierten Typen können in beiden Fällen verwendet werden.
Die Methoden-Aufrufe gehen alle gegen ein interface
und je nach Konfiguration ist die konkrete Ausprägung dann ein Typ der In-Process arbeitet od. ein Typ der via gRPC mit dem Server kommuniziert.
Kurz gesagt: wenns rein In-Process ist, so überspringe den gRPC-Kommunikationsteil einfach und ruf die Server-Methoden direkt auf.
mfG Gü
Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.
"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"
Ich glaube, Du hast mich falsch verstanden.
Ich nutze gRPC im Wesentlichen nur als Kommunikations-Layer, es wird nichts serialisiert und es gibt kaum zusätzlichen Overhead.
Die aus den proto-Dateien generierten Typen bieten mir aber alles, was ich brauche, um die Services und Methoden zu finden und genau das ist es, was ich nutze.
Effektiv ist mein Idee (bzw. mein erster Entwurf) also nur ein Quasi-Ersatz für die gRPC-Kommunikation hinter der generierten Abstraktion. Das Protokoll dahinter gibt es also gar nicht mehr, es gibt nur noch die Abstraktion der offiziellen .NET-Implementierung.
Aber klar, wenn man nur im selben Prozess kommunizieren will, ist das ganze überflüssig und unnötig. Aber es geht ja gerade um die Projekte, wo man beides haben möchte, abhängig von der Konfiguration.
NuGet Packages im Code auslesen
lock Alternative für async/await
Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.
Hallo Palladin007,
jetzt weiß ich gar nicht mehr was du willst 😉
es wird nichts serialisiert
gRPC nutzt Protobuf zur Serialisierung. Auch wenn es durch gRPC wegabstrahiert wurde, gibt es dennoch die Serialisierung -- anders könnten die Objekte ja nicht übermittelt werden.
... hinter der generierten Abstraktion. Das Protokoll dahinter gibt es also gar nicht mehr
So sollte eigentlich jede IPC / Kommunikation aussehen. Die tatsächliche Übertragung der Daten sollte nur ein Implementierungsdetail sein, das nicht direkt angesprochen wird.
Z.B. können die vom gRPC-Tooling erstellten Services auch ohne gRPC aufgerufen werden.
nur ein Quasi-Ersatz für die gRPC-Kommunikation
Das meinte ich im vorigen Kommentar. Ist es IPC dann kann der gRPC-Client verwendet werden, ist es In-Process kann der Service direkt* aufgerufen werden -- je nach Konfiguration.
Dazu ist aber keine "Channel-Implementierung" (aus deiner Frage) nötig und die würde das nur komplizierter als nötig machen.
* mit direkt meine ich hier schon via einer Schnittstelle, da sich über diese eben per Strategie-Muster das schön abstrahieren lässt
Aber es geht ja gerade um die Projekte, wo man beides haben möchte, abhängig von der Konfiguration.
Und bei dem Punkt verstehe ich nicht warum das per Strategie nicht einfacher gehen sollte?
Hast du noch andere Anforderungen die mir zum Bild deines Anliegens fehlen?
mfG Gü
Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.
"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"
Z.B. können die vom gRPC-Tooling erstellten Services auch ohne gRPC aufgerufen werden.
Genau das meine ich
Ich versuche es mal mit etwas Code zu verdeutlichen:
// Netzwerk - so wie man gRPC normalerweise benutzt
using var netChannel = GrpcChannel.ForAddress("https://localhost:1234");
await TestAsync(new TestService.TestServiceClient(netChannel));
// InProcess - meine Idee
await using var services = new ServiceCollection()
.AddInProcessGrpcChannel(b => b
.WithService(InProcessServiceInfo.Create<TestServiceImpl>())
.UseScopedServiceProvider())
.BuildServiceProvider();
var inpChannel = services.GetRequiredService<InProcessGrpcChannel>();
await TestAsync(new TestService.TestServiceClient(inpChannel));
// TestAsync()
static async Task TestAsync(TestService.TestServiceClient client)
{
// Der Code hier weiß nicht, ob die Kommunikation über das Netzwerk oder im selben Prozess stattfindet.
}
Der entscheidende Faktor ist die TestAsync-Methode, die für beides identisch ist.
Mein InProcess-Channel (bzw. der Invoker) sucht in seiner Liste von bekannten Services nach dem richtigen Service und der Methode und ruft beides auf. Es findet keine Netzwerk-Kommunikation statt, keine Serialisierung, kein Protokoll, es wird nur die Methode direkt aufgerufen.
Das, was ich von gRPC nutze, ist den vom gRPC-Tooling erstellte Code als Abstraktion, um die Services/Methoden zu suchen, zu instanziieren (ggf. in einem Scope) und aufzurufen.
NuGet Packages im Code auslesen
lock Alternative für async/await
Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.
Hallo Palladin007,
wenn du den TestService.TestServiceClient
durch ein ITestServiceClient
wegabstrahierst und das verwendest, so kann per DI je nach Konfiguration eine Instanz mit Netzwerkkommunikation od. ohne (also direktem Aufruf) durchgeführt werden.
Da ersparst du dir das Erstellen eines Invokers für die In-Process Variante.
mfG Gü
Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.
"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"
Natürlich, man kann es auch "zu Fuß" machen, muss das dann aber auch immer bei jeder Änderung tun.
Und dadurch gäbe es auch eine ganze Menge Overhead:
Und ich kann für die InProcess-Implementierung nicht einfach nur das Interface im Server-Service implementieren, da die Methoden-Signaturen völlig anders sind.
Man könnte natürlich eine weitere Abstraktion z.B. mit dem Mediator-Pattern dazwischen schieben, da würde das etwas einfacher werden, aber es gibt immer noch mehr Aufwand, der für jede Änderung anfällt.
Mit meiner Idee wäre das alles nicht nötig, man hat genau den Aufwand, den man bei gRPC sowieso immer hat und auch braucht, aber darüber hinaus nicht mehr.
Oder übersehe ich etwas?
NuGet Packages im Code auslesen
lock Alternative für async/await
Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.
Hallo Palladin007,
das gRPC-Tooling erstellt eine partielle Klasse, da kann das interface
einfach angehängt werden nachdem VS die Definition extrahiert hat.
Die InProcess-Implementierung ist trivial zu coden.
Wenn ich davon ausgehe dass Änderungen an der proto
-Datei selten sind, ist der Aufwand dafür viel geringer als eine robuste InProcess-Channel-Implementierung zu erstellen.
z.B. DI-Scope muss verwaltet werden
?
Wenn so in etwas für DI so registiert wird
services.AddScoped<ITestClient>(sp =>
{
if (inProcess /* via Konfiguration, etc. ermittelt */)
{
return new InProcessTestClient();
}
return // IPC
});
Nach meiner Erfahrung wird eben eine proto eher selten geändert, insofern hast du viel Aufwand für diese Implementierung bei wenig Nutzen.
Interface und InProcess Code könnte auch per Source-Generator erstellt werden, aber da gilt das Gleiche: viel Aufwand für ...
Daher würd eich diese einfach händisch coden -- aus Kosten/Nutzen-Sicht.
Ein weiterer Vorteil vom IClient
-Interface ist, dass es bei Tests einfacher ist dieses zu Mocken. Die gRPC-Calls können zwar auch gemockt werden (z.B. der Unary-Call), aber das ist wiederum mehr Aufwand.
mfG Gü
Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.
"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"
Die InProcess-Implementierung ist trivial zu coden.
Nur wenn es ein Unary-Call ist.
Bei Streaming-Calls unterscheidet sich der Client-Aufruf mit der Server-Implementierung sehr.
// Server
public override async Task DuplexStreamingCall(IAsyncStreamReader<Request> requestStream, IServerStreamWriter<Response> responseStream, ServerCallContext context)
{
while (await requestStream.MoveNext())
{
_logger.LogInformation("DuplexStreamingCall: {RequestData}", requestStream.Current.RequestData);
var response = new Response
{
ResponseData = $"Duplex response to {requestStream.Current.RequestData}"
};
await responseStream.WriteAsync(response);
await Task.Delay(100);
}
}
// Client
public static async Task DuplexStreamingCall(TestService.TestServiceClient client)
{
using (var call = client.DuplexStreamingCall())
{
var responseTask = Task.Run(async () =>
{
await foreach (var response in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Duplex Streaming Response: {response.ResponseData}");
}
});
for (int i = 0; i < 5; i++)
{
await call.RequestStream.WriteAsync(new Request { RequestData = $"Message {i}" });
await Task.Delay(100);
}
await call.RequestStream.CompleteAsync();
await responseTask;
}
}
Insofern wäre die mMn. einzige wirklich praktikable Option um sowas zu erreichen, das Mediator-Pattern. Die Services nutzen dann den Mediator und geben die Daten durch und wenn die Arbeit im selben Prozess stattfinden soll, wird der Mediator (hinter einer Abstraktion natürlich) aus dem Client heraus genutzt und kann sich dabei auch um den DI-Scope kümmern.
DI-Scope muss verwaltet werden
Damit meine ich, dass bei einer Client-Server-Architektur jeder Request/Call auf dem Server in einem DI-Scope resultiert.
Wenn ich nun auf eine reine Client-Architektur umstellen will und dafür eine einfache Implementierung schaffe, fällt das weg, das muss ich dann auch erst umsetzen. Mache ich das nicht, dann habe ich unterschiedliches Verhalten zwischen den zwei Varianten mit potentiell weitreichenden Auswirkungen.
Nach meiner Erfahrung wird eben eine proto eher selten geändert
Mit proto habe ich keine Erfahrungen, aber wenn ich meine Erfahrungen mit REST darauf übertrage, sind meine Erfahrungen sehr anders.
Tatsächlich muss bei meiner derzeitigen Arbeit eigentlich ständig irgendwas an der API geändert werden.
Ggf. arbeiten wir in anderen Gebieten?
Wenn die API für die Verwendung nach außen für externe Systeme gedacht ist, kann ich mir sehr gut vorstellen, dass sie sich nur selten ändert.
Wenn die API aber für die Kommunikation zwischen zwei zusammen (im selben Team) entwickelten Projekten (Client-Server) gedacht ist, kann das wieder anders aussehen.
Bei uns dient die REST-API hauptsächlich zur Kommunikation zwischen dem Web-Frontend und dem ASP.NET-Backend sowie zu anderen intern entwickelten System und bei der derzeitigen Auftragslage ist da ziemlich viel Bewegung.
Tatsächlich arbeiten wir aktuell mit einem Source-Generator, der Client-Code auf Basis der Controller-Definitionen generiert, allerdings ist der ziemlich komplex und bringt auch ein paar Nachteile mit sich.
Daher würd eich diese einfach händisch coden -- aus Kosten/Nutzen-Sicht.
Ja das stimmt natürlich, aus Sicht einer Firma wäre das für ein Projekt definitiv besser. Für viele Projekte könnte das aber wieder anders aussehen.
Und ich schreibe hier gar nicht für meine Firma, das ganze ist meine private Idee und würde dann auch auf NuGet landen, sodass man in den Projekten gar keinen Aufwand mehr hat - im Idealfall natürlich.
NuGet Packages im Code auslesen
lock Alternative für async/await
Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.