Blazor ServerとASP.NETでgRPC通信

# 前書き
この記事は自分の備忘録的な感じなので、若干雑に各部分もありますがご了承ください。。。
元々React信者でC#にはまだあまり知見がないので意味不明なことを言ってる部分もあるかもです。

この記事ではASP.NET Coreを使ってgRPCサーバとBlazor Serverを通信させていろいろしよう的なアプリのひな型を作ります。ハマりポイントがいくつかあったので、そこを重点的に書きます。

一応、リポジトリもおいておくので、ぜひ活用してみてください。
https://github.com/Uta-member/gRPC-Blazor-Server-Sample/tree/main

# 利用する技術
* ASP.NET Core
* Blazor Server
* gRPC

# 環境
* Visual Studio 2022
* .NET7.0

# バックエンド側のセットアップ
まずはバックエンドの作成を行います。

## プロジェクトの作成
Visual Studio 2022を開いて「新しいプロジェクトの作成」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/7cacb122-a8b1-9f93-a018-878c895e4d56.png)
「ASP.NET Core gRPC サービス」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/7e7cb1a3-aba6-cfaa-d1b6-5b5f3bd6cb84.png)
プロジェクト名などを入れた後の画面で、フレームワークは.NET7.0を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/ad481269-0398-8949-d45e-33bbe02c1e88.png)


# フロントエンド側のセットアップ
## プロジェクトの作成
Visual Studio 2022をもう一つ立ち上げたら、「新しいプロジェクトの作成」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/ac5174dc-4758-5172-9437-a8c6d88f7c30.png)

「Blazor Server アプリ」を選択します。「Blazor Server アプリが空です」のほうを選んでもいいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/b565827f-d2d8-ee86-0a33-19e7636653f9.png)

プロジェクト名などを入力したらフレームワークは.NET 7.0、認証の種類はなしを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/7c4cce20-1e70-f7a2-7239-afb3ffea3d9b.png)

## 必要なパッケージのインストール
gRPCで通信するにはgRPCのクライアントが必要です。gRPCに必要なパッケージをBlazorのプロジェクトに追加していきます。
NuGetで以下のパッケージをインストールしてください。

* Google.Protobuf
* Grpc.Net.ClientFactory
* Grpc.Tools

## Protoファイルの配置
gRPCではProtoファイルというものにAPIの仕様を記述し、それをフロントとバックエンドで共有することによって通信を可能にしています。Protoファイルはバックエンドのほうにあるので、そのファイルをフロントにも入れておきましょう。
※プロジェクトが小規模でフロントもバックエンドも同じエンジニアがやるならProtoファイルはバックエンドとフロントの両方から簡単に参照できる位置に置いて共有しておくのがいいかもですが、いい方法がまだ浮かばないので今回はコピーする方式でいきます。この方式は大規模なプロジェクトなどでは有効だと思います。

### ファイルをコピーする
バックエンドのプロジェクトフォルダ直下にある「Protos」フォルダをフロントのプロジェクトフォルダ直下にコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/4b25ca2b-cc62-1d65-c1f2-a653bb7eec35.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/31b02d07-93b6-2b19-8181-b179aaf454ca.png)

### csprojに追記する
csprojにprotoファイルの位置を記載する必要があります。フロントのcsprojファイルに以下のように記載しましょう（csprojはソリューションエクスプローラでプロジェクト名のところをダブルクリックすると開けます）。
```csproj:BlazorApp.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Google.Protobuf" Version="3.24.3" />
        <PackageReference Include="Grpc.Net.ClientFactory" Version="2.57.0" />
        <PackageReference Include="Grpc.Tools" Version="2.58.0">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
    </ItemGroup>

    <!--追記-->
    <ItemGroup>
		<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
	</ItemGroup>
    <!--ここまで-->

</Project>
```
上記のようにprotoファイルの位置を書いてあげる必要があるので、Protoファイルが増えるたびにこの操作は必須です。

### Protoを参照できるようにする
コピーしてきたProtoファイルでは、名前空間がバックエンドのものになっています。Protoファイル側の名前空間を変えてもいいし、_Importsファイルでusingしてもいいです。今回は後者でいきます。
まずusingするにはprotoファイルをプロジェクトに入れた状態で一度ビルドしておく必要があります。ビルドすることでProtoファイルをもとにC#のファイルが裏で作られてるみたいです。
フロントエンドをビルドしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/587d8b42-1152-1660-62f7-17c6ebb13ca7.png)

ビルドできたらプロジェクト直下にある「_Imports.razor」を以下のように修正します。
```razor:_Imports.razor
@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorApp
@using BlazorApp.Shared

@*バックエンドの名前空間をusingする*@
@using GrpcService
```

### サービスを登録する
バックエンドのポート番号がプロジェクトごとにランダムで変わるので、まずはバックエンドのポートがいくつかを確認しましょう。
バックエンドのプロジェクト内のProperties/launchSettings.jsonのhttpsのapplicationUrlを確認してください。今回の画像ではhttps://localhost:7280となっているので、ここと通信するようにフロントに書けばいいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/a6c4aca4-238a-3d03-3a9d-1b3bac99180a.png)

では、今度はフロントでgRPCのクライアントを定義していきます。
gRPCのクライアントを各ページで毎回インスタンス化するのは大変なので、サービスに登録しておいてDIしましょう。フロントのProgram.csに以下のように追記してください。

```cs:Program.cs
using BlazorApp.Data;
using GrpcService;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

// 追記する
builder.Services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
    // バックエンドのlaunchSettings.jsonのhttpsのapplicationUrlに記載されていたUrlを書く
    o.Address = new Uri("https://localhost:7280");
});


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 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.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
```

これでセットアップは完了です。

# 通信を行う
セットアップが完了したので、次は実際にプログラム内で通信を行う処理を書いていきます。
Pages/Index.razorに以下のように記述してください。

```razor:Index.razor
@page "/"

@inject Greeter.GreeterClient grpcClient

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<button @onclick="CallGrpcMethod">Call gRPC Method</button>
<p>@msg</p>

@code {
    private string msg = "";

    private async Task CallGrpcMethod()
    {
        msg = "Loading...";
        var res = await grpcClient.SayHelloAsync(new HelloRequest { Name = "client" });
        msg = res.Message;
    }
}

```
@injectを書くことで、サービスに登録されているインスタンスが魔法の力で降ってきます。
記述出来たらさっそく動かしてみましょう。まずバックエンドを起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/587bdcd3-235c-01b3-f461-2f503138b4fb.png)
こんなコマンドラインが出てくればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/4361f0d8-e256-8938-bdaa-5ba7a2d043f6.png)

次にフロントを動かしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/05a895d8-cbed-c2ec-1870-409804af06b8.png)
こんな画面が出ればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/b45f0fc6-ca38-34e9-8409-a84c80b3e01e.png)

「Call gRPC Method」ボタンを押すと、gRPCサーバと通信します。
ボタンの下に「Hello client」と表示されれば通信成功です！Loading...のままだったら何かしら問題があるかと思うので、もう一度最初から読み直したりしてみてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/a7a5b935-8887-d2b4-17a5-07896b59e186.png)

# 最後に
無事Blazor ServerとASP.NET Core間でgRPC通信ができました。ちなみにこの方法はBlazor Serverでしかできず、Blazor WebAssemblyでは使えません。WebAssemblyのほうでやる場合はgRPC-Webというのを使います。
最近はReact信者になってましたが、TypeScriptだとDIが弱いのでどうしてもC#などの型がしっかりしている言語でモダンなWebが書けたらなぁと思っていたところ、Blazorを使ってみて結構好印象でした。新しい技術なので記事などがかなり少なく、まだまだ手探りですが少しずつQiitaに書いてBlazor人口が増えてくれたらなぁと思いました。
リポジトリ: https://github.com/Uta-member/gRPC-Blazor-Server-Sample/tree/main