gRPCと.NET Core 3.0でマイクロサービスを実装する

# はじめに
この記事はgRPCと.NET Core 3.0でマイクロサービスを実装するチュートリアルで[こちらの原文](https://auth0.com/blog/implementing-microservices-grpc-dotnet-core-3/)を元に作成しています。Auth0の無料アカウントの取得とテナントの作成が完了していることが前提となっています。まだの方は[こちら](https://qiita.com/furuth/items/8826b66a3ec2b2b14222)の記事を参照の上ご準備をお願いします。

# 検証環境
- **OS** : `macOS Catalina 10.15.2`
- **.NET Core 3.0 SDK** : `3.1.1`

# 手順
**<font color="DarkOrange">.NET Coreのインストール手順は割愛します。この記事では[こちら](https://dotnet.microsoft.com/download/dotnet-core/thank-you/sdk-3.1.100-macos-x64-installer)からインストーラをダウンロードしました。</font>**

## gRPCサービスの構築
作業用のディレクトリを作成します。

```console
$ mkdir grpc-dotnet
$ cd grpc-dotnet
```
.NET Core 3.0 gRPCのテンプレートを利用してgRPCサービスのプロジェクトディレクトリを作成します。

```console
$ dotnet new grpc -o CreditRatingService
```

.protoを作成してContractを定義します。

```console
$ pwd
~/grpc-dotnet/CreditRatingService/Protos
$ rm greet.proto
$ touch credit-rating-service.proto
```

```protobuf:CreditRatingService/Protos/credit-rating-service.proto
syntax = "proto3";

option csharp_namespace = "CreditRatingService";

package CreditRating;

service CreditRatingCheck {
  rpc CheckCreditRequest (CreditRequest) returns (CreditReply);
}

message CreditRequest {
  string customerId = 1;
  int32 credit = 2;
}

message CreditReply {
  bool isAccepted = 1;
}
```

CreditRatingService.csprojを修正して作成した.protoファイルを指定します。

```xml:CreditRatingService/CreditRatingService.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Protobuf Include="Protos\credit-rating-service.proto" GrpcServices="Server" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.23.2" />
  </ItemGroup>
</Project>

```

CreditRatingService.csを作成してgRPCサービスを実装します。

```console
$ pwd
~/grpc-dotnet/CreditRatingService/Services
$ mv GreeterService.cs CreditRatingService.cs
```

```cs:CreditRatingService/Services/CreditRatingService.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Logging;

namespace CreditRatingService
{
    public class CreditRatingCheckService: CreditRatingCheck.CreditRatingCheckBase
    {
        private readonly ILogger<CreditRatingCheckService> _logger;
        private static readonly Dictionary<string, Int32> customerTrustedCredit = new Dictionary<string, Int32>() 
        {
            {"id0201", 10000},
            {"id0417", 5000},
            {"id0306", 15000}
        };
        public CreditRatingCheckService(ILogger<CreditRatingCheckService> logger)
        {
            _logger = logger;
        }

        public override Task<CreditReply> CheckCreditRequest(CreditRequest request, ServerCallContext context)
        {
            return Task.FromResult(new CreditReply
            {
                IsAccepted = IsEligibleForCredit(request.CustomerId, request.Credit)
            });
        }

        private bool IsEligibleForCredit(string customerId, Int32 credit) {
            bool isEligible = false;

            if (customerTrustedCredit.TryGetValue(customerId, out Int32 maxCredit))
            {
                isEligible = credit <= maxCredit;
            }

            return isEligible;
        }
    }
}
```

Startup.csを修正してgRPCサービスとインフラストラクチャを連携します。

```cs:CreditRatingService/Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CreditRatingService
{
    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.UseEndpoints(endpoints =>
            {
                endpoints.MapGrpcService<CreditRatingCheckService>();

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client.");
                });
            });
        }
    }
}
```

Program.csを修正してgRPCでTransport Layer Socketを無効化します。
**<font color="DarkOrange">MacOSや古いバージョンのWindowsで.NET CoreでTLSが有効化されているとエラーが発生する既知の事象が上がっており、そちらを回避するために行います。</font>**

```cs:CreditRatingService/Program.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;

namespace CreditRatingService
{
    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.ConfigureKestrel(options =>
                    {
                        // Setup a HTTP/2 endpoint without TLS.
                        options.ListenLocalhost(5000, o => o.Protocols = 
                            HttpProtocols.Http2);
                    });
                    webBuilder.UseStartup<Startup>();
                });
    }
}
```



## gRPCクライアントの構築
.NET Core 3.0 gRPCのテンプレートを利用してgRPCクライアントのプロジェクトディレクトリを作成します。

```console
$ pwd
~/grpc-dotnet
$ dotnet new grpc -o CreditRatingClient
```

必要なパッケージをインストールします。

```console
$ cd CreditRatingClient
$ dotnet add CreditRatingClient.csproj package Grpc.Net.Client
$ dotnet add CreditRatingClient.csproj package Google.Protobuf
$ dotnet add CreditRatingClient.csproj package Grpc.Tools
```

gRPCサービス側に作成したContractをコピーしてgRPCサービスをコールできるようにします。

```
$ pwd
~/grpc-dotnet/CreditRatingClient
$ mkdir Protos; cd Protos
$ cp ~/grpc-dotnet/CreditRatingService/Protos/credit-rating-service.proto .
```

credit-rating-service.protoを修正してgRPCサービス側のContractを指定します。

```xml:CreditRatingClient/Protos/credit-rating-service.proto
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.10.1" />
    <PackageReference Include="Grpc.Net.Client" Version="2.25.0" />
    <PackageReference Include="Grpc.Tools" Version="2.25.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="Protos\credit-rating-service.proto" GrpcServices="Client" />
  </ItemGroup>
</Project>
```

Program.csを修正してgRPCクライアントを実装します。

```cs:CreditRatingClient/Program.cs
using System;
using System.Threading.Tasks;
using CreditRatingService;
using Grpc.Net.Client;

namespace GrpcGreeterClient
using System;
using System.Threading.Tasks;
using CreditRatingService;
using Grpc.Net.Client;

namespace GrpcGreeterClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            AppContext.SetSwitch(
                "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

            // The port number(5001) must match the port of the gRPC server.
            var channel = GrpcChannel.ForAddress("http://localhost:5000");
            var client =  new CreditRatingCheck.CreditRatingCheckClient(channel);
            var creditRequest = new CreditRequest { CustomerId = "id0201", Credit = 7000};
            var reply = await client.CheckCreditRequestAsync(creditRequest);

            Console.WriteLine($"Credit for customer {creditRequest.CustomerId} {(reply.IsAccepted ? "approved" : "rejected")}!");
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}
```

# 動作確認
gRPCサービス、クライアントを起動して通信可能か確認します。クライアント側のコンソールに"Credit for customer id0201 approved! Press any key to exit..."が表示されれば成功です。

```console
$ pwd
~/grpc-dotnet/CreditRatingService
$ dotnet run
warn: Microsoft.AspNetCore.Server.Kestrel[0]
      Overriding address(es) 'https://localhost:5001'. Binding to endpoints defined in UseKestrel() instead.
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/hisashiyamaguchi/grpc-dotnet/CreditRatingService
```

```console
$ pwd
~/grpc-dotnet/CreditRatingClient
$ dotnet run
Credit for customer id0201 approved!
Press any key to exit...
```

# [執筆中].NET Core 3.0をAuth0に登録
**<font color="DarkOrange">この章はコードに不備があり動作しないことが判明しています。現在、記事原文の執筆者と協議しながらワークアラウンドを模索中です。</font>**

Auth0のDashboardにログインして左ペインの"APSs"をクリック、右上の"CREATE API"を押します。

<img src="https://i.imgur.com/jskrQuA.png" width=70%>

"Name", "Identifier"に任意の名前を入力, "Signing Algorithm"はデフォルトのままで下の”CREATE”を押します。

<img src="https://i.imgur.com/C3s6Scl.png" width=70%>

gRPCのStartup.csを修正してAuth0のドメインとAPIのIdentifierを指定します。

```cs:CreditRatingService/Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CreditRatingService
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            string domain = $"https://{Configuration["Auth0:Domain"]}/";
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.Authority = domain;
                options.Audience = Configuration["Auth0:Audience"];
            });
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

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

                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client.");
                });
            });

            app.UseAuthentication();
        }
    }
}
```

gRPCサービスのappsettings.jsonを修正してAuth0テナントのドメイン名とAPIのIdentifierを環境変数として設定します。

```json:CreditRatingService/appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http2"
    }
  },
  "Auth0": {
    "Domain": "kiriko.auth0.com",
    "Audience": "https://qiita.com"
  }
}
```