🔖

.NET 8 の Blazor で WASM + gRPC のプロジェクトを作る

2024/05/25に公開
3

更新履歴

  • 2024/05/25 初版
  • 2024/07/27 コメントでの指摘事項を反映

本文

この記事は、.NET 8 の Blazor で WASM + gRPC のプロジェクトを作る方法を紹介します。
基本的には、以前書いた.NET 8 の Blazor で WASM + API のプロジェクトを作るの記事と同じですが、gRPC に変更する部分を主に紹介します。

プロジェクトの作成

ここら辺は基本的に前回と同じなのでさくっと説明します。Blazor Web App で WebAssembly を Global で有効になるようにしてプロジェクトを作成します。

そして Routes.razor を以下のようにして静的 SSR の時には Loading... と表示するようにします。

Routes.razor
@if (!OperatingSystem.IsBrowser())
{
    <div>Loading...</div>
}
else
{
    <Router AppAssembly="typeof(Program).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
            <FocusOnNavigate RouteData="routeData" Selector="h1" />
        </Found>
    </Router>
}

gRPC サービスの追加

gRPC ですが .proto ファイルを作成してやるのが標準の方法ですが今回はコードファーストでやっていこうと思います。コードファーストでやる方法は、以下のドキュメントに詳細があるので、そちらもあわせて参考してください。

MyGrpcService というクラスライブラリプロジェクトを作成して以下のライブラリを追加します。

  • protobuf-net.Grpc

そして、サービスのコントラクトを定義していきます。今回はありきたりな挨拶を返すサービスを作成します。

GreetingsService.cs
using ProtoBuf.Grpc;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace MyGrpcService;

[ServiceContract]
public interface IGreetingService
{
    [OperationContract]
    Task<HelloReply> SayHello(HelloRequest request, CallContext context = default);
}

[DataContract]
public class HelloReply
{
    [DataMember(Order = 1)]
    public string Message { get; set; } = "";
}
[DataContract]
public class HelloRequest
{
    [DataMember(Order = 1)]
    public string Name { get; set; } = "";
}

そして Blazor Web App のサーバー側のプロジェクトに MyGrpcService プロジェクトと以下のライブラリを追加してサービスの実装を行います。

  • protobuf-net.Grpc.AspNetCore
  • Grpc.AspNetCore.Web (gRPC-Web で呼び出すために必要)

そして、サービスの実装を適当に行います。今回はありきたりなハローワールド的なものを以下のように実装しました。

Services/GreetingService.cs
using MyGrpcService;
using ProtoBuf.Grpc;

namespace BlazorWasmAndGrpcSingleProjectSample.Services;

public class GreetingService : IGreetingService
{
    public Task<HelloReply> SayHello(HelloRequest request, CallContext context = default) =>
        Task.FromResult(new HelloReply { Message = $"Hello, {request.Name}!" });
}

最後に Program.cs に以下のようにして gRPC エンドポイントとサービスを追加します。さらに、今回は Blazor WebAssembly から呼び出すので gRPC-Web で呼び出せるようにしています。

Program.cs
using BlazorWasmAndGrpcSingleProjectSample.Client.Pages;
using BlazorWasmAndGrpcSingleProjectSample.Components;
using BlazorWasmAndGrpcSingleProjectSample.Services;
using ProtoBuf.Grpc.Server;

var builder = WebApplication.CreateBuilder(args);

// gRPC サービスを追加
builder.Services.AddCodeFirstGrpc();
// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveWebAssemblyComponents();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

// gRPC のサービスの追加
app.UseGrpcWeb();
app.MapGrpcService<GreetingService>().EnableGrpcWeb();

app.MapRazorComponents<App>()
    .AddInteractiveWebAssemblyRenderMode()
    .AddAdditionalAssemblies(typeof(BlazorWasmAndGrpcSingleProjectSample.Client._Imports).Assembly);

app.Run();

追加した部分だけのコードを抜粋すると以下の2か所になります。

// gRPC サービスを追加
builder.Services.AddCodeFirstGrpc();
// gRPC のサービスの追加
app.UseGrpcWeb();
app.MapGrpcService<GreetingService>().EnableGrpcWeb();

これで GreetingService が gRPC で呼び出せるようになりました。

gRPC クライアントの追加

では最後に Blazor WebAssembly のプロジェクトから gRPC を呼び出すようにします。まずは、MyGrpcService プロジェクトと以下のパッケージを Blazor WebAssembly のプロジェクトに追加します。

  • Grpc.Net.Client
  • Grpc.Net.Client.Web
  • System.ServiceModel.Primitives

そして、Program.cs に以下のようにして gRPC クライアントを追加します。

Program.cs
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using ProtoBuf.Grpc.Configuration;
using MyGrpcService;
using Grpc.Net.Client.Web;
using Grpc.Net.Client;
using ProtoBuf.Grpc.Client;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

// クライアントを追加
builder.Services.AddSingleton(sp =>
{
    var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
    return GrpcChannel.ForAddress(
        builder.HostEnvironment.BaseAddress,
        new GrpcChannelOptions
        {
            HttpHandler = httpHandler,
        });
});
builder.Services.AddTransient<IGreetingService>(sp =>
{
    var channel = sp.GetRequiredService<GrpcChannel>();
    return channel.CreateGrpcService<IGreetingService>();
});

await builder.Build().RunAsync();

これで gRPC クライアントが追加されました。あとは、Blazor のコンポーネントから呼び出すだけです。
Home.razor を以下のようにして実際に呼び出してみましょう。

Home.razor
@using MyGrpcService
@page "/"
@inject IGreetingService GreetingService

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

<div>
    <label>
        Name:
        <input @bind="Name" />
    </label>
</div>
<div>
      <button @onclick="SayHelloAsync">Say Hello</button>
</div>
<div>
    @Message
</div>

@code {
    private string Name { get; set; } = "";
    private string Message { get; set; } = "";
    private async Task SayHelloAsync()
    {
        var response = await GreetingService.SayHello(new HelloRequest { Name = Name });
        Message = response.Message;
    }
}

実行して、表示された画面のテキストボックスに適当な名前を入力して Say Hello ボタンを押すと、gRPC でサービスが呼び出されて以下のように表示されます。

まとめ

ということで、.NET 8 の Blazor で WASM + gRPC のプロジェクトを作る方法をやってみました。gRPC はコードファーストで実装して .proto を書かない方法で試しました。普通の .proto を使う方法でも、ほぼ同じように作れると思います。

コードは、この記事を書くにあたって作ったプロジェクトを GitHub に置いておきますので、興味があれば参考にしてみてください。

https://github.com/runceel/BlazorWasmAndGrpcSingleProjectSample

Microsoft (有志)

Discussion

hojomhojom

いつも参考になる記事をありがとうございます。
質問なのですが、
GreetingsService.csのHelloReply,HelloRequestがrecordになっていますが、
私の環境ではそれだとうまく動きませんでした。
「Your WASM app is getting ready to be used by Rider debugger, please stand by…」のまま進まない。
GitHubのコードの方を確認したところ、
そちらはclassになっており、
classに変更すると動くようになりました。
recordだとうまく動かない理由があったりしますでしょうか?

Kazuki OtaKazuki Ota

すいません。
検証できていないのですが、もしかしたら記事を書く際に間違えて record にしてしまったのかもしれません。

Kazuki OtaKazuki Ota

@hojom さん
取り急ぎこちらでも検証してみた結果 record では動きませんでした。
記事のコードをリポジトリのものにあわせるように変更しました。対応が遅くなり申し訳ありません。

原因ですが恐らく Protocolbuf の方が record に対応していないのではないかと思います。
ありがとうございました。