Playground: gRPC-Web for .NET
Tags: grpc, dotnet, csharp, javascript
Published: 2020-06-29 12:59:25

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](https://docs.microsoft.com/pt-br/aspnet/core/gRPC/?view=aspnetcore-3.1).

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](https://code.visualstudio.com/download));
- [Protoc](https://github.com/protocolbuffers/protobuf/releases/tag/v3.12.3): uma aplicação CLI para gerar o proxy JS e os modelos de mensagem definidos em seu arquivo Protobuf;
- [Protoc-gen-gRPC-web](https://github.com/gRPC/gRPC-web/releases): um plugin para o `protoc` que define as configurações de exportação do JS gerado;
- [Webpack (npm)](https://www.npmjs.com/get-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:

```powershell
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 arquivo`Lottery.proto` com o seguinte conteúdo:

```protobuf
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:

```xml
<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:

```powershell
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 `.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:

```c#
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](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) (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:

```c#
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:

```html
@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`.

```powershell
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. 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:

```javascript
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:

```json
{
  "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:

```powershell
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:

```c#
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 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:

![](https://dev-to-uploads.s3.amazonaws.com/i/8ojjkbb96x4aj809rfr1.png)

É! 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](https://grpc-web-lottery.azurewebsites.net/).

Para acessar o código-fonte completo, [clique aqui](https://github.com/wsantosdev/playground-grpc-web)!

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](https://devblogs.microsoft.com/aspnet/grpc-web-for-net-now-available/)
>
> [gRPC-Web Hello World Guide](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld#write-client-code)