カイゼン・ジャーニー

主人公の江島がアジャイルな開発に出会い、アジャイルのプラクティスを1人から初めて、次第に周囲を巻き込んでいく。最終的には組織を越境したアジャイルな開発を実現するサクセスストーリーだった。

ストーリー仕立てになっているので、どのような問題に遭遇したときに、アジャイルスクラムのプラクティスをどう駆使して解決していくのか、イメージが湧きやすかった。プラクティスだけ紹介されても、それをいつどうやってやればいいのさ、ってなるだろうし。ストーリーとプラクティスが一緒に、頭にスッと入ってきた。

ただまぁ、サクセスストーリーなのでハッピーエンド前提だし、ちょっと都合良すぎなのではと思うところは無くもない。例えば第二部はチームでのカイゼンがテーマだけど、チームが組まれた経緯は巻き込んだというより巻き込まれた形に思えた気がする。

本書で紹介されたプラクティスを実践する際は、参考文献にもあたってそれぞれ深掘りするとして、まずは1人からでいいので始めるところからだな。始めるのが一番大事。周囲を巻き込めたら儲けもの。ダメ元で、転職したつもりになって行動してみるといいかもしれない。

Xamarin.iOS での Microsoft.Data.Sqlite の利用

Microsoft.Data.Sqlite は、Xamarin で SQLite を使うときの定番になっている sqlite-net-pcl と同じく、SQLitePCL.raw に依存している。 sqlite-net-pcl が Xamarin.iOS で利用できるということは、Microsoft.Data.Sqlite も利用できるに違いない。

サンプルを書いて試してみた。

using System;
using System.Collections.Generic;
using System.IO;
using Foundation;
using Microsoft.Data.Sqlite;
using UIKit;
using Xamarin.Essentials;

namespace HelloSqlite
{
    public class Application
    {
        static void Main(string[] args)
        {
            UIApplication.Main(args, null, "AppDelegate");
        }
    }

    [Register("AppDelegate")]
    public class AppDelegate : UIApplicationDelegate
    {
        public override UIWindow Window { get; set; }

        public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
        {
            SQLitePCL.Batteries_V2.Init();

            Window = new UIWindow(UIScreen.MainScreen.Bounds);
            Window.RootViewController = new UINavigationController(
                new ItemsViewController());
            Window.MakeKeyAndVisible();

            return true;
        }
    }

    public class Item
    {
        public int Id { get; set; }

        public string Content { get; set; }
    }

    public class ItemsViewController : UITableViewController
    {
        List<Item> items = new List<Item>();

        public ItemsViewController()
            : base(UITableViewStyle.Plain)
        {
            Title = "HelloSqlite";
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            NavigationItem.RightBarButtonItem = new UIBarButtonItem(
                UIBarButtonSystemItem.Add, HandleAdd);

            CreateTable();
            LoadItems();
        }

        public override nint RowsInSection(UITableView tableView, nint section)
        {
            return items.Count;
        }

        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            const string cellId = "ItemCell";
            var cell = tableView.DequeueReusableCell(cellId) ??
                new UITableViewCell(UITableViewCellStyle.Default, cellId);
            var item = items[indexPath.Row];
            cell.TextLabel.Text = item.Content;
            return cell;
        }

        void HandleAdd(object sender, EventArgs e)
        {
            AddItem();
            LoadItems();
        }

        void CreateTable()
        {
            using (var connection = CreateConnection())
            {
                connection.Open();
                using (var command = connection.CreateCommand())
                {
                    command.CommandText = @"
                        CREATE TABLE IF NOT EXISTS items (
                            id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
                            content TEXT NOT NULL
                        );";
                    command.ExecuteNonQuery();
                }
            }
        }

        void AddItem()
        {
            using (var connection = CreateConnection())
            {
                connection.Open();
                using (var command = connection.CreateCommand())
                {
                    command.CommandText = $@"
                        INSERT INTO items (
                            content
                        ) VALUES (
                            '{DateTime.Now}'
                        )";
                    command.ExecuteNonQuery();
                }
            }
        }

        void LoadItems()
        {
            var nextItems = new List<Item>();
            using (var connection = CreateConnection())
            {
                connection.Open();
                using (var command = connection.CreateCommand())
                {
                    command.CommandText = @"
                        SELECT id, content
                        FROM items
                        ORDER BY id";
                    using (var reader = command.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            var item = new Item
                            {
                                Id = reader.GetInt32(0),
                                Content = reader.GetString(1),
                            };
                            nextItems.Add(item);
                        }
                    }
                }
            }

            items = nextItems;
            TableView.ReloadData();
        }

        SqliteConnection CreateConnection()
        {
            var builder = new SqliteConnectionStringBuilder();
            builder.DataSource = Path.Combine(
                FileSystem.AppDataDirectory,
                "HelloSqlite.db");
            return new SqliteConnection(builder.ToString());
        }
    }
}

iOS のシミュレーターだと動的コード生成を行うプログラムであっても動かせてしまうので、実機で動くことを確認するまでは安心できない。

このサンプルを実機に転送して実行したところ、期待通り SQLite データベースにデータを登録できた。

Microsoft.Data.Sqlite が使えたということは、 ADO.NET の抽象化の上に構築されたライブラリが Xamarin.iOS でも使える、 ということになる。 ただし、動的コード生成を行っていなければね。 このあいだ Dapper のソースコードを読んだら DynamicMethod 使っていたんだよなぁ。 残念。

大地のうどん中洲店

福岡でうどんを食べるなら自分は牧のうどん派。 でもたまに、豊前裏打会のうどんも食べたくなる。 そして、福岡市で豊前裏打会といったら真っ先に名前が出てくる『大地のうどん』が、 川端商店街に中洲店をオープンしたのでさっそく行ってみた。

f:id:griefworker:20200310230846j:plain

タイミング良く待たずに座れたが、すぐに満席になったので間一髪だったな。 券売機に悪戦苦闘しながらなんとか購入した、ごぼう天うどんとミニカツ丼のセットの食券を渡して、 気長に待つこと10分弱。見るものを圧倒する巨大なごぼう天で蓋をされたうどんと、 ミニカツ丼が運ばれてきた。

f:id:griefworker:20200310230942j:plain

透き通っていてもちっとした食感の麺、これよこれ。この麺がたまに無性に食べたくなる。 豊前裏打会の麺は、自分のうどんの概念を変えてくれた。 あっさりしているようでしっかりとした旨味のあるつゆが、 この麺を一層引き立たせている。 ごぼう天はカリッと揚がっていて香ばしい。 ただ、慣れるまで食べるのに苦労しそうだ。 次からマルではなくバラで注文しよう。

f:id:griefworker:20200310231053j:plain

ミニカツ丼も濃過ぎず丁度いい塩梅で、 うどんのパートナーにピッタリだ。 そば屋のカツ丼が旨いとは良く言われるが、 うどん屋もなかなか。 つゆが重要なのはどちらも同じ。

f:id:griefworker:20200310231104j:plain

和助が天神から大名に移動して遠くなったのを残念に思っていたので、 職場から徒歩で行ける範囲内に豊前裏打会の、 しかも大地のうどんがオープンしたのは凄く嬉しい。 たびたび利用することになると思う。

関連ランキング:うどん | 中洲川端駅呉服町駅祇園駅

コードファースト ASP.NET Core gRPC でのインターセプター利用

はじめに

protobuf-net.Grpc を使ってコードファーストに ASP.NET Core gRPC サービスを実装する場合でも、 ASP.NET Core の JWT Bearer 認証ミドルウェアを使うことができそうだ、 というところまで前回確認できた。

tnakamura.hatenablog.com

Authorization ヘッダーを詰めた Metadata を毎回送信するのは手間なので、 自動化したいところ。 gRPC にはインターセプターという仕組みが用意されているので、 Authorization ヘッダーを送信するインターセプターを作成すれば良さそうだ。

問題は、protobuf-net.Grpc を使ってコードファーストに実装する場合でも、 インターセプターが使えるのか。 試してみた。

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 ITokenServie
    {
        ValueTask<TokenReply> TokenAsync(
            TokenRequest request);
    }

    [ServiceContract]
    public interface IGreetingService
    {
        ValueTask<HelloReply> HelloAsync(CallContext context = default);
    }

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

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

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

Server プロジェクトを作成

これまた前回と同じになるが、 gRPC サービスプロジェクトを作成し、 コードファーストで gRPC サービスを実装する。

コードファーストでやるために、 protobuf-net.Grpc.AspNetCore パッケージを追加しておく。 Microsoft.AspNetCore.Authentication.JwtBearer パッケージも追加し、 JWT Bearer 認証を組み込む。

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using CodeFirstGrpc.Shared;
using Grpc.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
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();

            services.AddAuthorization(options =>
            {
                // デフォルトの認証スキーマのポリシーを簡単なものに上書き
                options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
                {
                    policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
                    policy.RequireClaim(ClaimTypes.Name);
                });
            });

            // JWT Bearer 認証を組み込む
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    // サンプルを簡略化するため検証機能を OFF にする
                    // 本番でこんなことをしてはダメ
                    options.TokenValidationParameters =
                        new TokenValidationParameters
                        {
                            ValidateAudience = false,
                            ValidateIssuer = false,
                            ValidateActor = false,
                            ValidateLifetime = true,
                            IssuerSigningKey = SecurityKey
                        };
                });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseRouting();

            // パイプラインに認証と認可を組み込む
            app.UseAuthentication();
            app.UseAuthorization();

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

        internal static readonly SymmetricSecurityKey SecurityKey =
            new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());
    }

    public class TokenService : ITokenServie
    {
        public ValueTask<TokenReply> TokenAsync(TokenRequest request)
        {
            var token = GenerateJwtToken(request.UserName);
            return new ValueTask<TokenReply>(new TokenReply
            {
                Token = token,
            });
        }

        string GenerateJwtToken(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new InvalidOperationException("Name is not specified.");
            }

            var claims = new[]
            {
                new Claim(ClaimTypes.Name, name),
            };
            var credentials = new SigningCredentials(
                Startup.SecurityKey,
                SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                "ExampleServer",
                "ExampleClients",
                claims,
                expires: DateTime.Now.AddSeconds(60),
                signingCredentials: credentials);
            return JwtTokenHandler.WriteToken(token);
        }


        readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
    }

    // 認証必須のサービス
    [Authorize]
    public class GreetingService : IGreetingService
    {
        public ValueTask<HelloReply> HelloAsync(CallContext context = default)
        {
            // 認証に成功していれば HttpContext からユーザーの情報を取得できるはず
            var httpContext = context.ServerCallContext.GetHttpContext();
            var userName = httpContext.User.Identity.Name;
            return new ValueTask<HelloReply>(new HelloReply
            {
                Message = $"Hello, {userName}.",
            });
        }
    }
}

Client プロジェクトを作成

長い前置きが終わって、ここからが本番。

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

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

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

            // JWT Bearer トークン無しで保護されたサービスを呼び出す
            try
            {
                var greetingClient1 = channel.CreateGrpcService<IGreetingService>();
                _ = await greetingClient1.HelloAsync();
            }
            catch (RpcException ex)
            {
                Console.WriteLine(ex.Message);
            }

            // JWT Bearer トークン取得
            var tokenClient = channel.CreateGrpcService<ITokenServie>();
            var tokenReply = await tokenClient.TokenAsync(new TokenRequest
            {
                UserName = "Kubo",
            });

            // Metadata に常に Authorization を追加するインターセプターを利用する
            // Intercept の戻り値は CallInvoker なので、CreateGrpcService<T> は使えない。
            // 代わりに ClientFactory の CreateClient<T>(CallInvoker) を使う。
            var callInvoker = channel.Intercept(new AuthorizationInterceptor(tokenReply.Token));
            var greetingClient2 = ClientFactory.Default.CreateClient<IGreetingService>(callInvoker);
            var helloRepoly = await greetingClient2.HelloAsync();
            Console.WriteLine($"Message: {helloRepoly.Message}");

            Console.ReadLine();
        }
    }

    // Metadata に常に Authorization を追加するインターセプター
    class AuthorizationInterceptor : Interceptor
    {
        readonly string jwt;

        public AuthorizationInterceptor(string jwt)
        {
            this.jwt = jwt;
        }

        public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
        {
            AddAuthorizationMetadata(ref context);

            return continuation(request, context);
        }

        public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
        {
            AddAuthorizationMetadata(ref context);

            return continuation(context);
        }

        public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
        {
            AddAuthorizationMetadata(ref context);

            return continuation(request, context);
        }

        public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation)
        {
            AddAuthorizationMetadata(ref context);

            return continuation(context);
        }

        void AddAuthorizationMetadata<TRequest, TResponse>(
            ref ClientInterceptorContext<TRequest, TResponse> context)
            where TRequest : class
            where TResponse : class
        {
            var headers = context.Options.Headers;

            // ヘッダーのコレクションが無ければ作成する。
            // 作成したヘッダーのコレクションを後続のインターセプターに渡すために、
            // context も作り直す必要がある。
            if (headers == null)
            {
                headers = new Metadata();
                var options = context.Options.WithHeaders(headers);
                context = new ClientInterceptorContext<TRequest, TResponse>(
                    context.Method,
                    context.Host,
                    options);
            }

            // Authorization ヘッダーを追加
            headers.Add("Authorization", $"Bearer {jwt}");
        }
    }
}

Authorization ヘッダーを毎回自動で送信するためのインターセプターを作成した。 作成したインターセプターを組み込む場合、 今までお世話になってきた CreateGrpcService<T> が使えないので注意。 代わりに ClientFactory.CreateClient<T>(CallInvoker) を使えばいいみたい。

実行結果

Authorization ヘッダー無しで認証必須の gRPC サービスを呼び出すと、ちゃんとエラーになった。 インターセプターを組み込んだ場合は呼び出し成功。

f:id:griefworker:20200318163204p:plain

おわりに

インターセプターを自作して組み込むことで、 クライアントからサーバーに任意のメタデータを自動で送信できそう。

WCF から gRPC への移行は、 protobuf-net.Grpc を使ってコードファーストに行えそうな目途がついた。

コードファースト ASP.NET Core gRPC での JWT Bearer 認証の利用

はじめに

ASP.NET Core gRPC は ASP.NET Core のミドルウェアをサポートしているけど、 protobuf-net.Grpc を使ってコードファーストで実装する場合でも、 JWT Bearer 認証が使えるか試してみた。

Shared プロジェクトを作成

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

今回は JWT を取得するためのサービスコントラクトも定義している。

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

namespace CodeFirstGrpc.Shared
{
    [ServiceContract]
    public interface ITokenServie
    {
        ValueTask<TokenReply> TokenAsync(
            TokenRequest request);
    }

    [ServiceContract]
    public interface IGreetingService
    {
        ValueTask<HelloReply> HelloAsync(CallContext context = default);
    }

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

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

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

Server プロジェクトを作成

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

Microsoft.AspNetCore.Authentication.JwtBearer パッケージも追加し、 JWT Bearer 認証を組み込む。

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using CodeFirstGrpc.Shared;
using Grpc.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
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();

            services.AddAuthorization(options =>
            {
                // デフォルトの認証スキーマのポリシーを簡単なものに上書き
                options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
                {
                    policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
                    policy.RequireClaim(ClaimTypes.Name);
                });
            });

            // JWT Bearer 認証を組み込む
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    // サンプルを簡略化するため検証機能を OFF にする
                    // 本番でこんなことをしてはダメ
                    options.TokenValidationParameters =
                        new TokenValidationParameters
                        {
                            ValidateAudience = false,
                            ValidateIssuer = false,
                            ValidateActor = false,
                            ValidateLifetime = true,
                            IssuerSigningKey = SecurityKey
                        };
                });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseRouting();

            // パイプラインに認証と認可を組み込む
            app.UseAuthentication();
            app.UseAuthorization();

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

        internal static readonly SymmetricSecurityKey SecurityKey =
            new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());
    }

    public class TokenService : ITokenServie
    {
        public ValueTask<TokenReply> TokenAsync(TokenRequest request)
        {
            var token = GenerateJwtToken(request.UserName);
            return new ValueTask<TokenReply>(new TokenReply
            {
                Token = token,
            });
        }

        string GenerateJwtToken(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new InvalidOperationException("Name is not specified.");
            }

            var claims = new[]
            {
                new Claim(ClaimTypes.Name, name),
            };
            var credentials = new SigningCredentials(
                Startup.SecurityKey,
                SecurityAlgorithms.HmacSha256);
            var token = new JwtSecurityToken(
                "ExampleServer",
                "ExampleClients",
                claims,
                expires: DateTime.Now.AddSeconds(60),
                signingCredentials: credentials);
            return JwtTokenHandler.WriteToken(token);
        }


        readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
    }

    // 認証必須のサービス
    [Authorize]
    public class GreetingService : IGreetingService
    {
        public ValueTask<HelloReply> HelloAsync(CallContext context = default)
        {
            // 認証に成功していれば HttpContext からユーザーの情報を取得できるはず
            var httpContext = context.ServerCallContext.GetHttpContext();
            var userName = httpContext.User.Identity.Name;
            return new ValueTask<HelloReply>(new HelloReply
            {
                Message = $"Hello, {userName}.",
            });
        }
    }
}

Client プロジェクトを作成

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

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

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

            // JWT Bearer トークン無しで保護されたサービスを呼び出す
            var greetingClient = channel.CreateGrpcService<IGreetingService>();
            try
            {
                _ = await greetingClient.HelloAsync();
            }
            catch (RpcException ex)
            {
                Console.WriteLine(ex.Message);
            }

            // JWT Bearer トークン取得
            var tokenClient = channel.CreateGrpcService<ITokenServie>();
            var tokenReply = await tokenClient.TokenAsync(new TokenRequest
            {
                UserName = "Kubo",
            });

            // JWT Bearer トークンをヘッダーに付与して、保護されたサービスを呼び出す
            // CallOptions は CallContext に暗黙的に変換される
            var callOptions = new CallOptions(headers: new Metadata
            {
                new Metadata.Entry("Authorization", $"Bearer {tokenReply.Token}"),
            });
            var helloRepoly = await greetingClient.HelloAsync(callOptions);
            Console.WriteLine($"Message: {helloRepoly.Message}");

            Console.ReadLine();
        }
    }
}

Authorization ヘッダーは、Metadata に追加すれば良いみたいだった。

実行結果

Authorization ヘッダー無しで認証必須の gRPC サービスを呼び出すと、ちゃんとエラーになった。 Authorization ヘッダーを設定した場合は呼び出し成功。

f:id:griefworker:20200310174300p:plain

おわりに

コードファーストで ASP.NET Core gRPC のサービスを実装した場合でも、 ASP.NET Core の認証・認可ミドルウェアが使えそうだ。 gRPC サービスの認証をどうするかは課題だったので、ASP.NET Core MVC で慣れ親しんだ方法が使えるのは大きい。

からかい上手の高木さん(13)

西片くんと高木さんの「勝負」という名のいちゃつきは、 高木さんの攻めが最近かなり直接的になってきていたが、 ここにきてついに物理的な接触に。 高木さん自身ははぐらかしていたけど、 その前の流れからして、やはりおでこにキ…。 この作品が高木さんが西片くんを攻略するマンガだとしたら、 西片くんは外堀を埋められもう陥落寸前だろう。 そろそろ引き分けでもいいから、高木さんが慌てる展開とか見せて欲しい。

コードファースト 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 つ解消された。 次は認証を試したい。