コードファースト ASP.NET Core gRPC でのヘッダーの利用

はじめに

WCF では OperationContext の IncomingMessageHeaders と OutgoingMessageHeaders を使って、 カスタムヘッダーをクライアントから送ったり、サーバーから返したりできた。

tnakamura.hatenablog.com

gRPC には Metadata という同じような機能があるので、 WCF から gRPC に移行する際にカスタムヘッダーは Metadata を使うことになる。

そこで、WCF から gRPC への移行に protobuf-net.Grpc を使う場合、 Metadata をどう扱えるのか試してみた。

Shared プロジェクト作成

クライアントとサーバーの両方で使う、サービスコントラクトとデータコントラクトを定義する。 プロジェクトの種類は .NET Core クラスライブラリ。 ServiceContractAttribute を使うために System.ServiceModel.Primitives、 CallContext を使うために protobuf-net.Grpc のパッケージを追加しておく。

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

namespace CodeFirstGrpc.Shared
{
    [ServiceContract]
    public interface IGreetingService
    {
        ValueTask<HelloReply> HelloAsync(
            HelloRequest request,
            CallContext context = default);
    }

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

    [DataContract]
    public class HelloReply
    {
        // クライアントから Metadata で送られてきた値を格納して返すためのもの
        [DataMember(Order = 1)]
        public string ClientId { get; set; }

        [DataMember(Order = 2)]
        public string Message { get; set; }
    }
}

Server プロジェクト作成

gRPC サービスプロジェクトを作成し、コードファーストで gRPC サービスを実装する。 コードファーストでやるために、 protobuf-net.Grpc.AspNetCore パッケージを参照しておく。

using System;
using System.Linq;
using System.Threading.Tasks;
using CodeFirstGrpc.Shared;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Grpc.Core;
using ProtoBuf.Grpc;
using ProtoBuf.Grpc.Server;

namespace CodeFirstGrpc.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)
        {
            app.UseRouting();

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

    public class GreetingService : IGreetingService
    {
        public async ValueTask<HelloReply> HelloAsync(
            HelloRequest request,
            CallContext context = default)
        {
            await context.ServerCallContext.WriteResponseHeadersAsync(new Metadata
            {
                new Metadata.Entry("X-ServerId", Guid.NewGuid().ToString()),
            });

            return new HelloReply
            {
                ClientId = GetClient(context),
                Message = $"Hello, {request.Name}.",
            };
        }

        // クライアントから送られてきたカスタムヘッダーから値を取り出す。
        // ヘッダーのキーはすべて小文字に変わっているので注意。
        static string GetClient(CallContext context) =>
            context.RequestHeaders
                .Where(x => x.Key == "x-clientid")
                .Select(x => x.Value)
                .FirstOrDefault();
    }
}

クライアントから送られてくる予定のカスタムヘッダーは、CallContext から取り出せる。 サーバーがカスタムヘッダーを返す場合は、CallContext が持つ ServerCallContext を使って書き込む。

Client プロジェクト作成

.NET Core コンソールアプリのプロジェクトを作成し、 gRPC サービスを呼び出すためのクライアントを実装する。 コードファーストで実装した gRPC サービスを呼び出すために、 Grpc.Net.Client と protobuf-net.Grpc のパッケージを参照しておく。

using System;
using System.Linq;
using System.Threading.Tasks;
using CodeFirstGrpc.Shared;
using Grpc.Core;
using Grpc.Net.Client;
using ProtoBuf.Grpc;
using ProtoBuf.Grpc.Client;

namespace CodeFirstGrpc.Client
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = channel.CreateGrpcService<IGreetingService>();

            // gRPC サーバーに送信するカスタムヘッダー
            var callOptions = new CallOptions(headers: new Metadata
            {
                new Metadata.Entry("X-ClientId", Guid.NewGuid().ToString()),
            });

            // サーバーが返すヘッダーを参照したい場合は
            // CaptureMetadata を指定する
            var callContext = new CallContext(
                callOptions: callOptions,
                flags: CallContextFlags.CaptureMetadata);

            var reply = await client.HelloAsync(
                new HelloRequest
                {
                    Name = "Kubo"
                },
                callContext);
            Console.WriteLine($"Message: {reply.Message}");
            Console.WriteLine($"ClientId: {reply.ClientId}");

            // サーバーから返ってくるヘッダーのキーも小文字になっている
            var serverId = callContext.ResponseHeaders()
                .Where(x => x.Key == "x-serverid")
                .Select(x => x.Value)
                .FirstOrDefault();
            Console.WriteLine($"ServerId: {serverId}");

            Console.ReadLine();
        }
    }
}

クライアントからサーバーに送るカスタムヘッダーは、CallOption に Metadata で渡す。 サーバーから返ってくるカスタムヘッダーは CallContext から取り出せるが、 そのためには CallContext 作成時に CallContextFlags.CaptureMetadata を指定しておく必要がある。

実行結果

クライアントから送ったカスタムヘッダーーと、 サーバーから返ってきたカスタムヘッダー、 両方がちゃんと表示された。

f:id:griefworker:20200309163804p:plain

おわりに

コードファースト ASP.NET Core gRPC でも、Metadata を使ってカスタムヘッダーをクライアントから送ったり、 サーバーから返したりできることを確認。 WCF から gRPC に移行するための課題がまた 1 つ解消された。 次は認証を試したい。