gRPC Code First

In de laatste twee edities van het magazine heb ik je laten zien hoe je gRPC en gRPC-Web binnen .NET kunt gebruiken op basis van een contract geschreven in protobuf. Dit is alleen niet de enige aanpak. In dit artikel zal ik je laten zien hoe je kunt werken met gRPC met een code first aanpak. De NuGet-packages die je hiervoor nodig hebt, zijn niet afkomstig van Microsoft, maar van het open source project “protobuf-net.Grpc”.

Door: Johan Smarius

Opzetten van het contract

Bij het gebruik van een code first aanpak moet je het contract in code op gaan nemen. Deze code zal moeten worden gedeeld tussen de server en de client. De gemakkelijkste manier om dit te doen, is om deze code op te nemen in een class library. Vanuit de server en de client kun je dan gemakkelijk een referentie opnemen naar deze class library. Als je je servercode in .NET 5 wilt maken, is het wel handig om ook het target framework voor deze class library op .NET 5 te zetten. Helaas kun je deze keuze nog niet maken bij het aanmaken van de library, dus dit moet je na het aanmaken even nog instellen bij de properties van je project (Figuur 1).

Figuur 1 Instellen Target Framework

Voordat je het contract kunt coderen, moet je de NuGet-package “protobuf-net.Grpc” toevoegen aan het nieuw gemaakte project.

Je kunt nu het contract op twee manieren gaan definiëren. Voor contracten kun je gebruik maken van de types DataContract/DataMember (Figuur 2) en ServiceContract (Figuur 3). Je kent deze misschien nog wel van WCF.

Figuur 2 DataContract

Bij het datacontract is het belangrijk om het volgnummer van het veld expliciet op te nemen, net zoals je dat ook moet doen als je gebruik maakt van de normale protobuf-aanpak. In het datacontract kun je gebruik maken van de normale .NET typen.

Figuur 3 ServiceContract

Voor de interface moet je het attribuut “ServiceContract” gebruiken. De naam van de service wordt normaal bepaald aan de hand van de typenaam van de interface. Je kunt dit echter sturen door expliciet de naam op te geven bij het attribuut, zoals in de code van Figuur 3 gedaan is. In principe worden alle methoden binnen deze interface als beschikbare methoden voor het contract beschouwd. Om dit toch explicieter te maken, wordt in de voorbeeldcode altijd gebruik gemaakt van het attribuut “OperationContract”. Als returntype voor een methode kun je gebruik maken van een DataContract-type, void, Task of zoals in de voorbeeldcode gebruikt is ValueTask. Bij de normale protobuf-aanpak wordt standaard een synchrone en asynchrone methode gegenereerd op basis van het protobuf-bestand. Bij de code first aanpak wordt er geen code gegenereerd en moet je zelf expliciet een keuze maken door te kiezen voor een synchroon of asynchroon returntype. De code van een service wil je natuurlijk het liefst zo veel mogelijk asynchroon laten uitvoeren, dus in dit geval is Task of ValueTask het meest van toepassing. Om nu expliciet te maken dat een enkele waarde uit de methode wordt geretourneerd, is in deze voorbeeldcode gebruik gemaakt van ValueTask.

Voor simpele contracten waarbij je geen low-level invloed nodig hebt op het contract, voldoet deze aanpak. Als je meer controle nodig hebt, dan kun je in plaats van DataContract gebruik maken van de types ProtoContract en ProtoMember (Figuur 4). Beide contracten lijken heel erg op elkaar. Er wordt alleen gebruik gemaakt van andere attributen en de parameter voor het volgnummer hoef je niet meer expliciet te benoemen.

Figuur 4 Protocontract

Voor dit eenvoudige voorbeeld is het gebruik van ProtoContract niet direct nodig, maar als je bijvoorbeeld gebruik wilt maken van DateTime typen in je contract, dan heb je deze constructie wel nodig (Figuur 5).

Figuur 5 Protocontract met sturing datatype

Met de code first aanpak hoef je geen gebruik meer te maken van het speciale type “google.protobuf.Timestamp”, maar moet je wel specificeren dat de omzetting door het protobuf-net.Grpc package moet worden geregeld. Hiervoor gebruik je de parameter DataFormat. Als je deze instelt op DataFormat.WellKnown, dan wordt de mapping van het interne type van en naar het .NET DateTime type automatisch voor je geregeld. In dit contract kun je ook zien dat collecties en enumeraties gewoon ondersteund worden. CustomerType is namelijk een normale C# enum (Figuur 6).

Figuur 6 Enumeratie voor contract

Alle code in dit artikel maakt gebruik van de ProtoContract-variant, omdat ik binnen een codebase graag gebruik maak van dezelfde variant.

Door het coderen van je data- en servicecontract ben je klaar met het definiëren van je interface van de service.

Coderen van de server

Voor het coderen van de server kun je in principe als basis werken met een ASP.NET Core Web Application. De voorbeeldcode maakt echter gebruik van de normale gRPC-template binnen Visual Studio. De normale aanpak via protobuf contracten en de code first aanpak kunnen zonder problemen naast elkaar gebruikt worden en om dit te laten zien, heb ik voor de gRPC-template gekozen. Om je geheugen nog even op te frissen, zal ik de stappen die hiervoor nodig zijn nog even herhalen. Bovendien is een scherm in de template een heel klein beetje veranderd sinds het eerste artikel.

Figuur 7 Startscherm Visual Studio 2019

In het startscherm kun je kiezen voor “Create a new project” (Figuur 7).

Figuur 8 Create Project Page

In de templatepagina (Figuur 8) kun je nu zoeken naar gRPC. Voor gRPC is er op dit moment maar een template beschikbaar. Als je deze template kiest en op “Next” drukt, dan kom je terecht op de pagina die voor alle templates gebruikt wordt om de project naam, locatie en solution name in te stellen (Figuur 9).

Figuur 9 Projectconfiguratie

Als je nu op “Create” klikt dan kun je nog wat aanvullende opties instellen (Figuur 10).

Figuur 10 Service configuratie

Het is belangrijk om te controleren dat .NET 5 als framework geselecteerd staat en niet .NET Core 3.1. Als je nu weer op “Create” drukt, dan wordt de code voor je server gegenereerd.

Om de code voor de server te kunnen schrijven, moet je het NuGet-package “protobuf-net.Grpc.AspNetCore” toevoegen (Figuur 11).

Figuur 11 NuGet package voor server code

De definitie van de interface staat in de Class Library die je aangemaakt hebt, dus daar moet je ook een project referentie naar leggen (Figuur 12).

Figuur 12 Project referentie

De implementatiecode voor de server kun je het beste plaatsen in de map “Services”. Je kunt hiervoor gewoon een nieuw bestand maken van het type “Class”.

Figuur 13 Implementatie server

De hele implementatie van de server bestaat uit het implementeren van de interface (Figuur 13). Voor deze service komt dat vooral neer op het aanmaken van het response bericht.

Binnen deze code kun je ook gewoon gebruik maken van dependency injection (Figuur 14).

Figuur 14 Gebruik van dependency injection in server code

De server moet natuurlijk nog wel bekend gemaakt worden aan .NET. Hiervoor moeten wat aanpassingen in de Startup.cs worden gedaan.

Figuur 15 Aanpassingen ConfigureServices

Binnen de ConfigureServices methode moet de ondersteuning voor code first gRPC toegevoegd worden aan de services-collectie. Hiervoor is een aparte methode “AddCodeFirstGrpc” beschikbaar (Figuur 15). De normale gRPC-ondersteuning en de code first variant kunnen gewoon naast elkaar bestaan. In de code zie je dus ook gewoon de normale “AddGrpc” terug die door de standaard Microsoft implementatie gebruikt wordt.

De service zelf moet natuurlijk in de routing opgenomen worden. Net als bij de normale gRPC kun je dit bereiken door het type via de methode MapGrpcService toe te voegen aan de endpoints collectie (Figuur 16).

Figuur 16 Toevoegen aan routing

De code voor de server is hiermee volledig.

Gebruik maken van een service

Om gebruik te kunnen maken van de service moet je de NuGet-package “protobuf-net.Grpc” toevoegen aan je code en heb je natuurlijk ook een project referentie nodig naar de Class Library waarin de interface staat.

Voordat de service aangemaakt kan worden, moet je eerst een channel maken (Figuur 17).

Figuur 17 Aanmaken channel

De client kun je nu aanmaken door expliciet de service aan te maken (Figuur 18).

Figuur 18 Aanmaken client

Via deze client kun je dan weer de methode aanroepen, die je in het servicecontract op hebt genomen (Figuur 19). Bij het opzetten van het contract was al beschreven dat code first niet automatisch synchrone en asynchrone methoden voor je maakt. In deze afbeelding is dat ook duidelijk te zien.

Figuur 19 Beschikbare methoden

Voor het aanroepen moet natuurlijk nog wel een EchoRequest aan worden gemaakt (Figuur 20).

Bij het uitvoeren van deze code moet je er natuurlijk wel weer rekening mee houden dat je 2 uitvoerbare projecten hebt. Voor het makkelijk debuggen moet ik dus beide applicaties starten (Figuur 21).

Figuur 21 Instellen meerdere projecten om te starten

De output van de draaiende applicaties wordt getoond in 2 console windows. In deze consoles kun je zien dat de code inderdaad werkt (Figuur 22). In de logging kun je ook zien dat de naam van de service inderdaad “SDN.EchoService” is zoals opgegeven in de Name-parameter van het ServiceContract (Figuur 3).

Figuur 22 Uitvoer van de service

Uitgebreidere mogelijkheden

De EchoService is natuurlijk nog redelijk beperkt. Voor het eerste artikel uit deze reeks heb ik een uitgebreider voorbeeld gemaakt. Dit voorbeeld heb ik overgezet naar een code first aanpak. Aan de hand van deze code zal ik je nog wat uitgebreidere mogelijkheden laten zien.

In het begin van dit artikel heb ik bij het behandelen van het opzetten van het contract al laten zien dat ook enumeraties en collecties binnen een contract mogelijk zijn. In principe kun je bij het opzetten van het contract complete boomstructuren gebruiken. De enige beperking is dat dit niet mag resulteren in een graaf, omdat er dan cyclische afhankelijkheden ontstaan. Voor WCF-ontwikkelaars is dit geen onbekende beperking.

Bij de code first aanpak kun je gewoon gebruik maken van de .NET collecties die je al gewend bent om te gebruiken. Persoonlijk gebruik ik over het algemeen IEnumerable<T> voor collecties, om de exacte keuze niet in de interface vast te leggen en op die manier wat flexibiliteit te hebben bij de implementatie van de service. Bij het aanmaken van een collectie voor een aanroep, kun je gewoon gebruik maken van object en collection initializers (Figuur 23).

Figuur 23 Gebruik initializers binnen code

Als je binnen je contracten gebruik wilt maken van void of een lege parameterlijst voor een methode, dan wordt dit gewoon ondersteund. Je hebt hiervoor geen speciaal type nodig om dit aan te geven. Voor een methode die geen resultaat terug hoeft te geven, maar die wel asynchroon moet zijn, kun je gebruik maken van Task of ValueTask (Figuur 24).

Figuur 24 Retourneren van void async

Streaming binnen de code first aanpak wordt ook ondersteund door gebruik te maken van het type “IAsyncEnumerable”. Je kunt dit type gebruiken als returntype, maar ook als type voor een methodeparameter (Figuur 25).

Figuur 25 Server side streaming

In de code van de server kun je nu nieuwe returnwaarden aanmaken en via “yield return” teruggeven (Figuur 26).

Figuur 26 Return nieuwe returnwaarden

In principe kan de server eeuwig nieuwe waarden blijven sturen. Om de client een mogelijkheid te geven om aan geven dat de stroom mag stoppen, kun je gebruik maken een CancellationToken. Dit token is opgenomen in de CallContext klasse uit de ProtoBuf.Grpc namespace (Figuur 27).

Figuur 27 Implementatie van de interface
Figuur 28 Cancellationtoken in private implementatiemethode

In de uitwerking van de servicemethode heb ik ervoor gekozen om de code te splitsen in een private methode die alleen weet heeft van een cancellationtoken (Figuur 28) en de methode die nodig is vanuit de interface. Om het cancellationtoken goed te laten werken, moet je bij de parameter het attribuut “EnumeratorCancellation” gebruiken. De compiler geeft hier ook een waarschuwing voor als je dit per ongeluk zou vergeten.

Met deze combinatie van methoden kan de client een context meegeven, maar is dat niet verplicht.

Figuur 29 Client code voor server streaming

De client code kan gebruik maken van de await foreach constructie om de stroom van nieuwe data- elementen op te vangen. Deze code is opgenomen in een console applicatie dus om het annuleren te simuleren, wordt de foreach na 100 iteraties geforceerd afgebroken (Figuur 29). Bij het stoppen van de async stream wordt wel een RpcException gegooid, maar die kun je veilig negeren. Door het gebruik van een filter, kun je dit negeren ook heel specifiek doen.

Op dit moment ondersteunt protobuf-net.Grpc nog niet de integratie met het DI-framework van ASP.NET Core via de GrpcClientFactory. Als je dus gebruik wilt maken van een andere service binnen je code van een service, dan moet je expliciet de client aanmaken (Figuur 30).

Figuur 30 Aanroepen andere service in servicecode

Je kunt deze code natuurlijk wel netter maken door zelf met factory patterns te gaan werken. Het gebrek aan ondersteuning voor de GrpcClientFactory heeft wel tot gevolg dat deze variant niet goed samen kan werken met de gRPC-Web toevoeging aan gRPC.NET. Dit heeft natuurlijk ook gevolgen voor de flexibiliteit die je op dit moment hebt in de keuze van je hosting opties.

Samenvatting

In dit artikel heb ik de belangrijkste aspecten voor het bouwen van een gRPC service op basis van een code first aanpak behandeld. Met deze variant kun je gemakkelijk een contract in je code schrijven. Dit contract kan dan gedeeld worden tussen de server en de client. Het ontwikkelteam hoeft op deze manier veel minder kennis te hebben van protobuf en hoeft bovendien veel minder rekening te houden met een aantal specifieke typen, met een nogal aparte syntax, die bij het gebruik van de standaard protobuf aanpak helaas nodig is. Toch gaat mijn eigen voorkeur uit naar de protobuf aanpak. Vooral door het gebrek aan ondersteuning van GrpcClientFactory en daarmee dus ook gRPC-Web zal ik in de praktijk maar in heel specifieke gevallen voor deze code first aanpak kiezen. Voor ontwikkelteams die komen vanuit WCF kan deze optie wel heel handig en laagdrempelig zijn.

Op mijn github (https://github.com/JohanSmarius) heb ik een aantal gRPC demo projecten staan. De code uit dit artikel staan in de repo’s: “GrpcDemoCodeFirst” (port van de gRPC services uit het eerste artikel) en “GrpcServiceFromCode” (EchoService).

 

Biografie

Johan Smarius is een ervaren (lead) software developer, architect, spreker en trainer op het Microsoft .NET vlak met meer dan 25 jaar ervaring in de IT. Hij heeft ervaring met .NET sinds versie 1.0. Johan werkt als docent informatica bij Avans Hogeschool in Breda en als technical consultant bij zijn eigen bedrijf JMAC Software Solutions. In zijn vrije tijd leert Johan kinderen programmeren en is hij instructeur EHBO. Website: https://www.johansmarius.com