MagicOnionでサービスから別のサービスを呼び出す

C#で`gRPC`の通信を実現するにあたって、`MagicOnion`は非常に便利なライブラリです。クライアントサイドとサーバーサイドの言語を同じにできるというのは、保守性等に限らず勉強コストや人員配置コストでもいい点がたくさんありますよね。
利用例はUnityによるゲーム開発が多いですが、私は普通の業務アプリ開発での採用を現在検討しています。

https://github.com/Cysharp/MagicOnion

使ってて特に楽しいのが１リクエストで１レスポンス返ってくるようなよくある単発のAPI（本稿では**サービス**と呼びます）の実装です。`MagicOnion`では以下のように普通のメソッドとして自然に書けます。

```c#:SampleService.cs
// サーバーとクライアントで共有する定義プロジェクトにISampleServiceを定義する
// このソースはサーバープロジェクトで実装
// クライアント側はISampleService経由でawait SampleCallAsync()して呼び出す
public class SampleService : ServiceBase<ISampleService>, ISampleService
{
    // サービスの処理本体
    // UnaryResult<T>はサービスの戻り値をとるためのTask-likeな型
    // awaitが不要な場合も多々あるので、CS1998警告はプロジェクト全体で抑圧する
    public async UnaryResult<string> SampleCallAsync()
    {
        return "Sample";
    }
}
```

ここでよくあるのが、**別のクラスとして定義したサービスをどう呼ぶの？**という疑問です。当然、役割の違うものは別クラスとして定義したいですし、すでにサービスとして定義済みのものを呼び出したくなるケースってよくありますよね？

* ログイン用に、ユーザーIDを送信して存在確認をするサービスを定義
    * フレンド登録処理で、他人のユーザーIDの存在確認処理として流用したい
* 送信データをもとに従業員の作業日報を登録するサービスを定義
    * 出勤処理、退勤処理、休憩登録処理、などいろんなところで細かく自動で日報登録したい

本稿では、あるサービスから別のサービスを呼び出す方法について紹介したいと思います。

# サービスの定義
説明のために適当なサービスを定義します。以下サンプルコードから名前空間の定義は省略します。
まず、呼び出すサービスを`IHelloWorld`とします。

```c#:IHelloWorld.cs
public interface IHelloWorld : IService<IHelloWorld>
{
    // "Hello World! (日時)"を返すメソッド
    UnaryResult<string> HelloWorldAsync();
}
```

呼び出されるサービスを`IGetServerDateTime`とします。

```c#:IGetServerDateTime.cs
public interface IGetServerDateTime : IService<IGetServerDateTime>
{
    // DBサーバーの日時を返すメソッド
    UnaryResult<DateTime> GetServerDateTimeAsync();
}
```
# 別サービスの呼び出し方
２つの関係は「`IHelloWorld`は`IGetServerDateTime`に**依存**する」という形になります。ということはつまり、**`IGetServerDateTime`を`IHelloWorld`にコンストラクタインジェクションする**というのがきれいな方法でしょう。
## 呼び出される側の具象クラスの実装
呼び出される側は普通に実装するだけです。

```c#:GetServerDateTime.cs
public class GetServerDateTime : ServiceBase<IGetServerDateTime>, IGetServerDateTime
{
    public async GetServerDateTimeAsync() => MyDatabaseManager.GetDate();
}
```
## 呼び出す側の具象クラスの実装
呼び出す側には工夫が必要です。といっても、注意点は**通信コンテキストを同じにする**という点だけです。

```c#:GetServerDateTime.cs
public class HelloWorld : ServiceBase<IHelloWorld>, IHelloWorld
{
    // コンストラクタインジェクション
    private readonly IGetServerDateTime _getServerDateTime;
    public HelloWorld(IGetServerDateTime getServerDateTime) : base()
    {
        // note: base()は自身のコンテキストにdefaultを設定しているだけ
        _getServerDateTime = getServerDateTime;
    }

    public async UnaryResult<string> HelloWorldAsync()
    {
        // コンテキストを引き継ぐ
        ((ServiceBase<IGetServerDateTime>)_getServerDateTime).Context = this.Context;
        // あとは普通に実装する
        var dt = await _getServerDateTime.GetServerDateTimeAsync();
        return $"Hello World! {dt}";
    }
}
```

重要なのが`_getServerDateTime`への`Context`プロパティの設定です。`Context`はコンストラクタ時点では`null`であり、このクラスでは実際のメソッドまで到達しないと設定されていません。そのためインジェクションしたものを使うタイミングでの設定が必要になります。
また、`Context`は`ServiceBase<T>`のプロパティなので`IService<T>`からはアクセスできません。そのためキャストをしてから設定します[^1]。

`Context`が不要なら設定せずに呼び出してもエラーにはなりませんが、そもそもそのような処理は**`MagicOnion`のサービスである必要がない**ので普通のクラスにするか`static`なユーティリティメソッドとして取り回すべきでしょう。

実際の開発時ではいちいちキャストするのは面倒くさいので以下のような拡張メソッドを定義するのが楽だと思います。`null`の場合にどうするかは要検討ですけども。

```c#:MyMagicOnionExtension.cs
public static class MagicOnionExtensions
{
    public static void SetContext<T>(this IService<T> src, ServiceContext context)
        where T : IServiceMarker
    {
        if (src is null) throw new ArgumentNullException(nameof(src));
        ((ServiceBase<T>)src).Context = context;
    }
}

```
## DIコンテナの設定
最後にDIコンテナの設定をします。`MagicOnion`のサーバーサイドをgithubに記載の手順で作っている場合、`ASP .Net Core gRPCサービス`プロジェクトになっているはずです。これには標準でDIコンテナがついていますので、`Startup.cs`の`ConfigureServices`メソッドでサービスの登録を行います。

```c#:Startup.cs
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc();
        services.AddMagicOnion();

        // services.AddTranseint(毎回生成するタイプ)で具象クラスを登録
        services.AddTransient<IGetServerDateTime, GetServerDateTime>();
    }
```

ここで、利用者側である`IHelloWorld`は**登録不要**です。
本来、`IService<T>`はクライアントから処理を呼び出したときに`MagicOnion`がうまいこと具象クラスを取り出してくれます。なのでDIコンテナに内部で登録されているのかと思いましたがそうではないようです。実際に、`AddTranseint`の行を削除すると`HelloWorldAsync`を実行するタイミングで「`IGetServerDateTime`が見つからない」という実行時例外になります。**別サービスに注入されるものは明示的にDIコンテナへの登録が必要**、ということを覚えておきましょう。
ただし、いちいちコンテナへの登録は面倒なのでSourceGeneratorを使ってコンテナへの登録コードを具象クラス定義時に自動実装するのが楽でよいと思います。

https://qiita.com/ry18847/items/15c979b67e0372c22f1d

暴論でいうとDIコンテナを使わずに`HelloWorldAsync`の中で`GetServerDateTime`クラスを`new`することもできますが、密結合になるので良くない方法なのは言うまでもありません。
# 別のサービスを呼ぶ場合の注意点
``MagicOnion``では、サービスに対して以下方法で**フィルタ**を設定することができます。

* `Startup`時に`GlobalFilter`を設定する
* クラス全体に定義したフィルタを`FromTypeFilter`または`FromServiceFilter`で定義
* メソッド個別に定義したフィルタを`FromTypeFilter`または`FromServiceFilter`で定義

ただし、今回の方法を取ると通信が発生せずに直接メソッドを実行するため、それぞれの方法で定義したフィルタは**実行されません**。フィルタで生成した値を`Context.Items`経由で使用する場合などでは適用できないので注意してください。

# まとめ
あるサービスから別のサービスを利用するには以下手順を行います。

1. 呼び出される側は普通に作る
1. **呼び出す側はコンストラクタインジェクションし、利用時に`Context`を引き継がせる**
1. `Startup.cs`でDIコンテナに呼び出される側のインターフェイスと具象クラスを登録する

なお、あるストリーミング処理から別のサービスを呼び出すこともできます。`ServerStreamingContext`および`DuplexStreamingContext`は内部で`getter-only`な`ServiceContext`プロパティを公開しているので、これを使って同じ手順で呼び出すことが可能です。
ただし、ストリーミングクラスにグループへ参加するサービスを定義したからといって、ストリーミングやサービスから別のストリーミングへの参加メソッドを呼ぶようなことはしないほうがいいでしょう。素直にクライアントから参加するほうが、グループのin-outが明確になるため、システム全体の見通しが良くなります。

[^1]: `ServiceBase<T>`でDIすると今度は`T`であるサービスインターフェイスに定義した`GetServerDateTimeAsync()`が呼べなくなる