🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

Blazor WebAssemblyでgRPC-Webを使用する - Hosted編

先月の話ですが、Blazor WebAssemblyからgRPC-Webを使用する実験的サポートの発表がありました。

blog.stevensanderson.com

devblogs.microsoft.com

本エントリはBlazor WebAssemblyプロジェクトのREST通信をgRPC-Webに置き換える手順を追ってみたいと思います。 先生役はSteve Sanderson氏がGitHubで公開しているgRPC-Webのサンプルです。

目次です。

環境

本エントリ作成時の環境は以下の通りです。

  • .NET Core Version 3.1.1000
  • Microsoft.AspNetCore.Blazor.Templates Version 3.2.0-preview1.20073.1,
  • PowerShell Version 6.2.4

はじめに

今回はHosted編ということで、--hostedオプションを付与したBlazor WebAssemblyプロジェクトを作成します。

--hostedオプションを付与したBlazor WebAssemblyプロジェクトを作成するとClient, Server, Sharedという3つのプロジェクトが生成されます。

  • Client
    Blazor WebAssemblyのプロジェクト
  • Server
    ASP.NET CoreでBlazorを静的ファイル ホストする
  • Shared
    Client/Serverで共通のファイルを格納する

Serverフォルダ内のASP.NET CoreがClient内のBlazor WebAssemblyをホストする形ですね。共通コードはSharedに置く構成のようなので、gRPCの.protoファイルはSharedに配置する方針で進めましょう。

それではプロジェクトを作ります。

Project作成

下記コマンドでプロジェクトを作成し、必要なLibraryを追加します。
なおGrpc.Net.Client.WebGrpc.AspNetCore.Webは2020/02/18現在、バージョン指定する必要があるので要注意です。

dotnet new blazorwasm -n BlazorWASM --hosted

dotnet add ./BlazorWASM/Client/ package Grpc.Net.Client.Web --version 2.27.0-pre1

dotnet add ./BlazorWASM/Server/ package Grpc.AspNetCore
dotnet add ./BlazorWASM/Server/ package Grpc.AspNetCore.Web --version 2.27.0-pre1

dotnet add ./BlazorWASM/Shared/ package Google.Protobuf
dotnet add ./BlazorWASM/Shared/ package Grpc.Net.Client
dotnet add ./BlazorWASM/Shared/ package Grpc.Tools

各プロジェクトに追加するLibraryは以下の通りです。

  • Client

    • Grpc.Net.Client.Web Version 2.27.0-pre1
  • Server

    • Grpc.AspNetCore Version 2.27.0
    • Grpc.AspNetCore.Web Version 2.27.0-pre1
  • Shared

    • Google.Protobuf Version 3.11.4
    • Grpc.Net.Client Version 2.27.0
    • Grpc.Tools Version 2.27.0

.protoファイルの作成

weather.protoファイルをSharedフォルダへ追加します。

dotnet new proto -o .\BlazorWASM\Shared\ -n weather

Shared\BlazorWASM.Shared.csproj.protoへの参照を追加します。

<ItemGroup>
    <Protobuf Include="weather.proto" />
</ItemGroup>

.protoファイルの編集

BlazorのテンプレートでおなじみのWeatherForecastサービスのInterface定義を記載します。

Shared\weather.protoファイルの中身を以下の内容に変更します。
csharp_namespaceで指定している名前空間に注意しましょう。

syntax = "proto3";

import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";

option csharp_namespace = "BlazorWASM.Shared";

package WeatherForecast;

service WeatherForecasts {
  rpc GetWeather (google.protobuf.Empty) returns (WeatherReply);
}

message WeatherReply {
  repeated WeatherForecast forecasts = 1;
}

message WeatherForecast {
  google.protobuf.Timestamp dateTimeStamp = 1;
  int32 temperatureC = 2;
  string summary = 3;
}

GetWeather関数は引数なしなのでgoogle.protobuf.Emptyを指定していますね。

WeatherForecastクラスでは時刻を扱うためにgoogle.protobuf.Timestampを使用しています。TimeStamp型をそのまま使用するのは不便なので、packageで指定したWeatherForecastクラスをpartial化してDateTime型のDateプロパティでDateTimeStampをラップしている点がポイントですね。
※なおTimeStampについてはMicrosoft Docsのこのあたりが参考になるかと思います。

Shared\WeatherForecast.csを以下の内容に変更します。

using System;
using Google.Protobuf.WellKnownTypes;

namespace BlazorWASM.Shared
{
    public partial class WeatherForecast
    {
        public DateTime Date
        {
            get => DateTimeStamp.ToDateTime();
            set { DateTimeStamp = Timestamp.FromDateTime(value.ToUniversalTime()); }
        }

        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
}

ここで一旦dotnet buildでビルドします。
ビルドすることで.protoに定義したアセットが生成され、他のプロジェクトから.protoに定義したWeatherReplyクラスやWeatherForecastsサービスが使用できるようになります。

Server側 - WeatherForecastサービスの実装

Server\Controllersフォルダを削除しServicesフォルダを追加します。

rm Server\Controllers -Force -Recurse
mkdir Server\Services

Server\Services\WeatherForecastService.csを追加します。
自動生成されたアセットであるWeatherForecasts.WeatherForecastsBaseabstractクラスを継承し、サービスの中身を実装します。

using BlazorGrpcHosted.Shared;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;

namespace BlazorWASM.Server.Services
{
    public class WeatherForecastsService : WeatherForecasts.WeatherForecastsBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        public override Task<WeatherReply> GetWeather(Empty request, ServerCallContext context)
        {
            var reply = new WeatherReply();

            var rng = new Random();
            reply.Forecasts.Add(Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            }));

            return Task.FromResult(reply);
        }
    }
}

Server側 - gRPCサービスをMiddlewareへ追加

gRPCサービスをホストするためにServer\Startup.csを以下の内容に編集します。
※このあたりはMicrosoft Docsが参考になるかと思います。

 public void ConfigureServices(IServiceCollection services)
{
-   services.AddMvc();
+   services.AddGrpc();
    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/octet-stream" });
    });
}

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

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBlazorDebugging();
    }

    app.UseStaticFiles();
    app.UseClientSideBlazorFiles<Client.Startup>();

    app.UseRouting();
+   app.UseGrpcWeb();

    app.UseEndpoints(endpoints =>
    {
-       endpoints.MapDefaultControllerRoute();
+       endpoints.MapGrpcService<WeatherForecastsService>().EnableGrpcWeb();
        endpoints.MapFallbackToClientSideBlazor<Client.Startup>("index.html");
    });
}

Client側 - gRPC Clientをサービスに追加

Client\Program.csにgRPC Clientのサービスに登録します。

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("app");
+   builder.Services.AddSingleton(services =>
+   {
+       var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
+       var baseUri = services.GetRequiredService<NavigationManager>().BaseUri;
+       var channel = GrpcChannel.ForAddress(baseUri, new GrpcChannelOptions { HttpClient = httpClient });
+       return new WeatherForecasts.WeatherForecastsClient(channel);
+   });

    await builder.Build().RunAsync();
}

Client側 - gRPC ClientでgRPCサービスの呼び出し

Client\Pages\FetchData.razorを編集しgRPCサービスを呼び出してみます。

@page "/fetchdata"
@using BlazorWASM.Shared
- @inject HttpClient Http
+ @inject WeatherForecasts.WeatherForecastsClient WeatherForecastsClient

@* 途中省略 *@

@code {
-   private WeatherForecast[] forecasts;
+   private IList<WeatherForecast> forecasts;

    protected override async Task OnInitializedAsync()
    {
-       forecasts = await Http.GetJsonAsync<WeatherForecast[]>("WeatherForecast");
+       forecasts = (await WeatherForecastsClient.GetWeatherAsync(new Empty())).Forecasts;
    }
}

実行

Serverを起動します。

cd BlazorWASM/Server
dotnet run

gRPCのサービスへPOSTしているLogが表示されました。うまくいったようです。
Macの場合だと色々注意点があるっぽいです

f:id:piyo_esq:20200218190049p:plain
gRPCのresponseのLog

おわり

--hostedで作成したプロジェクトはASP.NET CoreがgRPCのサービスとBlazor WebAssemblyをホストする構成です。 通信が不要なBlazor WebAssemblyは静的なWebサイトでホストして、バックエンド役のgRPCサーバーはPaaSでホストしたいトコロですよね。

...ということで、次はBlazor WebAssemblyとgRPCサービスを分けたStandalone編に続きます。

おわり