【.NET8】MagicOnionでAPIサーバを立てる

# 概要
C#でAPIサーバを立てる際に、どうせクライアントもC#で作るのでC#のインターフェースを共有してうまいこと通信できないだろうかと思う瞬間がきっとあると思います（？）。そんなときに活躍するのがCySharp社が開発しているMagicOnionというライブラリです。
MagicOnionは通信にgRPCを採用しており、非常に高速な通信を行うことができます。また、C#のインターフェースを使用してコーディングするので、protoファイルの管理なども行う必要がなく非常にスムーズな開発を行うことができます。
ちなみに、gRPCだけでなく普通にRESTのサーバとしても使えるらしいので、それはまた別で記事にしようと思います。今回はシンプルにMagicOnionをAPIサーバ的な使い方で動かす方法を説明していきます。

なお、今回書いたコードは以下のリポジトリに置いておきますので、参考にしていただければと思います。
https://github.com/Uta-member/SampleProject.MO

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

# インターフェースの作成
まず、サーバとクライアント間で共有するインターフェースを定義します。このインターフェースを通して通信を行うわけですね。

## プロジェクトの準備
### プロジェクトの作成
テンプレートは`クラス ライブラリ`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/84aa7374-b023-4576-a7e5-fd37d493b259.png)

プロジェクト名は何でもいいです。なんとなくインターフェースの部分というのがわかる名前にしたほうがいいとは思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/af62dcc6-b500-4d84-b409-7cb0571b72b4.png)

### パッケージのインストール
インターフェースのプロジェクトには以下のパッケージをインストールします
* MagicOnion.Abstractions

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/e90df36c-4b40-4445-80fc-3b622dcc497c.png)

### フォルダの作成
別に必須ではありませんが、`Models`、`Services`というフォルダを作成して分けるといいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/4335b361-1e0d-4543-abd6-40e3ae687347.png)

## モデルの作成
データをやり取りする際に使用するクラスを`Models`に作成していきます。
とりあえずユーザのIDと名前を持ったクラスを作成してみます。
```csharp:MPUser.cs
using MessagePack;

namespace SampleProject.MO.Interface
{
    // MagicOnionの通信で使用するクラスには必ずMessagePackObject属性を付ける
    [MessagePackObject]
    public sealed record MPUser
    {
        // プロパティにはKey属性を付けて、0から順番に数字を振っていく
        
        [Key(0)]
        public required string Id { get; set; }

        [Key(1)]
        public required string Name { get; set; }
    }
}
```
頭のMPはMessagePackのクラスというのを明示したくてつけてます。ドメインオブジェクトとかと混在したら嫌なので。。
`sealed record`は筆者が好んで使っているだけなので、別に普通の`class`でも大丈夫です。`required`もつけておいたほうがコーディングは楽になることが多いので、必須のプロパティにはつけてます。

## サービスの作成
モデルができたのでこれを使ってインターフェースを`Services`フォルダに作成してみましょう。

```csharp:IMOUserService.cs
using MagicOnion;

namespace SampleProject.MO.Interface
{
    public interface IMOUserService : IService<IMOUserService>
    {
        UnaryResult<MPUser?> FindUserByUserId(string userId);

        UnaryResult RegisterUser(MPUser user);
    }
}
```

MOが頭についているのはMagicOnionのサービスというのを明示するためになんとなくつけてるだけです。なくても大丈夫です。サービスのインターフェースは必ず`IService`インターフェースを実装し、型引数に自分自身を指定します。MagicOnionのメソッドは必ず戻り値をUnaryResultにします。また、引数や戻り値に設定できるのはC#のプリミティブ型とMessagePackObject属性のついたクラスだけです。ちなみに、クラス自体にMessagePackObject属性がついていても、そのクラスのプロパティにMessagePackObject属性がついていないクラスを使用してしまうのもダメなので、やり取りに使用されるクラスはすべてMessagePackObject属性を付ける必要があります。

これでインターフェース側の準備は完了です。

# サーバの実装
サーバ側の実装を行っていきます。まずはプロジェクトの準備を行いましょう。

## プロジェクトの準備
### プロジェクトの作成
プロジェクトのテンプレートには`ASP.NET Core gRPC サービス`を使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/ed6fb765-b172-4801-b57d-3936c8501826.png)

プロジェクト名は何でもOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/cbc03528-5cd2-41d6-9dad-8abe9017edee.png)

フレームワークは`.NET 8.0`を指定して作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/bf2a78e3-70bf-4222-b399-0e72e22ab175.png)

### 参照の設定
先ほど作成したインターフェースのプロジェクトを参照します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/99993f9e-a981-49ea-8de5-d50cbfcb1871.png)

### パッケージの追加
以下のパッケージをインストールします。
* MagicOnion.Server

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/70077134-dcf5-4cb7-a9d3-f1b3b3a38b68.png)

### 不要なフォルダとファイルを削除
`Protos`フォルダ自体と`Services`フォルダ内のファイルをすべて削除します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/4e6a7acb-45d5-4a08-9e33-310597d2f271.png)

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

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

var app = builder.Build();

app.MapMagicOnionService();

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();

```

## サービスの実装
インターフェースのプロジェクトに作成したサービスのインターフェースを実装します。実装クラスは`Services`フォルダに作成しましょう。
以下のように、ServiceBaseを継承してインターフェースを実装すればOKです。ServiceBaseの型引数には実装するサービスのインターフェースを渡してください。UnaryResultはTaskなどと同じく非同期を扱えるクラスなので、メソッド内でawaitなども使用できます。
```csharp:MOUserService.cs
using MagicOnion;
using SampleProject.MO.Interface;

namespace SampleProject.MO.APIServer.Services
{
    public sealed class MOUserService : ServiceBase<IMOUserService>, IMOUserService
    {
        public UnaryResult<MPUser?> FindUserByUserId(string userId)
        {
            // DBなどから情報を取得して返す
            // これはサンプルなので適当にコンストラクタを呼んで返してます
            return UnaryResult.FromResult<MPUser?>(
                new MPUser() 
                { 
                    Id = userId,
                    Name = "ユーザの名前",
                });
        }

        public UnaryResult RegisterUser(MPUser user)
        {
            // DBに登録をする
            // これはサンプルなので何もしてません
            return UnaryResult.CompletedResult;
        }
    }
}
```

サーバ側の実装はこれで完了です。

# クライアントの実装
今回はとりあえず簡単に実装したいのでコマンドラインアプリで実装しますが、Blazorのサーバ側やWinForms、MAUI、WPFなどでも使用できます。ちなみに、BlazorWebAssemblyなどのようなブラウザ上で動くコードではそのまま使えません。ブラウザ上で動かすためにはgRPC-Webというものを使用する必要があり、サーバ側にも設定が必要なので、それはまた別の記事で解説しようと思います。

## プロジェクトの準備
### プロジェクトの作成
今回はコンソールアプリを作成したいので、テンプレートは`コンソール アプリ`を使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/1fe6f4e5-1cb7-4851-a01e-6f9d11864701.png)

プロジェクト名は何でもOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/76b78310-c740-4a93-8901-14941f0055ea.png)

フレームワークは`.NET 8.0`を選択して作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/26a2c604-a8e5-4d0c-ad6c-e0df0fb7372c.png)

### プロジェクト参照
インターフェースのプロジェクトを参照に追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/d7c5f1f1-c346-437d-bef5-fabde75b8311.png)


### パッケージの追加
以下のパッケージをインストールします。
* MagicOnion.Client

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/5c44daa1-a2a5-4011-a7fc-8fa7131c1ba3.png)


## APIの呼び出し
サーバのIPアドレスが必要なので、サーバ側のプロジェクトの`Properties\launchSettings.json`を開き、IPアドレスを確認しておきましょう。
`applicationUrl`のhttpsのほうです。httpで動かす場合はhttpのほうを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/97438673-c789-49b1-a830-bcaac2cec101.png)

クライアント側の`Program.cs`を以下のように変更します。
```csharp:Program.cs
using Grpc.Net.Client;
using MagicOnion.Client;
using SampleProject.MO.Interface;

// サーバのIPを入れる
// 実際のアプリではDIコンテナに入れて共有などを行うと思う
GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:7022");

// チャンネルを使ってサービスのクライアントを作成する
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();
```

# 実行してみる
### 準備
念のため書いておきますが、VisualStudioで複数のプロジェクトを一気にデバッグするには、[ソリューションを右クリック] - [スタートアップ プロジェクトの構成]を開き、マルチスタートアッププロジェクトで立ち上げたいプロジェクトのアクションを`開始`にしておくことで複数起動ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/3f7c949f-fff9-422f-84f3-31f07d725e5f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/feb7a8b3-7966-4d51-b825-4c3e6b0d4baf.png)

### 実行
実行するとこんな感じでちゃんとサーバ側で書いたモデルの情報が返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2524944/bacf1a71-6f2b-43fc-ab10-3b0962ca3494.png)

# 最後に
以上、MagicOnionで最低限通信を行うための方法でした。非常に高速で動作しますし通信のインターフェースがそのままC#のインターフェースなのでコーディングも非常に簡単で型安全ですね。
ただ、実際に大きめのプロジェクトで採用していますが、以下のような問題点があります。
* MessagePackObject属性書き忘れやすい（現状、LINTとかで頑張る？）
* OptionalやImmutableのようなよく使うけどプリミティブではない汎用的なクラスをそのまま使うとエラーになるので、それらもMessagePackObjectにしないといけない（シリアライザが存在していたりはするのでそれらを活用する）
* テストがRESTほど気楽にできない（Swaggerでデバッグできる環境を作れるパッケージが公式から出ているので、それで対応する）
* そのままだとブラウザ上で動作するコードから呼び出せない（gRPC-Webで通信すればできる）
* gRPCを使いたくないクライアントもある（RESTに変換できるパッケージがある）

### 関連リンク
* gRPC-WebをMagicOnionで使う方法: https://qiita.com/inco-cyber/items/74715318a7f40d819d64

MagicOnionはCySharpがゲーム開発で使用するために開発したものなのでゲーム開発向けとして書かれている記事が多い気がしますが、C#のAPIは基本的にこれでいいと感じるほどよくできているパッケージなので、ぜひ人口が増えたらうれしいなと思っています