先月の話ですが、Blazor WebAssemblyからgRPC-Webを使用する実験的サポートの発表がありました。
本エントリはBlazor WebAssemblyプロジェクトのREST通信をgRPC-Webに置き換える手順を追ってみたいと思います。 先生役はSteve Sanderson氏がGitHubで公開しているgRPC-Webのサンプルです。
目次です。
- 環境
- はじめに
- Project作成
- .protoファイルの作成
- .protoファイルの編集
- Server側 - WeatherForecastサービスの実装
- Server側 - gRPCサービスをMiddlewareへ追加
- Client側 - gRPC Clientをサービスに追加
- Client側 - gRPC ClientでgRPCサービスの呼び出し
- 実行
- おわり
環境
本エントリ作成時の環境は以下の通りです。
- .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.WebとGrpc.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.WeatherForecastsBase
abstractクラスを継承し、サービスの中身を実装します。
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の場合だと色々注意点があるっぽいです
おわり
--hosted
で作成したプロジェクトはASP.NET CoreがgRPCのサービスとBlazor WebAssemblyをホストする構成です。
通信が不要なBlazor WebAssemblyは静的なWebサイトでホストして、バックエンド役のgRPCサーバーはPaaSでホストしたいトコロですよね。
...ということで、次はBlazor WebAssemblyとgRPCサービスを分けたStandalone編に続きます。
おわり