かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ASP.NET Core 3.0 で gRPC してみよう

.NET Core になると WCF のサーバーサイドが消えて移行先として gRPC があげられてるのを何処かで見た気がします。OSS の WCF もあった気がするけど、そっちはよく見てない。

ということで、ASP.NET Core 3.0 Preview で gRPC 試してみようと思います。

プロジェクトの作成

今日は出先のカフェでコーヒー飲みながら Surface Go で書いてます。なので Visual Studio 2019 は入ってない(Surface Go には重すぎた)ので、Visual Studio Code でいきます。

適当なフォルダーで空の Web アプリを作ります。

$ dotnet new web -o GrpcServer

ソリューションも作って追加しておきましょう。

$ dotnet new sln
$ dotnet sln add GrpcServer/GrpcServer.csproj

Visual Studio Code でソリューションのあるフォルダーを開いて task.json とかを生成して Ctrl + Shift + B でビルドしたり F5 でデバッグできるようにしました。便利。

f:id:okazuki:20190830143509p:plain

gRPC のためのパッケージを追加します。

$ dotnet add .\GrpcServer\GrpcServer.csproj package Grpc.AspNetCore -v 0.1.22-pre3

そして、.proto ファイルを生成しましょう。これも dotnet new で生成できます。後で作成するクライアントでも使う予定なので、ソリューションのあるフォルダーに Proto/Proto.proto とかみたいな感じで作りました。

$ dotnet new proto -o Proto

サービスの定義を追加します。

syntax = "proto3";

option csharp_namespace = "GrpcSample";

service Greeter {
    rpc Greet (GreetRequest) returns (GreetReply);
}

message GreetRequest {
    string name = 1;
}

message GreetReply {
    string message = 1;
}

サーバープロジェクトに追加するために .csproj を編集しましょう。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="0.1.22-pre3" />
  </ItemGroup>

  <ItemGroup>
    <!-- これを追加 -->
    <Protobuf Include="../Proto/Proto.proto" LinkBase="Proto/Proto.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>

これでビルドをすると GrpcServer/obj/Debug に Proto.cs と ProtoGrpc.cs が生成されます。これを継承してサービスを実装します。

GrpcServer プロジェクトに Services フォルダーを作って、そこに GreeterService.cs を作って以下のように Greeter.GreeterBase を継承する形で処理を作ります。

using System.Threading.Tasks;
using Grpc.Core;
using GrpcSample;

namespace GrpcService.Services
{
    public class GreeterService : Greeter.GreeterBase
    {
        public override Task<GreetReply> Greet(GreetRequest request, ServerCallContext context)
        {
            return Task.FromResult(new GreetReply
            {
                Message = $"Hello {request.Name}",
            });
        }
    }
}

.proto に定義したクラスとサービスのメソッドのひな型は基本クラスで定義されているので、やることはメソッドをオーバーライドして実装するだけです。簡単。

Startup.cs で gRPC 機能の有効化と上で作成したサービスを登録する処理を追加します。

using GrpcService.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace GrpcServer
{
    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<GreeterService>(); // これを追加
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
}

これでサーバーは完成しましたといいたいところですが、もうちょっとだけ設定を… gRPC は HTTP/2 を使うので、その設定を追加します。appsettings.json に以下のような設定を追加します。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http2"
    }
  }
}

Kestrel の部分が追加したものになります。

クライアントの作成

サーバーだけ作っても、誰も呼んでくれないと何もできないのでクライアントも作ります。WPF でいきましょう。さくっとプロジェクトを作ってソリューションに追加します。

$ dotnet new wpf -o GrpcClient
$ dotnet sln add .\GrpcClient\GrpcClient.csproj

そして gRPC のクライアント側に必要なパッケージを入れます。

dotnet add .\GrpcClient\GrpcClient.csproj package Google.Protobuf -v 3.9.1
dotnet add .\GrpcClient\GrpcClient.csproj package Grpc.Net.Client -v 0.1.22-pre3
dotnet add .\GrpcClient\GrpcClient.csproj package Grpc.Tools -v 2.23.0 

そして、GrpcClient.csproj に Protobuf タグを追加します。今回は生成してもらうのはクライアントなので GrpcServices 属性には Client を設定してます。余談ですがクライアントとサーバーの両方を生成してほしいときは Both とかくみたいです。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.9.1" />
    <PackageReference Include="Grpc.Net.Client" Version="0.1.22-pre3" />
    <PackageReference Include="Grpc.Tools" Version="2.23.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="../Proto/Proto.proto" LinkBase="Proto/Proto.proto" GrpcServices="Client" />
  </ItemGroup>

</Project>

そして、名前を入力するための TextBox とサービスを呼ぶための Button を置いた画面を MainWindow.xaml に定義して…

<Window x:Class="GrpcClient.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:GrpcClient"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <TextBox x:Name="textBoxName" />
        <Button Content="Call gRPC service"
                Click="CallGrpcServiceButton_Click" />
    </StackPanel>
</Window>

コードビハインドにサービスを呼び出すコードを書きましょう。

using System;
using System.Net.Http;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using GrpcSample;

namespace GrpcClient
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void CallGrpcServiceButton_Click(object sender, RoutedEventArgs e)
        {
            using (var client = new HttpClient
            {
                BaseAddress = new Uri("https://localhost:5001")
            })
            {
                var greetServices = Grpc.Net.Client.GrpcClient.Create<Greeter.GreeterClient>(client);
                var response = await greetServices.GreetAsync(new GreetRequest
                {
                    Name = textBoxName.Text,
                });
                MessageBox.Show(response.Message);
            }
        }
    }
}

そうするとコンパイルエラー!!

MainWindow.xaml.cs(16,7): error CS0246: The type or namespace name 'GrpcSample' could not be found (are you missing a using directive or an assembly reference?) [c:\Users\k_ota\source\repos\GrpcLab\GrpcClient\GrpcClient_teh220ey_wpftmp.csproj]

起きてるエラーとしては以下の Issue と似てるけど、こっちはクラシックツールチェーン…

github.com

試しに WPF じゃなくてコンソールアプリで同じ手順を踏んで呼び出す処理を書いたらコンパイルエラーにならないので WPF on .NET Core 用のツールまわりのバグかな?とりあえずの回避方法はクライアントコードの生成をクラスライブラリにうつすことです。

プロジェクトを作成して、必要な参照を追加したりします。

$ dotnet new classlib -o GrpcClientLib
$ dotnet sln add .\GrpcClientLib\GrpcClientLib.csproj

生成されるのは .NET Standard 2.0 のプロジェクトなのですが、Grpc.Net.Client 0.1.22-pre3 は .NET Standard 2.1 (ターゲットにしてるライブラリ始めてみた!)なので GrpcClientLib.csproj の netstandard2.0 を netstandard2.1 に書き換えてから下記コマンドでライブラリを追加します。

$ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Google.Protobuf -v 3.9.1
$ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Grpc.Net.Client -v 0.1.22-pre3
$ dotnet add .\GrpcClientLib\GrpcClientLib.csproj package Grpc.Tools -v 2.23.0 

そして GrpcClientLib.csproj に、Protobuf タグの定義を追加します。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.9.1" />
    <PackageReference Include="Grpc.Net.Client" Version="0.1.22-pre3" />
    <PackageReference Include="Grpc.Tools" Version="2.23.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>
  
  <ItemGroup>
    <Protobuf Include="../Proto/Proto.proto" LinkBase="Proto/Proto.proto" GrpcServices="Client" />
  </ItemGroup>

</Project>

WPF 側からは Protobuf のタグを削除しておきましょう。

そして、GrpcClient に GrpcCLientLib への参照を追加します。

$ dotnet add .\GrpcClient\GrpcClient.csproj reference .\GrpcClientLib\GrpcClientLib.csproj

これでコンパイルエラーなしでビルドが通るようになります。

まだ一度も入れたことがない人は .NET Core の開発用の証明書をインストールして

$ dotnet dev-certs https --trust

dotnet run でサーバーとクライアントを起動して試してみましょう。

まずは、サーバー

$ dotnet run --project .\GrpcServer\GrpcServer.csproj

そして、クライアント

$ dotnet run --project .\GrpcClient\GrpcClient.csproj

適当に TextBox に何か入れてボタンを押すと無事動きました

f:id:okazuki:20190830160819p:plain

Azure にデプロイ!!

Azure の App Service に gRPC のサービスをデプロイして動かすことはできないみたいです。残念。

github.com

AKS 使えば出来そうですが、ここに書くにはちょっとヘビーなので、また今度トライしてみて書きます。

まとめ

ASP.NET Core の gRPC 割とサクッと作れていい感じです。 LTS 版の .NET Core 3.1 が出たら使ってみたいなぁ。でも App Service の対応は早くしてほしいところ。

ソースコードは、GitHub にあげておきました。

github.com