C# 汎用ホストのDIにデータベースプロバイダを組み込む

## このドキュメントの内容

任意の機能を汎用ホストのDIに組み込む例です。ADO.NET データベースプロバイダを例に挙げて説明します。

## 要件

今回は次の要件を満たすものを考えます。

* SqlDb, OleDb, Sqlite, ODP.NET など任意の種類の ADO.NET データベースプロバイダを扱えること。特定のプロバイダに依存しないこと。
* 構成ファイルでデータベースプロバイダの種類や接続文字列を設定できること。
* 複数の接続先を扱えること。

## 実装方針

* データベースプロバイダの種類は文字列で表すものとします。
* 複数の接続先の設定をコレクションとして保持するオプションクラスを実装し、構成ファイルから読み込むようにします。
* そのオプションクラスのインスタンスをDIから受け取り、コネクションを生成するファクトリクラスを実装します。このファクトリクラスのインスタンスもDIで管理します。
* データベースプロバイダ固有の実装はアプリケーション側に委譲します。

## 実装したコード

### DbProviderOptions クラス

データベースプロバイダのオプション。複数の接続先の設定を持ちます。

```c#:DbProviderOptions.cs
/// <summary>
/// データベースプロバイダのオプション。
/// </summary>
public class DbProviderOptions
{
    /// <summary>
    /// 構成セクションのキー。
    /// </summary>
    public const string ConfigurationSectionKey = "mxProject:DbProvider";

    /// <summary>
    /// 既定のコネクションを取得または設定します。
    /// </summary>
    public DbConnectionOptions? Default { get; set; }

    /// <summary>
    /// 追加のコネクションを取得または設定します。
    /// </summary>
    public Dictionary<string, DbConnectionOptions>? Additionals { get; set; }
}
```

### DbConnectionOptions クラス

コネクションを生成するために必要な設定値を持ちます。

```c#:DbConnectionOptions.cs
/// <summary>
/// データベースコネクションのオプション。
/// </summary>
public class DbConnectionOptions
{
    /// <summary>
    /// プロバイダの種類を取得または設定します。
    /// </summary>
    public string? ProviderType { get; set; }

    /// <summary>
    /// 接続文字列を取得または設定します。
    /// </summary>
    public string? ConnectionString { get; set; }

    /// <summary>
    /// 接続タイムアウトを取得または設定します。
    /// </summary>
    public int? ConnectionTimeout { get; set; }

    /// <summary>
    /// 任意のプロパティを取得または設定します。
    /// </summary>
    public Dictionary<string, string?>? Properties { get; set; }
}
```

### appsettings.json

DbProviderOptions クラスに対応する構成データ。json で指定する場合は次のように記述します。

* DbProviderOptions.ConfigurationSectionKey の値は "mxProject:DbProvider" です。これに合わせます。

```json
{
  "mxProject": {
    "DbProvider": {
      "Default": {
        "ProviderType": "sqlite",
        "ConnectionString": "data source=:memory:",
        "ConnectionTimeout": 30,
        "Properties": {
          "SqlTrace": true
        }
      },
      "Additionals": {
        "Custom1": {
          "ProviderType": "SqlDb",
          "ConnectionString": "接続文字列2"
        }
      }
    }
  }
}
```






### IDbProviderFactory インターフェース

ファクトリの機能を表すインターフェース。この型をDIに登録します。

```c#:IDbProviderFactory.cs
/// <summary>
/// データベースプロバイダオブジェクトの生成に必要な機能を提供します。
/// </summary>
public interface IDbProviderFactory : IDisposable
{
    /// <summary>
    /// 既定のコネクションを生成します。
    /// </summary>
    /// <returns>コネクション</returns>
    IDbConnection CreateDefaultConnection();

    /// <summary>
    /// 指定された名前に対応するコネクションを生成します。
    /// </summary>
    /// <param name="name">名前</param>
    /// <returns>コネクション</returns>
    IDbConnection CreateAdditionalConnection(string name);
}
```

### DbProviderFactory クラス

IDbProviderFactory インターフェースの実装。

* DbProviderOptions のインスタンスはDIから受け取ります。構成ファイルが変更されたときに最新の値を参照できるようにするため IOptionsMonitor を使用しています。
* コネクションを生成するメソッドを受け取ります。アプリケーションコードから受け取ることを想定しています。

基底クラス DbProviderFactoryBase の実装は畳んでおきます。
<details><summary>DbProviderFactoryBase.cs</summary>

```c#:DbProviderFactoryBase.cs
/// <summary>
/// データベースプロバイダの基底実装。
/// </summary>
public abstract class DbProviderFactoryBase : IDbProviderFactory
{
    /// <summary>
    /// インスタンスを生成します。
    /// </summary>
    /// <param name="options">オプション</param>
    protected DbProviderFactoryBase(IOptionsMonitor<DbProviderOptions> options)
    {
        m_Options = options;
    }

    /// <summary>
    /// ファイナライザ
    /// </summary>
    ~DbProviderFactoryBase()
    {
        Dispose(false);
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// 使用しているリソースを解放します。
    /// </summary>
    /// <param name="disposing">Dispose メソッドから呼び出されたかどうか</param>
    protected virtual void Dispose(bool disposing)
    {
    }

    /// <summary>
    /// オプションを取得します。
    /// </summary>
    protected DbProviderOptions Options
    {
        get { return m_Options.CurrentValue; }
    }

    private readonly IOptionsMonitor<DbProviderOptions> m_Options;

    #region コネクション生成

    /// <inheritdoc/>
    public IDbConnection CreateDefaultConnection()
    {
        if (Options.Default == null)
        {
            throw new NullReferenceException("既定のプロバイダの設定が見つかりません。");
        }

        return CreateConnection(Options.Default);
    }

    /// <inheritdoc/>
    public IDbConnection CreateAdditionalConnection(string name)
    {
        DbConnectionOptions? options = null;

        Options.Additionals?.TryGetValue(name, out options);

        if (options == null)
        {
            throw new NullReferenceException($"指定されたプロバイダの設定が見つかりません。プロバイダ名：{name}");
        }

        return CreateConnection(options);
    }

    /// <summary>
    /// コネクションを生成します。
    /// </summary>
    /// <param name="options">オプション</param>
    /// <returns>コネクション</returns>
    protected abstract IDbConnection CreateConnection(DbConnectionOptions options);

    #endregion
}
```
</details>

```c#:DbProviderFactory.cs
/// <summary>
/// データベースプロバイダを生成するファクトリ。
/// </summary>
public class DbProviderFactory : DbProviderFactoryBase
{
    /// <summary>
    /// インスタンスを生成します。
    /// </summary>
    /// <param name="options">オプション</param>
    /// <param name="connectionActivator">コネクションを生成するメソッド</param>
    public DbProviderFactory(
        IOptionsMonitor<DbProviderOptions> options,
        Func<DbConnectionOptions, IDbConnection> connectionActivator
    ) : base(options)
    {
        m_ConnectionActivator = connectionActivator;
    }

    private readonly Func<DbConnectionOptions, IDbConnection> m_ConnectionActivator;

    /// <inheritdoc/>
    protected override IDbConnection CreateConnection(DbConnectionOptions options)
    {
        return m_ConnectionActivator(options);
    }
}
```

### アプリケーションコード

「ワーカーサービス」プロジェクトテンプレートを使用しました。DbProviderOptions クラスと IDbProviderFactory インターフェースをDIに登録しています。

```c#:Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        var builder = Host.CreateDefaultBuilder(args);

        builder.ConfigureServices((context, services) =>        {
            // DbProviderOptions をサービスに登録します
            services.Configure<DbProviderOptions>(
                context.Configuration.GetSection(DbProviderOptions.ConfigurationSectionKey)
                );

            // IDbProviderFactory をサービスに登録します
            services.AddSingleton<IDbProviderFactory>(service =>
            {
                // DIから DbProviderOptions のインスタンスを取得します
                var options = service.GetRequiredService<IOptionsMonitor<DbProviderOptions>>();

                // プロバイダの種類に応じたコネクションを生成するメソッド
                IDbConnection activator(DbConnectionOptions options)
                {
                    switch (options.ProviderType)                    {
                        case "sqlite":
                            return new System.Data.SQLite.SQLiteConnection(options.ConnectionString);

                        default:
                            throw new NotSupportedException();
                    }
                }

                return new DbProviderFactory(options, activator);
            });
        });

        builder.ConfigureServices(services =>
        {
            services.AddHostedService<Worker>();
        });

        var host = builder.Build();

        host.Run();
    }
}
```

プロジェクトテンプレートで定義されているワーカークラス。コンストラクタ引数に IDbProviderFactory を追加しています。DIで管理されるシングルトンインスタンスが渡されます。

```c#:Worker.cs
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IDbProviderFactory _DbProvider;

    public Worker(
        ILogger<Worker> logger,
        IDbProviderFactory dbProvider
    )
    {
        _logger = logger;
        _DbProvider = dbProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // このようにコネクションを生成することができます
        using var connection = _DbProvider.CreateDefaultConnection();
    }
}
```

### 拡張メソッドを実装する

前述のDI関連の処理を IHostBuilder に対する拡張メソッドとして実装しておくと、アプリケーションコードの実装量を減らすことができます。

```c#
/// <summary>
/// <see cref="IHostBuilder"/> に対する拡張メソッド。
/// </summary>
public static class HostBuilderExtensions
{
    /// <summary>
    /// データベースプロバイダを構成します。
    /// </summary>
    /// <param name="hostBuilder"></param>
    /// <param name="connectionActivator">コネクションを生成するメソッド</param>
    /// <returns><paramref name="hostBuilder"/> に指定されたインスタンス自身</returns>
    public static IHostBuilder CondigureDbProvider(
        this IHostBuilder hostBuilder,
        Func<IServiceProvider, DbConnectionOptions,
        IDbConnection> connectionActivator
    )
    {
        return hostBuilder.ConfigureServices((context, services) =>
        {
            // DbProviderOptions をサービスに登録します
            services.Configure<DbProviderOptions>(
                context.Configuration.GetSection(DbProviderOptions.ConfigurationSectionKey)
                );

            // IDbProviderFactory をサービスに登録します
            services.AddSingleton<IDbProviderFactory>(service =>
            {
                var options = service.GetRequiredService<IOptionsMonitor<DbProviderOptions>>();

                IDbConnection activator(DbConnectionOptions options)
                {
                    return connectionActivator(service, options);
                }

                return new DbProviderFactory(options, activator);
            });
        });
    }
}
```

```c#:program.cs
public class Program
{
    public static void Main(string[] args)
    {
        var builder = Host.CreateDefaultBuilder(args);

        builder.CondigureDbProvider((services, options) =>
        {
            // プロバイダの種類に応じたコネクションを生成して返します
            switch (options.ProviderType)
            {
                case "sqlite":
                    return new System.Data.SQLite.SQLiteConnection(options.ConnectionString);

                default:
                    throw new NotSupportedException();
            }
        });

        builder.ConfigureServices(services =>
        {
            services.AddHostedService<Worker>();
        });

        var host = builder.Build();

        host.Run();
    }
}
```