.Net Core 中的選項Options


.NetCore的配置選項建議結合在一起學習,不了解.NetCore 配置Configuration的同學可以看下我的上一篇文章 [.Net Core配置Configuration源碼研究]

由代碼開始


定義一個用戶配置選項

public class UserOptions
{
    private string instanceId;
    private static int index = 0;
    public UserOptions()
    {
        instanceId = (++index).ToString("00");
        Console.WriteLine($"Create UserOptions Instance:{instanceId}");
    }
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString() => $"Name:{Name} Age:{Age} Instance:{instanceId} ";
}
public class UserOptions2
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString() => $" Name:{Name} Age:{Age}";
}

定義json配置文件:myconfig.json

{
  "UserOption": {
    "Name": "ConfigName-zhangsan",
    "Age": 666
  }
}

創建ServiceCollection

services = new ServiceCollection();
var configBuilder = new ConfigurationBuilder().AddInMemoryCollection().AddJsonFile("myconfig.json", true, true);
var iconfiguration = configBuilder.Build();
services.AddSingleton<IConfiguration>(iconfiguration);

示例代碼

services.Configure<UserOptions>(x => { x.Name = "張三"; x.Age = new Random().Next(1, 10000); });
services.AddOptions<UserOptions2>().Configure<IConfiguration>((x, config) => { x.Name = config["UserOption:Name"]; x.Age = 100; }); ;
services.PostConfigure<UserOptions>(x => { x.Name = x.Name + "Post"; x.Age = x.Age; });
services.Configure<UserOptions>("default", x => { x.Name = "Default-張三"; x.Age = new Random().Next(1, 10000); });
services.Configure<UserOptions>("config", configuration.GetSection("UserOption"));
using (var provider = services.BuildServiceProvider())
{
    using (var scope1 = provider.CreateScope())
    {
        PrintOptions(scope1, "Scope1");
    }

    //修改配置文件
    Console.WriteLine(string.Empty);
    Console.WriteLine("修改配置文件");
    var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "myconfig.json");
    File.WriteAllText(filePath, "{\"UserOption\": { \"Name\": \"ConfigName-lisi\", \"Age\": 777}}");
    //配置文件的change回調事件需要一定時間執行
    Thread.Sleep(300);
    Console.WriteLine(string.Empty);

    using (var scope2 = provider.CreateScope())
    {
        PrintOptions(scope2, "Scope2");
    }

    Console.WriteLine(string.Empty);

    using (var scope3 = provider.CreateScope())
    {
        PrintOptions(scope3, "Scope3");
    }
}

static void PrintOptions(IServiceScope scope, string scopeName)
{
    var options1 = scope.ServiceProvider.GetService<IOptions<UserOptions>>();
    Console.WriteLine($"手動注入讀取,IOptions,{scopeName}-----{ options1.Value}");

    var options2 = scope.ServiceProvider.GetService<IOptionsSnapshot<UserOptions>>();
    Console.WriteLine($"配置文件讀取,IOptionsSnapshot,{scopeName}-----{ options2.Value}");
    var options3 = scope.ServiceProvider.GetService<IOptionsSnapshot<UserOptions>>();
    Console.WriteLine($"配置文件根據名稱讀取,IOptionsSnapshot,{scopeName}-----{ options3.Get("config")}");

    var options4 = scope.ServiceProvider.GetService<IOptionsMonitor<UserOptions>>();
    Console.WriteLine($"配置文件讀取,IOptionsMonitor,{scopeName}-----{ options4.CurrentValue}");
    var options5 = scope.ServiceProvider.GetService<IOptionsMonitor<UserOptions>>();
    Console.WriteLine($"配置文件根據名稱讀取,IOptionsMonitor,{scopeName}-----{options5.Get("config")}");

    var options6 = scope.ServiceProvider.GetService<IOptions<UserOptions2>>();
    Console.WriteLine($"Options2-----{options6.Value}");
}

代碼運行結果

Create UserOptions Instance:01
手動注入讀取,IOptions,Scope1----- Name:張三Post Age:6575 Instance:01
Create UserOptions Instance:02
配置文件讀取,IOptionsSnapshot,Scope1----- Name:張三Post Age:835 Instance:02
Create UserOptions Instance:03
配置文件根據名稱讀取,IOptionsSnapshot,Scope1----- Name:ConfigName-zhangsan Age:666 Instance:03
Create UserOptions Instance:04
配置文件讀取,IOptionsMonitor,Scope1----- Name:張三Post Age:1669 Instance:04
Create UserOptions Instance:05
配置文件根據名稱讀取,IOptionsMonitor,Scope1----- Name:ConfigName-zhangsan Age:666 Instance:05
Options2----- Name:ConfigName-zhangsan Age:100

修改配置文件
Create UserOptions Instance:06

手動注入讀取,IOptions,Scope2----- Name:張三Post Age:6575 Instance:01
Create UserOptions Instance:07
配置文件讀取,IOptionsSnapshot,Scope2----- Name:張三Post Age:5460 Instance:07
Create UserOptions Instance:08
配置文件根據名稱讀取,IOptionsSnapshot,Scope2----- Name:ConfigName-lisi Age:777 Instance:08
配置文件讀取,IOptionsMonitor,Scope2----- Name:張三Post Age:1669 Instance:04
配置文件根據名稱讀取,IOptionsMonitor,Scope2----- Name:ConfigName-lisi Age:777 Instance:06
Options2----- Name:ConfigName-zhangsan Age:100

手動注入讀取,IOptions,Scope3----- Name:張三Post Age:6575 Instance:01
Create UserOptions Instance:09
配置文件讀取,IOptionsSnapshot,Scope3----- Name:張三Post Age:5038 Instance:09
Create UserOptions Instance:10
配置文件根據名稱讀取,IOptionsSnapshot,Scope3----- Name:ConfigName-lisi Age:777 Instance:10
配置文件讀取,IOptionsMonitor,Scope3----- Name:張三Post Age:1669 Instance:04
配置文件根據名稱讀取,IOptionsMonitor,Scope3----- Name:ConfigName-lisi Age:777 Instance:06
Options2----- Name:ConfigName-zhangsan Age:100

通過運行代碼得到的結論

  • Options可通過手動初始化配置項配置(可在配置時讀取依賴注入的對象)、或通過IConfiguration綁定配置
  • PostConfiger可在Configer基礎上繼續配置
  • 可通過IOptionsSnapshot或IOptionsMonitor根據配置名稱讀取配置項,未指定名稱讀取第一個注入的配置
  • IOptions和IOptionsMonitor生命周期為Singleton,IOptionsSnapshot生命周期為Scope
  • IOptionsMonitor可監聽到配置文件變動去動態更新配置項

問題

  • IOptions,IOptionsSnapshot,IOptionsMonitor 如何/何時注入、初始化
  • Options指定名稱時內部是如何設置的
  • Options如何綁定的IConfiguration
  • IOptionsMonitor是如何同步配置文件變動的


配合源碼解決疑惑


Configure注入

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
{
    return services.Configure(Microsoft.Extensions.Options.Options.DefaultName, configureOptions);
}

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class
{
	services.AddOptions();
	services.AddSingleton((IConfigureOptions<TOptions>)new ConfigureNamedOptions<TOptions>(name, configureOptions));
	return services;
}

public static IServiceCollection AddOptions(this IServiceCollection services)
{
	services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
	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;
}

通過上面的源碼可以發現,Options相關類是在AddOptions中注入的,具體的配置項在Configure中注入。

如果不指定Configure的Name,也會有個默認的Name=Microsoft.Extensions.Options.Options.DefaultName

那么我們具體的配置項存到哪里去了呢,在ConfigureNamedOptions這個類中,在Configer函數調用時,只是把相關的配置委托存了起來:

public ConfigureNamedOptions(string name, Action<TOptions> action)
{
	Name = name;
	Action = action;
}

OptionsManager

private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache = new ConcurrentDictionary<string, Lazy<TOptions>>(StringComparer.Ordinal);

public TOptions Value => Get(Options.DefaultName);

public virtual TOptions Get(string name)
{
	name = name ?? Options.DefaultName;
	return _cache.GetOrAdd(name, () => _factory.Create(name));
}

OptionsManager實現相對較簡單,在查詢時需要執行Name,如果為空就用默認的Name,如果緩存沒有,就用Factory創建一個,否則就讀緩存中的選項。

IOptionsIOptionsSnapshot的實現類都是OptionsManager,只是生命周期不同。


OptionsFactory

那么OptionsFactory又是如何創建Options的呢?我們看一下他的構造函數,構造函數將所有ConfigurePostConfigure的初始化委托都通過構造函數保存在內部變量中

public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
	{
        _setups = setups;
        _postConfigures = postConfigures;
	}

接下來看Create(有刪改,與本次研究無關的代碼沒有貼出來):

	public TOptions Create(string name)
	{
        //首先創建對應Options的實例
		TOptions val = Activator.CreateInstance<TOptions>();
        //循環所有的配置項,依次執行,如果對同一個Options配置了多次,最后一次的賦值生效
		foreach (IConfigureOptions<TOptions> setup in _setups)
		{
			var configureNamedOptions = setup as IConfigureNamedOptions<TOptions>;
			if (configureNamedOptions != null)
			{
                //Configure中會判斷傳入Name的值與本身的Name值是否相同,不同則不執行Action
                //這解釋了我們一開始的示例中,注入了三個UserOptions,但是在IOptionsSnapshot.Value中獲取到的是第一個沒有名字的
                //因為Value會調用OptionsManager.Get(Options.DefaultName),進而調用Factory的Create(Options.DefaultName)
				configureNamedOptions.Configure(name, val);
			}
			else if (name == Options.DefaultName)
			{
				setup.Configure(val);
			}
		}
        
        //PostConfigure沒啥可多說了,名字判斷邏輯與Configure一樣
		foreach (var postConfigure in _postConfigures)
		{
			postConfigure.PostConfigure(name, val);
		}
		
		return val;
	}

NamedConfigureFromConfigurationOptions

IConfiguration配置Options的方式略有不同

對應Configure擴展方法最終調用的代碼在Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions這個類中

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder) where TOptions : class
{
	services.AddOptions();
	services.AddSingleton((IOptionsChangeTokenSource<TOptions>)new ConfigurationChangeTokenSource<TOptions>(name, config));
	return services.AddSingleton((IConfigureOptions<TOptions>)new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}

擴展方法里又注入了一個IOptionsChangeTokenSource,這個類的作用是提供一個配置文件變動監聽的Token

同時將IConfigureOptions實現類注冊成了NamedConfigureFromConfigurationOptions

NamedConfigureFromConfigurationOptions繼承了ConfigureNamedOptions,在構造函數中用IConfiguration.Bind實現了生成Options的委托

	public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
		: base(name, (Action<TOptions>)delegate(TOptions options)
		{
			config.Bind(options, configureBinder);
		})

所以在Factory的Create函數中,會調用IConfigurationBind函數

由於IOptionsSnapshot生命周期是Scope,在配置文件變動后新的Scope中會獲取最新的Options


ValidateOptions

OptionsBuilder還包含了一個Validate函數,該函數要求傳入一個Func<TOptions,bool>的委托,會注入一個單例的ValidateOptions對象。

OptionsFactory構建Options的時候會驗證Options的有效性,驗證失敗會拋出OptionsValidationException異常

對於ValidateOptionsPostConfigureOptions都是構建Options實例時需要用到的主要模塊,不過使用和內部實現都較為簡單,應用場景也不是很多,本文就不對這兩個類多做介紹了


結論

Configure擴展函數中會首先調用AddOptions函數

IOptions,IOptionsSnapshot,IOptionsMonitor都是在AddOptions函數中注入的

Configure配置的選項配置委托最終會保存到ConfigureNamedOptionsNamedConfigureFromConfigurationOptions

IOptionsIOptionsSnapshot的實現類為OptionsManager

OptionsManager通過OptionsFactory創建Options的實例,並會以Name作為鍵存到字典中緩存實例

OptionsFactory會通過反射創建Options的實例,並調用ConfigureNamedOptions中的委托給實例賦值

現在只剩下最后一個問題了,OptionsMonitor是如何動態更新選項的呢?

其實前面的講解中已經提到了一個關鍵的接口IOptionsChangeTokenSource,這個接口提供一個IChangeToken,通過ChangeToken監聽這個Token就可以監聽到文件的變動,我們來看下OptionsMonitor是否是這樣做的吧!

//構造函數
public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
{
    _factory = factory;
    _sources = sources;
    _cache = cache;
    //循環屬於TOptions的所有IChangeToken
    foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
    {
        ChangeToken.OnChange(() => source.GetChangeToken(), delegate(string name)
                             {

                                //清除緩存 
                                name = name ?? Options.DefaultName;
								_cache.TryRemove(name);
                             }, source.Name);
    }
}



public virtual TOptions Get(string name)
{
    name = name ?? Options.DefaultName;
    return _cache.GetOrAdd(name, () => _factory.Create(name));
}

果然是這樣的吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM