Azure WebApps に ASP.NET Core gRPC をデプロイしてみよう

Azure App Service が gRPC をデプロイ出来ない問題があるので悶々としてたのですが gRPC-Web プロトコルになりますが Azure App Service にデプロイできるみたいです！！ASP.NET Blog で [gRPC-Web for .NET now available](https://devblogs.microsoft.com/aspnet/grpc-web-for-net-now-available/) という記事でアナウンスされていました。ということでやってみましょう。

参考ドキュメントはこちら: [ブラウザー アプリでの gRPC の使用](https://docs.microsoft.com/ja-jp/aspnet/core/grpc/browser?view=aspnetcore-3.1)

## プロジェクトの作成

ASP.NET Core Web アプリケーションで空のプロジェクトから始めてみましょう。
まず、2 つのパッケージを追加します。

- Grpc.AspNetCore
- Grpc.AspNetCore.Web

どちらも執筆時点の最新の 2.30.0 を入れました。
とりあえずシンプルなハローワールド用の `SayHello.proto` をプロジェクトに追加します。

```proto:SayHello.proto
syntax = "proto3";

option csharp_namespace = "Grpc.HelloWorld.Web";

message HelloRequest {
	string name = 1;
}

message HelloReply {
	string message = 1;
}

service Greeter {
	rpc SayHello(HelloRequest) returns (HelloReply) {}
}
```

プロジェクトファイルを開いて、Protobuf タグで上記ファイルを追加しておきます。

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.30.0" />
    <PackageReference Include="Grpc.AspNetCore.Web" Version="2.30.0" />
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="SayHello.proto" GrpcServices="Server" /> <!-- これ-->
  </ItemGroup>
</Project>
```

適当にサービスも実装しておきましょう。

```csharp
using Grpc.Core;
using System.Threading.Tasks;

namespace Grpc.HelloWorld.Web
{
    public class GreeterService : Greeter.GreeterBase
    {
        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = $"Hello {request.Name}! Powered by ASP.NET Core gRPC.",
            });
        }
    }
}
```

では `Startup.cs` でサービスの登録をします。
その際に `Configure` で `UseGrpcWeb` を呼ぶのと `MapGrpcService` で `EnableGrpcWeb` を呼びます。

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Grpc.HelloWorld.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc(); // add
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseGrpcWeb(); // add

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>().EnableGrpcWeb(); // add
            });
        }
    }
}
```

サービスが沢山あって、全部で gRPC-Web を使いたい場合は `UseGrpcWeb` メソッドでデフォルトでオンにすることも出来ます。

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Grpc.HelloWorld.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpc();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); // デフォルトでオンにする

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<GreeterService>();
            });
        }
    }
}
```

## クライアントの実装

ブラウザーから呼ぶのが主目的なんでしょうが、私は gRPC を Azure WebApps にデプロイして .NET のクライアントから呼びたいのでコンソールアプリで作ります。コンソールアプリを作って以下のパッケージを追加します。

- Grpc.Net.Client
- Grpc.Net.ClientFactory
- Google.Protobuf
- Grpc.Net.Client.Web
- Grpc.Tools

そしてコンソールアプリのプロジェクトで接続済みサービスの追加で、サーバー側のプロジェクトに追加した SayHello.proto を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/1bde98df-41c0-c531-2c6e-d434004f19f1.png)

クライアント側はシンプルに呼び出すだけにしました。

```csharp
using Grpc.Core;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Grpc.HelloWorld.Web.Client
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var channel = GrpcChannel.ForAddress("https://localhost:44338", new GrpcChannelOptions
            {
                HttpHandler = new GrpcWebHandler(new HttpClientHandler()),
            });

            var client = new Greeter.GreeterClient(channel);
            var reply = await client.SayHelloAsync(new HelloRequest { Name = "okazuki" });
            Console.WriteLine(reply.Message);
        }
    }
}
```

サーバー側のプロジェクトを起動してコンソールアプリを起動すると以下のような結果になります。ここまでは OK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/8b5c282e-55f2-711f-6255-c9eb7ed5aec5.png)

## Azure にデプロイ

Azure で適当に WebApps を作ってデプロイします。
私の場合は kaotagrpcweb という名前で WebApps を作ったので以下のようにクライアント側のコードを書き替えました。

```csharp
using Grpc.Core;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace Grpc.HelloWorld.Web.Client
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var channel = GrpcChannel.ForAddress("https://kaotagrpcweb.azurewebsites.net/", new GrpcChannelOptions
            {
                HttpHandler = new GrpcWebHandler(new HttpClientHandler()),
            });

            var client = new Greeter.GreeterClient(channel);
            var reply = await client.SayHelloAsync(new HelloRequest { Name = "okazuki" });
            Console.WriteLine(reply.Message);
        }
    }
}
```

今度はサーバーは Azure にいるのでローカルのサーバーは立ち上げずにコンソールアプリだけを動かします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/a67a3734-a09e-73d0-f8f4-d969f941bd85.png)

あっさり動き過ぎて怖いですが動きましたね！！

## 制限事項

最初にリンクをはったドキュメントにも書いてありますが以下の制限があります。引用します。

> ## gRPC-Web とストリーミング
従来の HTTP/2 による gRPC では、すべての方向でストリーミングがサポートされます。 gRPC-Web では、ストリーミングのサポートが制限されています。

> - gRPC-Web ブラウザー クライアントでは、クライアント ストリーミング メソッドと双方向ストリーミング メソッドの呼び出しはサポートされていません。
> - Azure App Service および IIS でホストされている ASP.NET Core gRPC サービスでは、双方向ストリーミングはサポートされていません。
> 
> gRPC-Web を使用するときは、単項メソッドとサーバー ストリーミング メソッドのみを使用することをお勧めします。

フル機能が使えるわけではないので、そこは注意が必要ですが簡単にタイプセーフな RPC サービスを作れるというメリットは享受できそうです。

## ブラウザーから呼びたい場合の追加設定

ブラウザーから呼び出す場合は追加の構成が必要になります。といっても REST API を作ってる人たちにはおなじみの CORS の設定です。
試しにローカルで ASP.NET Core の Blazor WebAssembly を使って先ほどの Azure にデプロイしたサービスを呼び出してみようと思います。

適当に ASP.NET Core プロジェクトでホストされる Blazor WebAssembly プロジェクトを作って参照の追加と proto ファイルをプロジェクトに追加して Index.razor を以下のようにしました。

```html
@page "/"
@using Grpc.Net.Client
@using Grpc.Net.Client.Web
@using Grpc.HelloWorld.Web

<h1>Hello, world!</h1>

<input type="text" @bind-value="Name" />
<br/>
<button @onclick="InvokeButton_Click">Invoke</button>
<br/>
<span>@Message</span>

@code {
    private string Name { get; set; }
    private string Message { get; set; }

    private async void InvokeButton_Click(MouseEventArgs e)
    {
        var channel = GrpcChannel.ForAddress("https://kaotagrpcweb.azurewebsites.net/", new GrpcChannelOptions
        {
            HttpHandler = new GrpcWebHandler(new HttpClientHandler()),
        });

        var client = new Greeter.GreeterClient(channel);
        var reply = await client.SayHelloAsync(new HelloRequest { Name = Name });
        Message = reply.Message;
        StateHasChanged();
    }
}
```

このままローカル実行して Invoke ボタンを押しても以下のように真っ赤なエラーになります。わかりやすい CORS のエラーですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/75c83d3a-c24d-5193-23a3-c6f069d587af.png)

CORS の設定が必要なことも最初に紹介したドキュメントページに書いてあります。サービス側のプロジェクトで C# で CORS の設定をしてもいいですが Azure WebApps だとポータルからも出来るでのポータルからやってみましょう。

CORS の設定ページから以下のように入れるだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/c9aca4c8-fe14-abce-419c-c14be0de60f9.png)

保存して再度ローカルから呼び出してみると…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/db8e39ce-d3f0-ee5a-cd2d-c5d25507e81f.png)

動きました。やったね！

## まとめ

gRPC のフル機能が使えるわけではないですが gRPC-Web を使えば ASP.NET Core の gRPC のサービスを Azure の AppService にもデプロイして呼び出すことが出来ました。
これは個人的にはかなり嬉しいです。