Playground: gRPC-Web for .NET

William Santos - Jun 29 '20 - - Dev Community

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .