前回のHosted編はBlazor WebAssemblyとgRPCサーバーが一体となった構成でした。 今回はStandalone編ということで、Blazor WebAssemblyとgRPCサーバーを分けて作ります。
お互いを分離することでBlazor Assemblyは静的なWebサイトとして置いて、gRPCサーバはPaaSに置く、そんな感じにデプロイできます。とても自然ですね。
前回同様、完成したソースコードはこちらと概ね同じものになるはずです。
目次です。
- 環境
- はじめに
- Project作成
- .protoファイルの作成
- .protoファイルの編集
- Server側 - WeatherForecastサービスの実装
- Server側 - gRPCサービスをMiddlewareへ追加
- Client側 - gRPC Clientをサービスに追加
- Client側 - gRPCのメッセージクラスの定義
- 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
はじめに
Blazor WebAssemblyとgRPCサーバーを別々のプロジェクトとして作成します。
下記構成でいきましょう。
- BlazorWASM.Client
Blazor WebAssemblyのプロジェクト - BlazorWASM.Server
ASP.NET CoreのgRPCサーバー
それではプロジェクトを作っていきましょう。
Project作成
下記コマンドでプロジェクトを作成し、必要なLibraryを追加します。
前回同様Grpc.Net.Client.WebとGrpc.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.WeatherForecastsBase
はweather.proto
のcsharp_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:54070
とhttps://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です。
名前空間は.proto
のcsharp_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が表示されました。
おわり
これでBlazor WebAssemblyとgRPCサーバーを完全分離させる構成ができました。
あとはCloudにデプロイするだけですね…次はAzureでホストする編です!
おわり