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
protocque 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 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; } 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> 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 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.protona instruçãooption csharp_namesapce, neste casoGrpc.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); } } } 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")}!" }); } } } 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> 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 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. Omode=grpcwebutiliza serialização binária no payload das chamadas, enquanto omode=grpcwebtextutiliza 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(); }); }); })(); 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" } } 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 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(); }); } Importante! A declaração de uso do gRPC-Web,
app.UseGrpcWeb(), deve estar entreapp.UseRouting()eapp.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:

Top comments (1)
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