重新整理 .net core 實踐篇—————服務與配置之間[十一二]

不問前世發表於2021-06-06

前言

前面基本介紹了,官方對於asp .net core 設計配置和設計服務的框架的一些思路。看下服務和配置之間是如何聯絡的吧。

正文

服務:

public interface ISelfService
{
	string ShowOptionName();
}

public class SelfService : ISelfService
{
	IOptions<SelfServiceOption> _options;
	public SelfService(IOptions<SelfServiceOption> options)
	{
		this._options = options;
	}
	public string ShowOptionName()
	{
		return _options.Value.Name;
	}
}

實體配置類:

public class SelfServiceOption
{
	public string Name { get; set; }
}

配置:

"SelfService": {
"name" : "zhangsan" 
}

註冊:

services.AddSingleton<ISelfService, SelfService>();
services.Configure<SelfServiceOption>(Configuration.GetSection("SelfService"));

獲取呼叫在startup.Configure中:

var SelfService = app.ApplicationServices.GetService<ISelfService>();

Console.WriteLine(SelfService.ShowOptionName());

結果:

經過前面系列中,我們非常好的能夠理解:services.Configure(Configuration.GetSection("SelfService"));

在反射通過屬性獲取值過程中就是SelfService+":"+屬性名,對字典進行獲取對應的值。

那麼看下為什麼我們在配置的時候需要IOptions options,也就是套一個Ioption呢?Ioption它是怎麼實現的呢?它的機制是什麼?

看下IOptions 的實現類OptionsManager:

public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
	private readonly IOptionsFactory<TOptions> _factory;
	private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); // Note: this is a private cache

	/// <summary>
	/// Initializes a new instance with the specified options configurations.
	/// </summary>
	/// <param name="factory">The factory to use to create options.</param>
	public OptionsManager(IOptionsFactory<TOptions> factory)
	{
		_factory = factory;
	}

	/// <summary>
	/// The default configured <typeparamref name="TOptions"/> instance, equivalent to Get(Options.DefaultName).
	/// </summary>
	public TOptions Value
	{
		get
		{
			return Get(Options.DefaultName);
		}
	}

	/// <summary>
	/// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
	/// </summary>
	public virtual TOptions Get(string name)
	{
		name = name ?? Options.DefaultName;

		// Store the options in our instance cache
		return _cache.GetOrAdd(name, () => _factory.Create(name));
	}
}

那麼我們呼叫Value 其實是呼叫IOptionsFactory的_factory.Create(name)。

檢視一下,IOptionsFactory的實現類OptionsFactory,看裡面的create方法。:

public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class, new()
{
	private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
	private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
	private readonly IEnumerable<IValidateOptions<TOptions>> _validations;

	/// <summary>
	/// Initializes a new instance with the specified options configurations.
	/// </summary>
	/// <param name="setups">The configuration actions to run.</param>
	/// <param name="postConfigures">The initialization actions to run.</param>
	public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, validations: null)
	{ }

	/// <summary>
	/// Initializes a new instance with the specified options configurations.
	/// </summary>
	/// <param name="setups">The configuration actions to run.</param>
	/// <param name="postConfigures">The initialization actions to run.</param>
	/// <param name="validations">The validations to run.</param>
	public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
	{
		_setups = setups;
		_postConfigures = postConfigures;
		_validations = validations;
	}

	/// <summary>
	/// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
	/// </summary>
	public TOptions Create(string name)
	{
		var options = new TOptions();
		foreach (var setup in _setups)
		{
			if (setup is IConfigureNamedOptions<TOptions> namedSetup)
			{
				namedSetup.Configure(name, options);
			}
			else if (name == Options.DefaultName)
			{
				setup.Configure(options);
			}
		}
		foreach (var post in _postConfigures)
		{
			post.PostConfigure(name, options);
		}

		if (_validations != null)
		{
			var failures = new List<string>();
			foreach (var validate in _validations)
			{
				var result = validate.Validate(name, options);
				if (result.Failed)
				{
					failures.AddRange(result.Failures);
				}
			}
			if (failures.Count > 0)
			{
				throw new OptionsValidationException(name, typeof(TOptions), failures);
			}
		}

		return options;
	}	
}

切開三段看:

第一段

var options = new TOptions();
foreach (var setup in _setups)
{
	if (setup is IConfigureNamedOptions<TOptions> namedSetup)
	{
		namedSetup.Configure(name, options);
	}
	else if (name == Options.DefaultName)
	{
		setup.Configure(options);
	}
}

上面是例項化我們的配置類。

namedSetup.Configure(name, options); 就是給我們selfServiceOption具體繫結。

這就要回到我們注入的地方了。

services.Configure<SelfServiceOption>(Configuration.GetSection("SelfService"));

來看下Configure 寫的是什麼,自己看具體的實現哈:

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
	where TOptions : class
{
	if (services == null)
	{
		throw new ArgumentNullException(nameof(services));
	}

	if (config == null)
	{
		throw new ArgumentNullException(nameof(config));
	}

	services.AddOptions();
	services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
	return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}

AddOptions方法 就是註冊具體實現的。

public static IServiceCollection AddOptions(this IServiceCollection services)
{
	if (services == null)
	{
		throw new ArgumentNullException(nameof(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;
}

重點來看下NamedConfigureFromConfigurationOptions 這個東西:

public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
	where TOptions : class
{
	/// <summary>
	/// Constructor that takes the <see cref="IConfiguration"/> instance to bind against.
	/// </summary>
	/// <param name="name">The name of the options instance.</param>
	/// <param name="config">The <see cref="IConfiguration"/> instance.</param>
	public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
		: this(name, config, _ => { })
	{ }

	/// <summary>
	/// Constructor that takes the <see cref="IConfiguration"/> instance to bind against.
	/// </summary>
	/// <param name="name">The name of the options instance.</param>
	/// <param name="config">The <see cref="IConfiguration"/> instance.</param>
	/// <param name="configureBinder">Used to configure the <see cref="BinderOptions"/>.</param>
	public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
		: base(name, options => config.Bind(options, configureBinder))
	{
		if (config == null)
		{
			throw new ArgumentNullException(nameof(config));
		}
	}
}

NamedConfigureFromConfigurationOptions 繼承ConfigureNamedOptions是不是很眼熟了。

看下我們第一段呼叫部分:

if (setup is IConfigureNamedOptions<TOptions> namedSetup)
{
	namedSetup.Configure(name, options);
}

後面就會呼叫NamedConfigureFromConfigurationOptions的Configure。

看下NamedConfigureFromConfigurationOptions的例項化方法,傳遞方法的第二個引數,options => config.Bind(options, configureBinder);

這個bind 是不是特別的眼熟,就是用來實體類繫結配置的,如有疑問可以看下前面幾篇,講述了繫結過程。

那麼看下ConfigureNamedOptions的Configure(NamedConfigureFromConfigurationOptions繼承ConfigureNamedOptions) 做了什麼吧,我們來到ConfigureNamedOptions:

public Action<TOptions> Action { get; }

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

public virtual void Configure(string name, TOptions options)
{
	if (options == null)
	{
		throw new ArgumentNullException(nameof(options));
	}

	// Null name is used to configure all named options.
	if (Name == null || name == Name)
	{
		Action?.Invoke(options, Dependency);
	}
}

這時候就會呼叫傳遞進來的Action:options => config.Bind(options, configureBinder),傳遞的這個options就是我們例項化的SelfServiceOption。

configureBinder 是一個配置哈,這是個配置的,下面第二個引數,如果沒穿預設是一個()=>{};

如果需要傳遞可以這麼寫:

services.Configure<SelfServiceOption>(Configuration.GetSection("SelfService"), BinderOptions =>
{
	BinderOptions.BindNonPublicProperties = true;
});

是不是很眼熟,就是設定可以繫結私有屬性的選項。

這就把我們的SelfServiceOption 和 Configuration.GetSection("SelfService") 已經繫結了。

那麼來看第二段:

foreach (var post in _postConfigures)
{
	post.PostConfigure(name, options);
}

這個是什麼呢? 這個是這樣的,比如說我們的現在獲取到了配置,得到了實體類SelfServiceOption,裡面的name 是zhangsan。

這個PostConfigure 可以幫我們做後續修改,比如說我獲取到了是zhangsan,但是呢,我想做一個判斷,如果name 是zhangsan,就加一個字尾,name= name+"_a";

大概就是一些後續操作。

services.AddOptions<SelfServiceOption>().Configure((options) =>
{
	if (options.Name == "zhangsan")
	{
		options.Name = "zhangsan_a";
	}
});

效果如下:

第三段

if (_validations != null)
{
	var failures = new List<string>();
	foreach (var validate in _validations)
	{
		var result = validate.Validate(name, options);
		if (result.Failed)
		{
			failures.AddRange(result.Failures);
		}
	}
	if (failures.Count > 0)
	{
		throw new OptionsValidationException(name, typeof(TOptions), failures);
	}
}

這一段很顯然就是來驗證我們的配置是否符合規格。原始碼就不解釋了,實踐篇注重實踐。

看下怎麼用的吧。

services.AddOptions<SelfServiceOption>().Configure((options) =>
{
	if (options.Name == "zhangsan")
	{
		options.Name = "zhangsan_a";
	}
}).Validate(options =>
{
	return options.Name != "zhangsan_a";
});

然後就會報錯。因為上面的邏輯是如果zhangsan,那麼我把名字改成zhangsan_a,然後驗證裡面寫名字不能等於zhangsan_a。

以上只是個人整理,如有錯誤,望請指點。

下一節,服務中的配置熱更新。

相關文章