🐥note.

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

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

前回のHosted編はBlazor WebAssemblyとgRPCサーバーが一体となった構成でした。 今回はStandalone編ということで、Blazor WebAssemblyとgRPCサーバーを分けて作ります。

お互いを分離することでBlazor Assemblyは静的なWebサイトとして置いて、gRPCサーバはPaaSに置く、そんな感じにデプロイできます。とても自然ですね。

前回同様、完成したソースコードこちらと概ね同じものになるはずです。

目次です。

環境

下記環境で作成します。

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

はじめに

Blazor WebAssemblyとgRPCサーバーを別々のプロジェクトとして作成します。

下記構成でいきましょう。

  • BlazorWASM.Client
    Blazor WebAssemblyのプロジェクト
  • BlazorWASM.Server
    ASP.NET CoreのgRPCサーバー

それではプロジェクトを作っていきましょう。

Project作成

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

dotnet new sln -n BlazorWASM
dotnet new blazorwasm -n BlazorWASM.Client
dotnet new grpc -n BlazorWASM.Server
dotnet sln BlazorWASM.sln add BlazorWASM.Client BlazorWASM.Server

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

dotnet add BlazorWASM.Client package Google.Protobuf
dotnet add BlazorWASM.Client package Grpc.Net.Client
dotnet add BlazorWASM.Client package Grpc.Net.Client.Web --version 2.27.0-pre1
dotnet add BlazorWASM.Client package Grpc.Tools

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

  • Client

    • Google.Protobuf Version 3.11.4
    • Grpc.Net.Client Version 2.27.0
    • Grpc.Net.Client.Web Version 2.27.0-pre1
    • Grpc.Tools Version 2.27.0
  • Server

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

.protoファイルの作成

weather.protoファイルをBlazorWASM.Server\Protosフォルダへ追加します。
既存のgreet.protoは削除します。

dotnet new proto -o BlazorWASM.Server\Protos -n weather
rm BlazorWASM.Server\Protos\greet.proto

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

<ItemGroup>
    <Protobuf Include="..\BlazorWASM.Server\Protos\weather.proto" />
</ItemGroup>

BlazorWASM.Server\BlazorWASM.Server.csproj.protoへの参照を追加します。 既存のgreet.protoへの参照は削除します。

  <ItemGroup>
-   <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
+   <Protobuf Include="Protos\weather.proto" GrpcServices="Server" />
  </ItemGroup>

.protoファイルの編集

BlazorWASM.Server\Protos\weather.protoにBlazorのテンプレートでおなじみのWeatherForecastサービスのInterface定義を記載します。

csharp_namespaceで指定している名前空間に注意しましょう。

syntax = "proto3";

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

option csharp_namespace = "BlazorGrpc";

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;
}

ここで一旦dotnet buildでビルドします。 まだビルドは通りませんが、ビルドすることで.protoに定義したアセットを生成します。

Server側 - WeatherForecastサービスの実装

BlazorWASM.Server\Services\GreetService.csを削除し代わりに
BlazorWASM.Server\Services\WeatherForecastService.csを追加します。

WeatherForecasts.WeatherForecastsBaseweather.protocsharp_namespaceで定義した名前空間で定義されていますのでusing BlazorGrpc;を忘れずに追加しましょう。

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using BlazorGrpc;

namespace BlazorWASM.Server
{
    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
            {
                DateTimeStamp = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(index)),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            }));

            return Task.FromResult(reply);
        }
    }
}

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

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

CORSのOriginsはhttp://localhost:54070https://localhost:44316を指定しています。
上記アドレスのアクセスのみを受け付ける、という設定ですね。

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc();
+   services.AddCors();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

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

    app.UseEndpoints(endpoints =>
    {
-       endpoints.MapGrpcService<GreeterService>();
+       endpoints.MapGrpcService<WeatherForecastsService>().EnableGrpcWeb()
+               .RequireCors(cors => cors.AllowAnyHeader().AllowAnyMethod()
+                              .WithOrigins("http://localhost:54070", "https://localhost:44316"));

-       endpoints.MapGet("/", async context =>
-       {
-           await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
-       });
    });
}

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

Client\Program.csにgRPC Clientをサービスに登録します。
gRPCのエンドポイントはhttps://localhost:5001を指定しています。
よって、実行する際はServerは上記アドレスで起動する必要がありますね。

+ using Grpc.Net.Client;
+ using Grpc.Net.Client.Web;
+ using BlazorGrpc;

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 channel = GrpcChannel.ForAddress("https://localhost:5001",
+                       new GrpcChannelOptions { HttpClient = httpClient });
+       return new WeatherForecasts.WeatherForecastsClient(channel);
+   });

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

Client側 - gRPCのメッセージクラスの定義

BlazorWASM.Client\Messages\WeatherForecast.csを作成します。
上記ファイルはgRPCの.protoで定義したクラスのうちTimeStamp型の値をClient側で扱いやすいDateTime型プロパティにラップするためのpartial classです。
名前空間.protocsharp_namespaceと同じ名前空間である点に注意が必要です。

using System;
using Google.Protobuf.WellKnownTypes;

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

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

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

Client\Pages\FetchData.razorを編集しgRPCサービスを呼び出すページを実装します。

@page "/fetchdata"
+ @using BlazorGrpc
+ @using Google.Protobuf.WellKnownTypes
- @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[]>("sample-data/weather.json");
+       forecasts = (await WeatherForecastsClient.GetWeatherAsync(new Empty())).Forecasts;
    }

-    public class WeatherForecast
-    {
-        public DateTime Date { get; set; }
-        public int TemperatureC { get; set; }
-        public string Summary { get; set; }
-        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-    }
}

実行

ClientとServer両方を起動します。

CORS関連の設定通りClientはhttps://localhost:44316で起動させます。

dotnet run --project BlazorWASM.Client --urls="https://localhost:44316"

Serverはhttps://localhost:5001で起動させます。

dotnet run --project BlazorWASM.Server --urls="https://localhost:5001"

Hosted編と同様にgRPCのサービスへPOSTしているLogが表示されました。

f:id:piyo_esq:20200218234916p:plain
gRPCへのPostのLog

おわり

これでBlazor WebAssemblyとgRPCサーバーを完全分離させる構成ができました。
あとはCloudにデプロイするだけですね…次はAzureでホストする編です!

おわり