DEV Community

Cover image for Playground: gRPC-Web for .NET
William Santos
William Santos

Posted on • Edited on

Playground: gRPC-Web for .NET

Olá!

Este é um post da sessão Playground, uma iniciativa para demonstrar, com pequenos tutoriais, tecnologias e ferramentas que entendo ter potencial para trazer ganhos aos seus projetos.

Apresentando o gRPC Web for .NET

Neste artigo quero fazer uma pequena apresentação sobre como funciona a biblioteca gRPC-Web for .NET, lançada pela Microsoft para suportar o padrão gRPC-Web em aplicações .NET Core e, com ela, superar algumas limitações encontradas no uso do gRPC.

Importante! Este artigo presume que você já tenha algum conhecimento sobre o padrão gRPC e sua implementação para .NET.
Caso não tenha, não se preocupe! O artigo cobre algumas noções básicas, e é possível ter uma introdução um pouco mais detalhada com este artigo da Microsoft: Introdução ao gRPC.

Como dito acima, há certas limitações no uso do gRPC. As que considero principais são:
1) Não poder hospedar um serviço no IIS ou no Azure App Service;
2) Não poder chamar métodos gRPC via navegador.

A primeira limitação nos obriga a criar serviços auto-hospedados, como Windows Services ou Linux Daemons por exemplo, e nos impede de usar uma implementação de servidor web tão familiar a nós desenvolvedores .NET, bem como um serviço de hospedagem que muitos já utilizamos para nossas aplicações, devido a certas features do protocolo HTTP/2 que não são suportadas por ambos.

A segunda é um tanto pior porque interfere na arquitetura dos nossos serviços. Isso porque serviços concebidos para falar Protobuf via gRPC dentro da rede vão precisar fornecer seus dados para o cliente via Web API, que vai serializá-los em formato JSON.
Essa necessidade adiciona complexidade (na forma de uma nova camada de aplicação), um ponto de falha (na forma da Web API), e um desempenho inferior na entrega dos dados, já que JSON é um formato de serialização em texto (e verboso!) enquanto Protobuf é um formato de serialização binário.

Entendendo essas limitações do gRPC como justificativas para o uso do gRPC Web, vamos ver como fazê-lo!

Você vai precisar de:

  • Um editor ou IDE (ex.: VSCode);
  • Protoc: uma aplicação CLI para gerar o proxy JS e os modelos de mensagem definidos em seu arquivo Protobuf;
  • Protoc-gen-gRPC-web: um plugin para o protoc que define as configurações de exportação do JS gerado;
  • Webpack (npm): para criar o JS final para distribuição, com todas as dependências necessárias ao gRPC-Web.

Começando a aplicação

A aplicação de exemplo será bem simples, e simulará um jogo de loteria com 6 números, selecionáveis de um intervalo de 1 a 30.

O primeiro passo para a criação de nossa aplicação é sua infraestrutura. Por praticidade, vamos criar a aplicação como uma Web API padrão do .NET Core, remover a pasta Controllers e o arquivo WeatherForecast.cs da raiz do projeto:

dotnet new webapi -o Grpc.Web.Lottery 
Enter fullscreen mode Exit fullscreen mode

Em seguida, precisamos definir os contratos do serviço gRPC via arquivo .proto. Para isso, vamos criar, na raiz do projeto, a pasta Protos, e incluir o arquivoLottery.proto com o seguinte conteúdo:

syntax="proto3"; option csharp_namespace="gRPC.Web.Lottery.Rpc"; package Lottery; service LotteryService { rpc Play(PlayRequest) returns (PlayReply); } message PlayRequest { repeated int32 Numbers=1; } message PlayReply { string Message=1; } 
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, a definição dos contratos é exatamente a mesma que atende ao gRPC. Não há qualquer mudança para suportar o gRPC-Web!

Com os contratos definidos, é hora de tornar possível a geração do proxy C# do serviço gRPC e suas mensagens a partir do Protobuf. Para isso são necessários dois pacotes, e a indicação do arquivo .proto que será usado como fonte:

<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Grpc.AspnetCore" Version="2.29.0" /> <PackageReference Include="Grpc.AspnetCore.Web" Version="2.29.0" /> </ItemGroup> <ItemGroup> <Protobuf Include="Protos/Lottery.proto" GrpcServices="Server" /> </ItemGroup> </Project> 
Enter fullscreen mode Exit fullscreen mode

O pacote Grpc.AspnetCore é responsável pela geração do código C# com os contratos definidos no arquivo .proto e oferecer suporte ao gRPC. Já o pacote Grpc.AspnetCore.Web oferece o suporte ao padrão gRPC-Web. Após a instalação dos pacotes, vamos gerar o código C#. Para isso, basta invocar um build via CLI:

dotnet build 
Enter fullscreen mode Exit fullscreen mode

Importante! O código gerado pela lib Grpc.AspnetCore não é incluído diretamente no projeto. Em vez disso, é gerado na pasta obj, podendo ser importado normalmente pelo namespace definido no arquivo .proto na instrução option csharp_namesapce, neste caso Grpc.Web.Lottery.Rpc.

A lógica e o serviço

Uma vez criada a infraestrutura do projeto, e o código C# com o proxy gRPC e suas mensagens, vamos criar a lógica para a nossa aplicação. Primeiro vamos criar uma pasta chamada Models na raiz do projeto e, em seguida, o arquivo LotteryDrawer.cs com o seguinte conteúdo:

using System; using System.Collections.Generic; using System.Linq; namespace Grpc.Web.Lottery.Models { public class LotteryDrawer { private const int LotteryRange = 30; private const int NumbersToDraw = 6; private static readonly Random _random = new Random(); public static IEnumerable<int> Draw() { int[] numbers = Enumerable.Range(1, LotteryRange).ToArray(); for(int oldIndex = 0; oldIndex < LotteryRange -2; oldIndex++) { int newIndex = _random.Next(oldIndex, LotteryRange); (numbers[oldIndex], numbers[newIndex]) = (numbers[newIndex], numbers[oldIndex]); } return numbers.Take(NumbersToDraw); } } } 
Enter fullscreen mode Exit fullscreen mode

O código acima gera uma sequência com 30 números, os embaralha com um algoritmo chamado Embaralhamento de Fisher-Yates (texto em inglês) e retorna os 6 primeiros, que serão comparados adiante com os números informados pelo jogador via cliente JS.

Agora que temos a lógica para escolher os números, vamos à implementação do serviço gRPC propriamente dito. Para isso, criaremos a pasta Rpc na raiz do projeto, e adicionaremos o arquivo LotteryServiceHandler.cs com o seguinte conteúdo:

using System; using System.Linq; using System.Threading.Tasks; using Grpc.Web.Lottery.Models; namespace Grpc.Web.Lottery.Rpc { public class LotteryServiceHandler : LotteryService.LotteryServiceBase { override public Task<PlayReply> Play (PlayRequest request, Core.ServerCallContext context) { var result = LotteryDrawer.Draw(); bool won = result.OrderBy(i => i) .SequenceEqual(request.Numbers .AsEnumerable() .OrderBy(i => i)); return Task.FromResult(new PlayReply { Message = $"Números sorteados: {string.Join('-', result)}. Você {(won ? "ganhou" : "perdeu")}!" }); } } } 
Enter fullscreen mode Exit fullscreen mode

Acima nós temos o código que vai manipular as requisições gRPC-Web. Note que a classe LotteryServiceHandler herda de LotteryService.LotteryServiceBase, o proxy que foi gerado no build feito a partir do arquivo .proto. Além disso, o método Play recebe como argumento o tipo PlayRequest e retorna o tipo PlayReply, ambos declarados como mensagens no mesmo arquivo.

O que o serviço faz é bastante simples: sorteia 6 números de um intervalo entre 1 e 30 e, após ordená-los, os compara com os números escolhidos pelo jogador, também ordenados. Se a sequência for igual, o jogador ganhou!

O Front-end

Agora vamos nos dedicar à interface de usuário pela qual o jogador escolherá seus números. Por praticidade, vamos usar uma Razor Page e, para criá-la, vamos adicionar a pasta Pages à raiz do projeto e, dentro dela, criar o arquivo Index.cshtml com o seguinte conteúdo:

@page <!DOCTYPE html> <html lang="pt"> <head> <meta charset="utf-8"/> <title>gRpc Web Lotery</title> </head> <body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;"> <div style="margin:0 0 10px 3px"><span>Escolha 6 números de 1 a 30:</span></div> <table> <tbody> <tr> <td><input type="number" name="chosen1" min="1" max="30"></td> <td><input type="number" name="chosen2" min="1" max="30"></td> <td><input type="number" name="chosen3" min="1" max="30"></td> </tr> <tr> <td><input type="number" name="chosen4" min="1" max="30"></td> <td><input type="number" name="chosen5" min="1" max="30"></td> <td><input type="number" name="chosen6" min="1" max="30"></td> </tr> </tbody> </table> <div style="margin: 20px 0 0 3px"><button id="buttonPlay">Jogar!</button></div> <div style="margin: 20px 0 0 3px"><span id="resultSpan"></span></div> <script src="~/js/dist/main.js"></script> </body> 
Enter fullscreen mode Exit fullscreen mode

E, agora, assim como criamos o proxy gRPC e suas mensagens em C# a partir do arquivo .proto, vamos gerar seus equivalentes gRPC-Web em JS. Para hospedá-los, vamos aproveitar o recurso de arquivos estáticos do Asp.Net Core, criando as pastas wwwroot\js na raíz do projeto. Em seguida, na CLI, vamos à pasta Protos e chamar o protoc em conjunto com o plugin protoc-gen-grpc-web.

PS X:\code\Grpc.Web.Lottery\Protos> protoc -I='.' Lottery.proto --js_out=import_style=commonjs:..\wwwroot\js --grpc-web_out=import_style=commonjs,mode=grpcweb:..\wwwroot\js 
Enter fullscreen mode Exit fullscreen mode

O comando acima vai exportar para a pasta wwwroot\js um arquivo JS com os contratos Protobuf a partir do arquivo Lottery.proto e, em seguida, um segundo arquivo JS com o proxy gRPC-Web.

Um detalhe interessante: no trecho mode=grpcweb é definido o modo de serialização das mensagens. O mode=grpcweb utiliza serialização binária no payload das chamadas, enquanto o mode=grpcwebtext utiliza serialização em texto, como uma sequência de bytes codificados como uma string em base 64.

Agora que temos criados nosso cliente e contratos gRPC-Web, vamos implementar a chamada ao servidor. Na pasta wwwroot\js vamos criar o arquivo lottery-client.js com o conteúdo a seguir:

const {PlayRequest, PlayReply} = require('./Lottery_pb.js'); const {LotteryServiceClient} = require('./Lottery_grpc_web_pb.js'); const client = new LotteryServiceClient('https://localhost:5001'); (function() { document.querySelector('#buttonPlay').addEventListener("click", function(event) { var request = new PlayRequest(); var chosenNumbers = []; for(var i = 1; i<= 6; i++) chosenNumbers[i-1] = document.querySelector('input[name="chosen' + i + '"]').value; request.setNumbersList(chosenNumbers); client.play(request, {}, (err, response) => { document.querySelector("#resultSpan").innerHTML = response.getMessage(); }); }); })(); 
Enter fullscreen mode Exit fullscreen mode

Repare que no código acima importamos os arquivos gerados pelo protoc e pelo protoc-gen-grpc-web para termos acesso ao proxy gRPC-Web e às mensagens que serão trocadas com o servidor. Em seguida, quando o documento é carregado, adicionamos um manipulador de evento de clique ao botão definido em nossa Razor Page para enviar os números escolhidos pelo jogador para o servidor.

Agora que temos nossa lógica pronta, precisamos adicionar aos nossos scripts o arquivo de pacote npm com as dependências do nosso cliente JS. Na pasta wwwroot\js vamos adicionar o arquivo package.json com o seguinte conteúdo:

{ "name": "grpc-web-lottery", "version": "0.1.0", "description": "gRPC-Web Lottery", "main": "lottery-client.js", "devDependencies": { "@grpc/grpc-js": "~1.0.5", "@grpc/proto-loader": "~0.5.4", "async": "~1.5.2", "google-protobuf": "~3.12.0", "grpc-web": "~1.1.0", "lodash": "~4.17.0", "webpack": "~4.43.0", "webpack-cli": "~3.3.11" } } 
Enter fullscreen mode Exit fullscreen mode

E, por fim, vamos criar nosso JS final com o webpack:

PS X:\code\Grpc.Web.Lottery\wwwroot\js> npm install PS X:\code\Grpc.Web.Lottery\wwwroot\js> npx webpack lottery-client.js 
Enter fullscreen mode Exit fullscreen mode

Toques finais!

Estamos quase lá! Precisamos agora voltar à infraestrutura do projeto e adicionar algumas configurações. No arquivo Startup.cs na raiz do projeto, vamos adicionar as seguinte instruções aos métodos de configuração:

public void ConfigureServices(IServiceCollection services) { services.AddGrpc(); services.AddRazorPages(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseStaticFiles(); app.UseRouting(); app.UseGrpcWeb(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<LotteryServiceHandler>() .EnableGrpcWeb(); endpoints.MapRazorPages(); }); } 
Enter fullscreen mode Exit fullscreen mode

Importante! A declaração de uso do gRPC-Web, app.UseGrpcWeb(), deve estar entre app.UseRouting() e app.UseEndpoints(...) para ter efeito!

E voi la!

Agora podemos testar nossa aplicação. Estando tudo certo, o resultado será o seguinte:

É! Infelizmente eu perdi! :(

Mas, apesar disso, temos nossa primeira aplicação utilizando gRPC-Web, que poderá ser hospedada em um IIS, Azure App Service, e que dispensa a necessidade de falar JSON com o navegador, aproveitando o formato binário do Protobuf! :)

Para ver um exemplo funcional, segue uma versão hospedada no Azure App Service: gRPC-Web Lottery.

Para acessar o código-fonte completo, clique aqui!

Gostou? Me deixe saber com uma curtida. Tem dúvidas? Mande um comentário que responderei assim que possível.

Até a próxima!

Referências:

gRPC-Web for .NET now available

gRPC-Web Hello World Guide

Top comments (1)

Collapse
 
mariomeyrelles profile image
Mario Meyrelles

Parabéns! Gostei muito do artigo e aproveitei para aprender mais sobre um assunto que ainda não pude testar em campo.

Abraços!
Mário