.NET ライブラリ作成者のためのオプション パターン ガイダンス

依存関係の挿入の助けを借りて、サービスとそれに対応する構成を登録すると、"オプション パターン" を利用できるようになります。 オプション パターンを使用すると、ライブラリ (およびサービス) のコンシューマーで、TOptions がオプション クラスであるオプション インターフェイスのインスタンスを要求できます。 厳密に型指定されたオブジェクトを使用して構成オプションを使用すると、一貫性のある値の表現が保証され、データ注釈による検証を可能にし、文字列値を手動で解析する負担がなくなります。 ライブラリのコンシューマーで使用する多くの構成プロバイダーがあります。 コンシューマーでこれらのプロバイダーを使用すると、さまざまな方法でライブラリを構成できます。

.NET ライブラリの作成者のために、ライブラリのコンシューマーにオプション パターンを正しく公開する方法に関する一般的なガイダンスを説明します。 同じことを実現するのにさまざまな方法があり、いくつかの考慮事項があります。

名前付け規則

慣例により、サービスを登録する拡張メソッドの名前は Add{Service} であり、{Service} は意味のあるわかりやすい名前です。 Add{Service} 拡張メソッドは、ASP.NET Core でも .NETでも一般的です。

✔️ 自分のサービスと他のオファリングを明確に区別できる名前を考えます。

❌ .既に NET エコシステムの一部になっている、Microsoft の公式パッケージに含まれる名前は使用しないでください。

✔️ 拡張メソッドを公開する静的クラスは、{Type}Extensions という名前にします。{Type} は拡張する型です。

名前空間のガイダンス

Microsoft のパッケージでは、Microsoft.Extensions.DependencyInjection 名前空間を使用して、さまざまなサービス オファリングの登録が統合されています。

✔️ 自分のパッケージ オファリングを明確に識別できる名前空間を考えます。

❌ Microsoft が公式に認めていないパッケージには、Microsoft.Extensions.DependencyInjection 名前空間を使用しないでください。

パラメーターなし

サービスが最小限の構成または明示的ではない構成で動作できる場合は、パラメーターなしの拡張メソッドを検討します。

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
        this IServiceCollection services)
    {
        services.AddOptions<LibraryOptions>()
            .Configure(options =>
            {
                // Specify default option values
            });

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

上のコードの AddMyLibraryService では、次のことが行われています。

IConfiguration パラメーター

多くのオプションをコンシューマーに公開するライブラリを作成するときは、IConfiguration パラメーター拡張メソッドが必要になる場合があります。 予想される IConfiguration インスタンスでは、IConfiguration.GetSection 関数を使用することにより、構成の名前付きセクションにスコープを設定する必要があります。

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      IConfiguration namedConfigurationSection)
    {
        // Default library options are overridden
        // by bound configuration values.
        services.Configure<LibraryOptions>(namedConfigurationSection);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

上のコードの AddMyLibraryService では、次のことが行われています。

このパターンのコンシューマーは、名前付きセクションのスコープ設定された IConfiguration インスタンスを提供します。

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(
    builder.Configuration.GetSection("LibraryOptions"));

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

.AddMyLibraryService の呼び出しは、IServiceCollection メソッドで行われます。

既定値を指定するかどうかは、ライブラリの作成者が決定します。

Note

構成をオプション インスタンスにバインドすることができます。 ただし、名前が衝突してエラーが発生するリスクがあります。 また、この方法を使用して手動でバインドするときは、オプション パターンの使用を読み取り 1 回に制限します。 設定を変更しても再バインドされないため、コンシューマーは IOptionsMonitor インターフェイスを使用できません。

services.AddOptions<LibraryOptions>()
    .Configure<IConfiguration>(
        (options, configuration) =>
            configuration.GetSection("LibraryOptions").Bind(options));

代わりに、BindConfiguration 拡張メソッドを使用する必要があります。 この拡張メソッドは、構成をオプション インスタンスにバインドし、構成セクションの変更トークン ソースも登録します。 これにより、コンシューマーは IOptionsMonitor インターフェイスを使用できます。

構成セクションのパス パラメーター

ライブラリのコンシューマーは、基になる TOptions 型をバインドするための構成セクション パスを指定できます。 このシナリオに場合は、拡張メソッドで string パラメーターを定義します。

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      string configSectionPath)
    {
        services.AddOptions<SupportOptions>()
            .BindConfiguration(configSectionPath)
            .ValidateDataAnnotations()
            .ValidateOnStart();

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

上のコードの AddMyLibraryService では、次のことが行われています。

  • IServiceCollection のインスタンスを拡張します
  • string パラメーター configSectionPath を定義します
  • 呼び出し:

次の例では、 Microsoft.Extensions.Options.DataAnnotations NuGet パッケージを使用して、データ注釈の検証を有効にします。 SupportOptions クラスは、次のように定義されます:

using System.ComponentModel.DataAnnotations;

public sealed class SupportOptions
{
    [Url]
    public string? Url { get; set; }

    [Required, EmailAddress]
    public required string Email { get; set; }

    [Required, DataType(DataType.PhoneNumber)]
    public required string PhoneNumber { get; set; }
}

次の JSON appsettings.json ファイルが使用されるとします:

{
    "Support": {
        "Url": "https://support.example.com",
        "Email": "help@support.example.com",
        "PhoneNumber": "+1(888)-SUPPORT"
    }
}

Action<TOptions> パラメーター

ライブラリのコンシューマーは、オプション クラスのインスタンスを生成するラムダ式を提供することに関心を持つ場合があります。 このシナリオに場合は、拡張メソッドで Action<LibraryOptions> パラメーターを定義します。

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
        this IServiceCollection services,
        Action<LibraryOptions> configureOptions)
    {
        services.Configure(configureOptions);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

上のコードの AddMyLibraryService では、次のことが行われています。

  • IServiceCollection のインスタンスを拡張します
  • Action<T> のパラメーター configureOptions を定義します。ここで、TLibraryOptions です
  • configureOptions アクションを指定して Configure を呼び出します

このパターンのコンシューマーは、ラムダ式 (または、Action<LibraryOptions> パラメーターを満たすデリゲート) を提供します。

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(options =>
{
    // User defined option values
    // options.SomePropertyValue = ...
});
                                                                        
using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

オプション インスタンスのパラメーター

ライブラリのコンシューマーは、インラインのオプション インスタンスを使用することを好む場合があります。 このシナリオの場合は、オプション オブジェクト LibraryOptions のインスタンスを受け取る拡張メソッドを公開します。

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      LibraryOptions userOptions)
    {
        services.AddOptions<LibraryOptions>()
            .Configure(options =>
            {
                // Overwrite default option values
                // with the user provided options.
                // options.SomeValue = userOptions.SomeValue;
            });

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

上のコードの AddMyLibraryService では、次のことが行われています。

このパターンのコンシューマーは、LibraryOptions クラスのインスタンスを提供し、必要なプロパティ値をインラインで定義します。

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(new LibraryOptions
{
    // Specify option values
    // SomePropertyValue = ...
});

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

構成後

すべての構成オプション値をバインドまたは指定すると、事後構成の機能を使用できるようになります。 前に詳しく説明したのと同じ Action<TOptions> パラメーターを公開すると、PostConfigure を呼び出すことができます。 事後構成は、.Configure のすべての呼び出しの後に実行されます。 PostConfigure を使用することを検討する理由がいくつかあります:

  • 実行順序: .Configure 呼び出しで設定されたすべての構成値をオーバーライドできます。
  • 検証: 他のすべての構成が適用された後で、既定値が設定されていることを検証できます。
using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      Action<LibraryOptions> configureOptions)
    {
        services.PostConfigure(configureOptions);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

上のコードの AddMyLibraryService では、次のことが行われています。

  • IServiceCollection のインスタンスを拡張します
  • Action<T> のパラメーター configureOptions を定義します。ここで、TLibraryOptions です
  • configureOptions アクションを指定して PostConfigure を呼び出します

このパターンのコンシューマーは、事後構成ではないシナリオの Action<TOptions> パラメーター と同様に、ラムダ式 (または、Action<LibraryOptions> パラメーターを満たすデリゲート) を提供します。

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(options =>
{
    // Specify option values
    // options.SomePropertyValue = ...
});

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

関連項目