DEV Community 👩‍💻👨‍💻

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

Posted on • Updated 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

Make Your Github Profile Stand Out

Github is great, but have you considered how to make yours more attractive for potential employers or other visitors? Even non-tech ones like recruiters!

Take a couple of hours and show your best side as a person - and a programmer.