コードファースト ASP.NET Core gRPC

この記事は、C# その2 Advent Calendar 2019 の二日目の記事です

qiita.com

はじめに

ASP.NET Core 3.0 で gRPC がサポートされた。 一方で WCF .NET Core に正式には移植されず、コミュニティによる開発にシフト。 業務では WCF をバリバリ使っているけど、gRPC への移行を検討しなければいけない時期になってきた。 2020 年には .NET 5 も来るし。

コードファーストで開発したい

Visual Studio で proto ファイルを編集したら保存時に C# のコードが生成されるようになったとはいえ、できれば proto ファイル書きたくない。

WCF では C# でデータコントラクトとサービスコントラクトのインタフェースを定義したら、 それをクライアントとサーバーの両方で利用できていた。 gRPC でも同じような体験が理想だ。

protobuf-net.Grpc

WCF から gRPC への移行方法を調べていたら、protobuf-net.Grpc というパッケージに出会った。

github.com

README には次のように書いてある。

protobuf-net.Grpc adds code-first support for services over gRPC using either the native Grpc.Core API, or the fully-managed Grpc.Net.Client / Grpc.AspNetCore.Server API.

WCF のときみたいに、コードファーストで gRPC サービスを開発できそうなシロモノに見える。

コードファースト ASP.NET Core gRPC をやってみる

.NET Core ライブラリ作成

クライアントとサーバーで共通のインタフェースを使うために、 クライアントとサーバーの両方で参照する .NET Core ライブラリプロジェクトを作成する。 名前は『HelloGrpc.Shared』。

NuGet パッケージ追加

プロジェクトには次のパッケージを追加しておく。

データコントラクトとサービスコントラクトを定義

この辺とても WCF っぽい。 引数が 1 つだけなのは protobuf-net.Grpc の制約。 gRPC でも 1 つなのは同じだし。 あと、DataMemberAttribute で Order を指定するのも必須。 .proto ファイルでもフィールドにタグ値を指定するけど、その代わりになる。

using System.Runtime.Serialization;
using System.ServiceModel;
using System.Threading.Tasks;

namespace HelloGrpc.Shared
{
    [ServiceContract]
    public interface IGreetingService
    {
        [OperationContract]
        Task<HelloReply> HelloAsync(HelloRequest request);
    }

    [DataContract]
    public class HelloRequest
    {
        [DataMember(Order = 1)]
        public string Name { get; set; }
    }

    [DataContract]
    public class HelloReply
    {
        [DataMember(Order = 1)]
        public string Message { get; set; }
    }
}
gRPC サービス作成

Visual Studio から gRPC サービスのテンプレートを選択してプロジェクトを作成。 名前は『HelloGrpc.Server』。

NuGet パッケージ追加

ASP.NET Core で gRPC サービスを実装するのに必要なパッケージはあらかた参照してあるけど、 下記のパッケージも参照に追加する。

gRPC サービス実装

services.AddCodeFirstGrpc() でコードファーストを有効にし、 endpoints.MapGrpcService<T>() で実際にコードで定義したサービスを登録するだけ。

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ProtoBuf.Grpc.Server;

namespace HelloGrpc.Server
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCodeFirstGrpc();
        }

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

            app.UseRouting();

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

    public class GreetingService : Shared.IGreetingService
    {
        public Task<Shared.HelloReply> HelloAsync(Shared.HelloRequest request)
        {
            return Task.FromResult(new Shared.HelloReply
            {
                Message = $"Hello {request.Name}"
            });
        }
    }
}
.NET Core コンソールアプリケーション作成

gRPC サービスを呼び出すクライアントは、.NET Core コンソールアプリケーションで作成する。 プロジェクト名は『HelloGrpc.Client』で。

NuGet パッケージ追加

HelloGrpc.Client プロジェクトに次のパッケージを追加。

  • Grpc.Net.Client
    • .NET 用 gRPC クライアント
  • protobut-net.Grpc
    • C# コードで定義した gRPC サービスのインタフェースをもとに動的にクライアントを生成してくれるパッケージ
gRPC クライアントを実装

protobut-net.Grpc が提供する GrpcClientFactory クラスが、インタフェースから動的に gRPC クライアントを生成する拡張メソッドを提供しているので、それを使う。

using System;
using System.Threading.Tasks;
using Grpc.Net.Client;
using HelloGrpc.Shared;
using ProtoBuf.Grpc.Client;

namespace HelloGrpc.Client
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = channel.CreateGrpcService<IGreetingService>(); // インタフェースから動的に gRPC クライアントを生成
            var reply = await client.HelloAsync(new HelloRequest
            {
                Name = "Kubo"
            });
            Console.WriteLine(reply.Message);
            Console.ReadLine();
        }
    }
}
実行

コードファーストで作成した gRPC サービスをホストし、クライアントから呼び出すことに成功。

f:id:griefworker:20191016164249p:plain

まとめ

WCF から gRPC に移行する手段として、proto ファイルを書くのではなく、protobuf-net.Grpc を使いコードファーストで開発するのは良い線いってそうだ。さらに検証が必要だけど、希望が見えてきた。

なお、コードファーストで開発する別の選択肢としては MagicOnion もある。

github.com

既に大量の WCF 資産があり、MagicOnion だとそれらに結構な修正が必要そうだったので、 今回は選ばなかった。 ベンチマークを取ったわけではないけど、シリアライザに MessagePack を使っているので、 protobuf-net.Grpc を使った場合よりも速くなりそうな期待がある。 新規にコードファーストで開発するなら、MagicOnion という選択肢は有力だと思う。