LoginSignup
6

posted at

updated at

.NETでCode-firstなgRPC(gRPC-Web)を使う

本記事は Sansan Advent Calendar 2022 の14日目の記事です。

自分が所属している組織では一部gRPC-WebとASP .NET Coreのインテグレーションを用いて開発しています。
.NETにおいてはCode-first gRPCを用いることで通常の開発からシームレスにgRPCサービスの開発に移行することができるため、それまであまりgRPCに馴染みの無かった自分にとっても慣れるのが非常に容易でした。
ということでこの記事ではCode-first gRPCについて紹介します。Azure App Serviceにて構築する際の考慮点についても少しだけ触れます。

おしながきは以下のとおりです。

  1. .NETでgRPCのClient/Serverを作る
  2. .NETでCode-firstなgRPCのClient/Serverを作る
  3. Azure App ServiceでgRPC(gRPC-Web)のサービスを使う
  4. 補足

この記事で使ったコードはこちらにあります。
なお今回用いるコードは以下のドキュメントをもとに作成しています。

また今回gRPCやgRPC-Webそのものについての説明は割愛します。
「gRPCってなんですか?」という方は以下をご参照ください。

1. .NETでgRPCのClient/Serverを作る

今回は以下のようなprotoで定義されるサービスを作っていきます。
InputNumberに入れた数字に10足して文字列で結果を伝えるという単純なものです。

以下はServer側のものですが、Clientでも同じようなものを作ります。

syntax = "proto3";

option csharp_namespace = "SampleGrpcServer";

package calculator;

service Calculator {
  rpc Add10 (AddRequest) returns (AddResponse);
}

message AddRequest {
  int32 input_number = 1;
}

message AddResponse {
  string message = 1;
}

Server側の設定

先ほどのprotoファイルをもとにしたServiceの実装は以下のとおりです。


using Grpc.Core;

namespace SampleGrpcServer.Services
{
    public class CalculatorService : Calculator.CalculatorBase
    {
        public override Task<AddResponse> Add10(AddRequest request, ServerCallContext context)
        {
            return Task.FromResult(new AddResponse
            {
                Message = $"Result is {request.InputNumber + 10}"
            });
        }
    }
}

Program.csではgRPCを使うための設定をします。

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SampleGrpcServer.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

var app = builder.Build();

app.MapGrpcService<CalculatorService>();

app.Run();

builder.Services.AddGrpc();がgRPCのサービスを使うための設定で、app.MapGrpcService<CalculatorService>();がgRPCサービスへのルーティングをしている部分です。
ここの設定の仕方はかなりシンプルだと感じます。

Client側の設定

gRPCの設定をServer側と同様Program.csにて行います。
メソッドの呼び出し用にクラスとか作ってもよいのですが(実際はそんな感じなのですが)、今回はチュートリアル同様Program.cs内でClientを立てメソッドの呼び出しをします。

using System;
using Grpc.Net.Client;
using SampleGrpcClient;

using var channel = GrpcChannel.ForAddress("https://localhost:7103");

var client = new Calculator.CalculatorClient(channel);
var response = await client.Add10Async(
    new AddRequest {InputNumber = 5});

Console.WriteLine(response.Message);

GrpcChannel.ForAddress("https://localhost:7103");の部分でチャンネルの設定をし、そこで作ったチャンネルをもとにClientを作っています。

これでClient/Serverの両方を起動することで以下のようなメッセージを確認できます。

2. .NETでCode-firstなgRPCのClient/Serverを作る

先の例ではprotoファイルを作成しインターフェースの定義をしました。
これ自体そんなに大変なことではないのですが、Code-first gRPCにおいては通常の開発で使っているクラスでインターフェースの定義ができる(=protoファイルを作成しなくてもよくなる)ので更に楽になります。

先の例ではprotoファイルをClient/Serverの両方においていましたが、Code-first gRPCではこの定義を一箇所においてそれを共有することができます1
ということでこれまでClient/Serverの2つのプロジェクトのみでしたが、これに加え共通のインターフェースを定義するContractsのプロジェクトも用意します。
プロジェクト・ファイルの構成は以下のようになります。

SampleCodeFirstGrpcClient/
  └ Program.cs
SampleCodeFirstGrpcContracts/
  └ CalculatorContract.cs
SampleCodeFirstGrpcServer/
  ├ CalculatorService.cs
  └ Program.cs

構成を確認したところで、先程Client/Serverに作っていたprotoファイルの内容をContractsに移植していきます。

Contracts

移植するとは書いたものの、やっていることは概ねサービスインターフェースとクラスをいつもどおり書いているだけです。
サービスインターフェースにはServiceのAttributeが、クラスにはProtoContractのAttributeが付きます。

using System.Threading.Tasks;
using ProtoBuf;
using ProtoBuf.Grpc.Configuration;

namespace SampleCodeFirstGrpcContracts;

[Service]
public interface ICalculatorService
{
    [Operation]
    Task<AddResponse> Add10Async(AddRequest request);
}

[ProtoContract]
public class AddRequest
{
    [ProtoMember(1)]
    public int InputNumber { get; set; }
}

[ProtoContract]
public class AddResponse
{
    [ProtoMember(1)]
    public string Message { get; set; }
}

Server側の設定

Contractsに作成したインターフェースを実装するクラスをServer側に作っていきます。
といってもやっていることは先ほどのものとあまり変わらず、いつもの.NETの開発ともかわりません。

using SampleCodeFirstGrpcContracts;

namespace SampleCodeFirstGrpcServer;

public class CalculatorService : ICalculatorService
{
    public async Task<AddResponse> Add10Async(AddRequest request, CancellationToken cancellationToken)
    {
        return new AddResponse
        {
            Message = $"Result is {request.InputNumber + 10}"
        };
    }
}

Program.csは以下のようになります。
builder.Services.AddGrpc();builder.Services.AddCodeFirstGrpc();にするだけでCode-first gRPCを使えます。

using ProtoBuf.Grpc.Server;
using SampleCodeFirstGrpcContracts;
using SampleCodeFirstGrpcServer;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCodeFirstGrpc();
builder.Services.AddSingleton<ICalculatorService, CalculatorService>();

var app = builder.Build();

app.MapGrpcService<ICalculatorService>();

app.Run();

Client側の設定

最後にClientです。
Serviceの登録の仕方が少し異なりますが、やっていることは先ほどまでのものと概ね変更無いです。

using System;
using System.Threading;
using Grpc.Net.Client;
using ProtoBuf.Grpc.Client;
using SampleCodeFirstGrpcContracts;

using var channel = GrpcChannel.ForAddress("https://localhost:7103");

var client = channel.CreateGrpcService<ICalculatorService>();
var response = await client.Add10Async(
    new AddRequest { InputNumber = 5 }, CancellationToken.None);

Console.WriteLine(response.Message);

これにてCode-first gRPCを使うための準備は完了です。
ClientとServerを起動することで先ほどと同様の動きをしていることを確認できます。

3. Azure App ServiceでgRPC(gRPC-Web)のサービスを使う

ローカルでの起動も確認できたところで、作成したサービスをApp Serviceに載せていきます。
が、WindowsベースのApp Serviceは2022年12月現在gRPCに対応しておらず、gRPC-Webを使う必要があります2
なのでその設定も追加していきます。

gRPC-Webってなに?という方は以下をご参照ください。

Server側の設定

変更を加えるのはProgram.csのみです。

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SampleGrpcServer.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCodeFirstGrpc();
builder.Services.AddSingleton<ICalculatorService, CalculatorService>();

var app = builder.Build();

app.UseRouting();

app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });

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

app.Run();

主な変更点は以下の部分です。

app.UseRouting();

app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });

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

なお設定に当たりGrpc.AspNetCore.Webパッケージを追加で入れています。

UseGrpcWebでgRPC-Webを使うためのミドルウェアの設定をよしなにやってくれます。
gRPC-WebにおいてはgRPC ClientとgRPC Serverは本当に直接やり取りをしているわけではなく、ここで設定しているミドルウェアによってHTTPコールをgRPCフレンドリーなコールに変換されています(参考記事)。
このように設定するだけで裏側の通信のことを意識せずに済むので本当に助かります。

なおGrpcWebOptionsは指定しなくても書けます。
その場合は以下のような指定になります。

app.UseRouting();

app.UseGrpcWeb();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGrpcService<CalculatorService>().EnableGrpcWeb();
});

gRPC-Web以外のサービスをMapしたいような場合はこのようにServiceごとにEnableGrpcWeb()するのがよいと思いますが、自分たちの場合そのようなケースはなかったためデフォルトの指定をしています。

Client側の設定

チャンネルの設定を少しいじります。

using System;
using System.Net.Http;
using Grpc.Net.Client;
using Grpc.Net.Client.Web;
using SampleGrpcClient;

using var channel = GrpcChannel.ForAddress("https://localhost:7103", new GrpcChannelOptions
{
    HttpHandler = new GrpcWebHandler(new HttpClientHandler())
});

var client = new Calculator.CalculatorClient(channel);
var response = await client.Add10Async(
    new AddRequest {InputNumber = 5});

Console.WriteLine(response.Message);

ここのGrpcWebHandlerはHTTPリクエストを行うためのハンドラーです。
またこれを入れることでContent-Typeをapplication/grpc-webにするよう指定しています(オプションの説明はこちらをご参照ください)。

補足

Serverのエンドポイントについて

何も設定しない場合、gRPCサーバーのエンドポイントはhttps://{Domain}/{Namespace}.{Service}/{Operation}となります。
Client/Serverで直接やりとりする場合基本的にいじる必要はありませんが、API Management等を使って通じてサービスドメインごとにドメイン名以降の部分を設定したいような場合はインターフェース側で設定する必要があります。

例えばエンドポイントをhttps://{Domain}/Calculator/{Service}/{Operation}とする場合、インターフェースのうちServiceAttributeの部分に以下のような指定をします。

[Service("Calculator/CalculatorService")]
public interface ICalculatorService
{
    [Operation]
    Task<AddResponse> Add10Async(AddRequest request);
}

Protobufにてサポートされていない型を使いたい

DateTimeやDateTimeOffsetのように、.NETでは比較的使われていてもProtobufのスカラ型ではサポートされていない型が存在しています3参考記事)。
その場合Surrogateを定義し、protobuf-netがDateTimeOffsetの代わりにそのSurrogateを使用するよう設定してあげる必要があります。
具体的にやっていることはこちらのStackOverflowに記載の通りなので詳しくは書きませんが、Surrogateを作ったらProgram.csにて以下のようなコードを一行いれるだけでその設定をすることができます。

RuntimeTypeModel.Default.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate));

Code-firstで定義したインターフェイスからprotoを確認する

Code-firstで定義したインターフェースからどういうprotoファイルが作られるか確認することもできます(protobuf-net.Grpc.AspNetCore.Reflectionを追加で入れる必要があります)。

var generator = new SchemaGenerator();
var schema = generator.GetSchema<ICalculatorService>();
Console.WriteLine(schema);

出力結果は以下のとおりです。

syntax = "proto3";
package SampleCodeFirstGrpcContracts;

message AddRequest {
   int32 InputNumber = 1;
}
message AddResponse {
   string Message = 1;
}
service CalculatorService {
   rpc Add10 (AddRequest) returns (AddResponse);
}

終わりに

今回はCode-first gRPCをつかってgRPC Client/Serverを立てるところまで見ていきました(ついでにApp Serviceで使うときの考慮点も述べました)。

gRPCでサービスを作る場合のREST APIと比べたときのメリット一般については知っていたもののちょっとキャッチアップが面倒だなと感じていました。
Code-first gRPCを使うことでそのあたりのキャッチアップが容易になったと個人的には感じました。
また実際にプロジェクトを進めていくにあたり、設定が非常に楽なのとHttpClientの取り回しをすることがなくなったことから裏側の通信についてあまり意識することなく、機能開発に集中できた点は非常によかったと思います。

ということで今回はこんなところで終わりです。

  1. このように定義を共有できることから公式においてはClient/Serverの両方が.NETにて開発されているときに採用することを推奨しています(参考リンク

  2. 2022年9月にLinuxベースのApp ServiceにてgRPCサポートがpublic previewとなりましたが、筆者の携わったプロジェクトがそれよりも前に進められていたこともありgRPC-Webを使う必要がありました(参考リンク

  3. もちろんTimestamp等Protobufに対応するスカラ型の用意されているものを使えればそれでよいのですが、今回は諸々の都合からDateTimeOffsetを使いたいようなケースを想定しています

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6