ASP.NETのDIコンテナでファクトリを扱いたい

しばしば、DBやObjectStorageなど外部のサービスに依存したコードを書くことがあります。
扱うサービスがそれぞれ１つだけなら固定でも良いのですが、リクエストに応じて接続先を変えたいといった場合はどうでしょうか。

こういった時は「Factory Method」パターンを採用し、Factoryに「リクエスト」を与えると「適切な接続先をもったサービス」を返すといった感じのコードが書けます。必要な情報を渡せばよしなにしてくれるわけです。

ASP.NETのDIコンテナで扱う場合は、愚直にサービスを返すFactoryクラスを書いてFactoryクラスをサービスとして登録すればよいのですが、このFactoryクラスを書くのが億劫で楽できないか探したところ、StackOverflowの回答にやりたいことがありました。

https://stackoverflow.com/a/44177920

使っているメソッドは以下です。

https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.extensions.dependencyinjection.servicecollectionserviceextensions.addsingleton?view=dotnet-plat-ext-5.0#Microsoft_Extensions_DependencyInjection_ServiceCollectionServiceExtensions_AddSingleton__1_Microsoft_Extensions_DependencyInjection_IServiceCollection_System_Func_System_IServiceProvider___0__

このメソッド自体は「サービス」を登録するための「サービスを返す関数」を与えるといったメソッドになっていて、結局登録されるのは「サービス」になります。なので今回やりたいことの趣旨とはちょっと違います。

Stackoverflowの回答の内容このメソッドを使うのですが、「サービス」を登録するのではなく代わりに「サービスを返す関数（ファクトリ）」を登録しています。これが「Factoryクラスをサービスとして登録する」に当たります。なので、このメソッドには「サービスを返す関数」を登録するために「サービスを返す関数を返す関数（ファクトリを返す関数）」を与えています。このときにデリゲート型で「サービスを返す関数」を定義しており、いい感じに書いています。

後はASP.NETのDIコンテナ部分以外でやっていることは、コンストラクタ引数では「サービス」の代わりに「サービスを返す関数」を受け取るようにするだけです。

説明が下手で変ですが、コードを見ればわかるかもしれません。やっていきましょう


### 1. Serviceクラスを用意する

例えば適当なSSHClientをラップしたSSHClientServiceを作るとしましょう。SSHに接続してコマンド実行して標準出力・標準エラー出力を返すイメージです（SSHClientServiceクラスの実装は手抜きです。実際はExecuteを実行するとネットワーク通信が発生するものと考えてください。）


``` SSHClientService.cs
using System;

namespace Example.Services
{
    /// サービスを返す関数の定義
    public delegate ISSHClientService SSHClientServiceResolver(string hostname, string username, string password, int port = 22);

    /// サービスのInterface
    public interface ISSHClientService : IDisposable
    {
        (string, string) Execute(params string[] command);
    }

    /// サービス
    public class SSHClientService : ISSHClientService
    {
        private string hostname;
        private string username;
        private string password;
        private int port;

        public SSHClientService(string hostname, string username, string password, int port = 22)
        {
            this.hostname = hostname;
            this.username = username;
            this.password = password;
            this.port = port;
            // TODO: SSH Client Connect
        }

        public void Dispose()
        {
            // TODO: SSH Client Disconnect
        }

        public (string, string) Execute(params string[] command)
        {
            var stdout = $"{username}@{hostname}:{port}: {string.Join(" ", command)}";
            return (stdout , "");
        }
    }
}
```

ポイントはdelegate型の`SSHClientServiceResolver`を定義している点です。これは引数を4つ受け取って適切なISSHClientServiceを返す関数を定義していることになります。後でDIコンテナに登録するときに使います。

SSHClientぐらい必要になったらnewすれば良いという気持ちに負けそうになりますが、テストコードが書きにくくなって詰むのでこうしています。

### 2. Startup.csでサービス登録する

ASP.NETのDIのお作法に従いサービスを登録します。この時に型変数に先ほどのデリゲート型を指定し、デリゲート型に合う関数を与えます。

``` Startup.cs
namespace Example
{
    public class Startup
    {
        // 中略
        public void ConfigureServices(IServiceCollection services)
        {
            // 中略
            services.AddSingleton<SSHClientServiceResolver>((serviceProvider => (hostname, username, password, port) =>
            {
                return new SSHClientService(hostname, username, password, port);
            }));
        }
    }
}
```

「サービスを返す関数(ファクトリ=デリゲート型のSSHClientServiceResolver)」を登録するために「サービスを返す関数を返す関数（ファクトリを返す関数）」を実装しています。
`(serviceProvider => (hostname, username, password, port) => ...` とラムダ式を２つ重ねており「サービスを返す関数を返す関数」を実現しています。

もし別のサービスに依存する場合はserviceProviderをうまく使います。今回はサンプルなので使っていませんが、例えば今は引数で接続先を取っていますが、これを引数にはIDなどの識別情報だけを渡す形にしてDBにSELECTしてSSHの接続先を取ってくる場合、`serviceProvider.GetService<SSHInfoRepository>()`といった感じで取得して使うことになるでしょう。

スコープは今回はSingletonになっていますが、ServiceProviderの都合に応じて変える必要が出るかと思います。（今回はServiceProviderを使わないのでSingletonで良いはずです）

### 3. Controllerでの利用例

使い方は以下です。今回はASP.NET MVCのControllerですが、WebJobs等を使う場合も同様です。


``` ExampleController.cs
namespace Example.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ExampleController: ControllerBase{
        private SSHClientServiceResolver sshClientServiceResolver;

        public ExampleController(SSHClientServiceResolver sshClientServiceResolver){
            this.sshClientServiceResolver = sshClientServiceResolver;
        }
        [HttpGet]
        public IActionResult Get(){
            using var sshClientService = sshClientServiceResolver("localhost", "fukasawah", "*****");
            var (stdout, stderr) = sshClientService.Execute("ls");
            return new OkObjectResult(stdout);
        }
    }
}
```

コンストラクタでサービス登録に使ったデリゲート型の値を受け取り、インスタンス変数に持っておきます。あとは必要な時に引数を与えてISSHClientServiceを得るだけです。
これで、リクエストに応じてサービスを生成することができるようになります。（今回は手を抜いて固定値にしています。）

作りによりますが、今回はサービスのリソース解放は自身で行う必要があります。今回でDIコンテナが管理しているのは「サービスを返す関数（ファクトリ）」で「サービス」ではないからです。なので生成した「サービス」は自分で始末する必要があります。

ただ、ファクトリで返すサービスがserviceProviderで得たサービスを返している場合は、DIコンテナのライフサイクルに従うはずなので不要です。StackOverflowの回答はこちらに該当します。


### 4. テストコード例

Moqを使ったテスト例は以下です。モックを返すファクトリを与えるだけです。もちろん引数に応じて返すモックを変えたりもできます。簡単ですね。

``` ExampleControllerTest.cs
using Example.Controllers;
using Example.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Example.Test.Controllers
{
    [TestClass]
    public class ExampleControllerTest
    {
        [TestMethod]
        public void TestGet()
        {
            var sshClientServiceMock = new Mock<ISSHClientService>();
            sshClientServiceMock.Setup(m => m.Execute("ls")).Returns(("FOO.txt", ""));
            SSHClientServiceResolver resolver = (_1, _2, _3, _4) => sshClientServiceMock.Object; // モックを返すファクトリ

            var controller = new ExampleController(resolver);
            var result = controller.Get() as OkObjectResult;

            Assert.AreEqual("FOO.txt", result.Value);
        }
    }
}
```