MagicOnionでgRPC-Webを使う

# 概要
C#でAPIを立てる際、最近はMagicOnionばっかり使ってます。今までBlazorServerやWinFormsとMagicOnionサーバを通信させることが多かったんですが、最近はBlazorWebAssemblyを使用することが多くなってきました。その際に、WebAssemblyだと普通のgRPCが使えないため、MagicOnionをgRPC-Webに対応させる必要がありました。ネット上にその方法がなかなか落ちていなかったので、備忘録的な感じで残しておきます。
なお、この記事ではMagicOnionの環境がすでに整っている前提で手順を書きます。そもそもMagicOnionの使い方がわからないという方は[こちら](https://qiita.com/inco-cyber/items/3253235a0a9d5fda2b1e)の記事を参照ください。

ちなみに今回説明しているコードは以下のリポジトリにありますので、参考にしていただければと思います。
https://github.com/Uta-member/SampleProject.MO.Web

# 環境
* VisualStudio 2022 Community
* .NET8
* C# 13
* MagicOnion 7.0.2

# サーバ側の設定
サーバのサービスクラス側は特に変える必要がなく、パッケージを追加して`Program.cs`をちょっと変えれば対応できます。

## パッケージの追加
サーバ側のプロジェクトに以下のパッケージをインストールします。
* Grpc.AspNetCore.Web
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/0a7bf370-6d5c-42b5-af16-8b80c94ca47d.png)

## Program.csの修正
以下のように修正を行います。
```csharp:Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddMagicOnion();

// ここから追加 >>>
// CorePolicyを追加
// 内容はプロジェクトによって合わせてください
builder.Services
    .AddCors(
        o => o.AddPolicy(
            "AllowAll",
            builder =>
            {
                builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
            }));
// ここまで <<<

var app = builder.Build();

// ここから追加 >>>
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();

app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });

app.UseCors();
// ここまで <<<

// ミドルウェアの追加
// app.MapMagicOnionService();
app.MapMagicOnionService()
    // gRPC-webを有効化
    .EnableGrpcWeb()
    // 上で設定したCORS Policy名を指定する
    .RequireCors("AllowAll");

app.MapGet(
    "/",
    () => "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");

app.Run();
```

基本的にはCORSの設定を行いgRPC-webを有効化するだけです。

# クライアント側の設定
クライアント側もソース自体というよりはGrpcChannelのインスタンスの設定のみで対応可能です。

## パッケージの追加
クライアント側のプロジェクトに以下のパッケージをインストールします。
* Grpc.Net.Client.Web

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/1ad6cc21-8fc2-4f69-8554-7efe42c92a70.png)

## GrpcChannelの修正
今回はとりあえず最低限の動きを再現するためにコンソールアプリで実装します。
以下のようにProgram.csを記述します。
```csharp:Program.cs
using Grpc.Net.Client;
using Grpc.Net.Client.Web;
using MagicOnion.Client;
using SampleProject.MO.Web.Interface;

// gRPC-Webのクライアントを作成
var httpClient = new HttpClient(new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()));
// サーバのIPを入れる
// 実際のアプリではDIコンテナに入れて共有などを行うと思う
// オプションにgRPC-WebのHTTPクライアントを指定
GrpcChannel channel = GrpcChannel.ForAddress(
    "https://localhost:7283",
    new GrpcChannelOptions { HttpClient = httpClient });

// チャンネルを使ってサービスのクライアントを作成する
IMOUserService userService = MagicOnionClient.Create<IMOUserService>(channel);

// 実行
var userResult = userService.FindUserByUserId("userId");
var user = userResult.GetAwaiter().GetResult();

// 普通にawaitも使えます
// var user = await userService.FindUserByUserId("userId");

if(user != null)
{
    Console.WriteLine($"UserId: {user.Id}");
    Console.WriteLine($"UserName: {user.Name}");
} else
{
    Console.WriteLine("ユーザが見つかりませんでした");
}

Console.ReadLine();
```

## 実行
実行するとちゃんとサーバ側の処理を呼べているのがわかりますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/da3e4a5e-ee8b-49c3-a6e2-1a87ede8ae25.png)

実際にはコンソールではなくBlazorWebAssemblyなどで使うことになると思います。
また、これはC#に限らずReactなどのような別の言語からも呼べるはずなので、そのあたりも気力があれば記事にしていきたいと思います。

# さいごに
ネット上になかなかこの方法が落ちておらず、gRPC-Webに関してもそこまで詳しくなかったので、ちゃんとBlazorWASMで通信できるようになるまでかなり時間がかかりました。。この記事で救われる人がいれば幸いです。

ちなみにSwaggerとの共存も可能です。基本的にはgRPC-WebのミドルウェアのあとにSwaggerのミドルウェアをかまして、最後にMagicOnionのミドルウェアをかませればOKですが、そのあたりの細かい解説はMagicOnionをSwaggerでテストする方法の記事にまとめたいと思います。