.NETのオプションパターンを使用するとき、ConfigureメソッドやPostConfigureメソッドを使ってオプションの値を構成するアクションを指定すると思います。

この投稿では次の2点を確認したいと思います。

  • ConfigureメソッドとPostConfigureメソッドで指定したアクションは、オプションがDIで解決されるときに呼び出されること
  • PostConfigureで指定したアクションは、Configureで指定したアクションが呼び出された後に呼び出されること

ConfigureメソッドとPostConfigureメソッドで指定したアクションの呼び出しを確認する

この2点を確認するサンプルコードです。

// DIで解決するオプション
private class SampleOptions {
	public int Value { get; set; }
}

var services = new ServiceCollection();
services
	.AddOptions<SampleOptions>()
	.Configure(options => {
		Console.WriteLine("Configure");
		// オプションを構成する
		options.Value = 1;
	})
	.PostConfigure(options => {
		Console.WriteLine("PostConfigure");
		// オプションを構成する
		options.Value = 2;
	});
var serviceProvider = services.BuildServiceProvider();

// IOptionsを解決する
Console.WriteLine("GetRequiredService");
var optionsProvider = serviceProvider.GetRequiredService<IOptions<SampleOptions>>();

// IOptionsからSampleOptionsを取り出す
// このときConfigureやPostConfigureで指定したアクションが呼び出される
Console.WriteLine("IOptions.Value");
var options = optionsProvider.Value;
Console.WriteLine(options.Value);

/*
// 実行結果
GetRequiredService
IOptions.Value
Configure
PostConfigure
2
*/

実行結果から、IOptions.ValueプロパティでSampleOptionsのインスタンスを取得(解決)するときに、 ConfigureメソッドやPostConfigureメソッドで指定したアクションが呼び出されることを確認できます。

さらに、Configureメソッドで指定したアクション、PostConfigureメソッドで指定したアクションの順番に呼び出され、SampleOptions.Valueの値が上書きされていることも確認できます。

.NETの実装を確認する

これらの動きを実装している部分を.NETのコードから確認したいと思います。

まずAddOptionsメソッドの実装を確認しましょう。IOptions<>に対してUnnamedOptionsManager<>IOptionsFactory<>に対してOptionsFactory<>が登録されています。この2つの登録以外はこの投稿では扱いません。無視してください。

public static class OptionsServiceCollectionExtensions
{
    public static IServiceCollection AddOptions(this IServiceCollection services)
    {
        ThrowHelper.ThrowIfNull(services);

        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
        services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
        services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
        services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
        return services;
    }
}

runtime/OptionsServiceCollectionExtensions.cs at main · dotnet/runtime · GitHub

次にUnnamedOptionsManager.Valueプロパティを確認すると、IOptionsFactory.Createメソッドを呼び出してオプションを生成していることがわかります。

internal sealed class UnnamedOptionsManager<TOptions> : IOptions<TOptions> where TOptions : class
{
    private readonly IOptionsFactory<TOptions> _factory;

    // 省略

    public TOptions Value
    {
        get
        {
            if (_value is TOptions value)
            {
                return value;
            }

            lock (_syncObj ?? Interlocked.CompareExchange(ref _syncObj, new object(), null) ?? _syncObj)
            {
                return _value ??= _factory.Create(Options.DefaultName);
            }
        }
    }
}

runtime/UnnamedOptionsManager.cs at main · dotnet/runtime · GitHub

最後にOptionsFactory.Createメソッドを見てみると、IConfigureOptions<TOptions>.Configureメソッド、IPostConfigureOptions<TOptions>.PostConfigureメソッドを順番に呼び出していることがわかります。

public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class
{
    // 省略

    public TOptions Create(string name)
    {
        TOptions options = CreateInstance(name);
        foreach (IConfigureOptions<TOptions> setup in _setups)
        {
            if (setup is IConfigureNamedOptions<TOptions> namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }
        foreach (IPostConfigureOptions<TOptions> post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }

        // 省略

        return options;
    }
}

runtime/OptionsFactory.cs at main · dotnet/runtime · GitHub

以上、ConfigureメソッドとPostConfigureメソッドについて、サンプルと.NETの実装から確認してみました。