ASP.NET Core - 選項系統之源碼介紹

来源:https://www.cnblogs.com/wewant/archive/2023/03/30/17111746.html
-Advertisement-
Play Games

.NET Core 選項系統的主要實現在 Microsoft.Extensions.Options 和 Microsoft.Extensions.Options.ConfigurationExtensions 兩個 Nuget 包。對於一個框架的源碼進行解讀,我們可以從我們常用的框架中的類或方法入手 ...


.NET Core 選項系統的主要實現在 Microsoft.Extensions.Options 和 Microsoft.Extensions.Options.ConfigurationExtensions 兩個 Nuget 包。對於一個框架的源碼進行解讀,我們可以從我們常用的框架中的類或方法入手,這些類或方法就是我們解讀的入口。

從上面對選項系統的介紹中,大家也可以看出,日常對選項系統的使用涉及到的主要有 Configure 方法,有 IOptions、IOptionsSnapshot、IOptionMonitor 等介面。

Configure

首先看選項註冊,也就是 Configure 方法,註冊相關的方法都是擴展方法,上面也講到 Configure 方法有多個擴展來源,其中最常用的是 OptionsConfigurationServiceCollectionExtensions 中的 Configure 方法,該方法用於從配置信息中讀取配置並綁定為選項,如下,這裡將相應的方法單獨摘出來了。

點擊查看代碼 OptionsConfigurationServiceCollectionExtensions.Configure
public static class OptionsConfigurationServiceCollectionExtensions
{
	/// <summary>
	/// Registers a configuration instance which TOptions will bind against.
	/// </summary>
	/// <typeparam name="TOptions">The type of options being configured.</typeparam>
	/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
	/// <param name="config">The configuration being bound.</param>
	/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
	[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
	public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
		=> services.Configure<TOptions>(Options.Options.DefaultName, config);

	/// <summary>
	/// Registers a configuration instance which TOptions will bind against.
	/// </summary>
	/// <typeparam name="TOptions">The type of options being configured.</typeparam>
	/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
	/// <param name="name">The name of the options instance.</param>
	/// <param name="config">The configuration being bound.</param>
	/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
	[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
	public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class
		=> services.Configure<TOptions>(name, config, _ => { });

	/// <summary>
	/// Registers a configuration instance which TOptions will bind against.
	/// </summary>
	/// <typeparam name="TOptions">The type of options being configured.</typeparam>
	/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
	/// <param name="config">The configuration being bound.</param>
	/// <param name="configureBinder">Used to configure the <see cref="BinderOptions"/>.</param>
	/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
	[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
	public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services, IConfiguration config, Action<BinderOptions> configureBinder)
		where TOptions : class
		=> services.Configure<TOptions>(Options.Options.DefaultName, config, configureBinder);

	/// <summary>
	/// Registers a configuration instance which TOptions will bind against.
	/// </summary>
	/// <typeparam name="TOptions">The type of options being configured.</typeparam>
	/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
	/// <param name="name">The name of the options instance.</param>
	/// <param name="config">The configuration being bound.</param>
	/// <param name="configureBinder">Used to configure the <see cref="BinderOptions"/>.</param>
	/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
	[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
	public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] 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));
	}
}

其中 IOptionsChangeTokenSource 介面是用來監聽配置變化的服務,這個後面講。

另外還有 OptionsServiceCollectionExtensions 中的 Configure 方法,用於直接通過委托對選項類進行配置。

點擊查看代碼 OptionsServiceCollectionExtensions.Configure
public static class OptionsServiceCollectionExtensions
{

	public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
		=> services.Configure(Options.Options.DefaultName, configureOptions);

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

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

		services.AddOptions();
		services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
		return services;
	}
}

可以看出,其實選項系統中的選項都是命名模式的,預設名稱為 Options.Options.DefaultName,實際就是 string.Empty。當我們調用 Configure 方法對選項進行配置的時候,實際上時調用了 AddOptions 方法,並且往容器中添加了一個單例的實現了 IConfigureOptions 介面的實現。

IConfigureOptions、IConfigureNamedOptions、IPostConfigureOptions

其中 IConfigureOptions 是選項配置行為服務介面,ConfigureOptions 是它的預設實現,該類的內容很簡單,它的內部主要就是保存了一個委托,用於記錄使用者對選項的配置操作。

點擊查看代碼 ConfigureOptions
public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
	/// <summary>
	/// Constructor.
	/// </summary>
	/// <param name="action">The action to register.</param>
	public ConfigureOptions(Action<TOptions> action)
	{
		Action = action;
	}

	/// <summary>
	/// The configuration action.
	/// </summary>
	public Action<TOptions> Action { get; }

	/// <summary>
	/// Invokes the registered configure <see cref="Action"/>.
	/// </summary>
	/// <param name="options">The options instance to configure.</param>
	public virtual void Configure(TOptions options)
	{
		if (options == null)
		{
			throw new ArgumentNullException(nameof(options));
		}

		Action?.Invoke(options);
	}
}

IConfigureNamedOptions 繼承了 IConfigureNamedOptions 介面,預設實現是 ConfigureNamedOptions ,作用一樣,只不過多了一個方法用於應對命名選項模式。它有多個重載泛型重載,也是之前的文章ASP.NET Core - 選型系統之選型配置 中講到的“使用DI服務配置選項”的具體實現。

點擊查看代碼 ConfigureNamedOptions ```csharp public class ConfigureNamedOptions : IConfigureNamedOptions where TOptions : class { /// /// Constructor. /// /// The name of the options. /// The action to register. public ConfigureNamedOptions(string name, Action action) { Name = name; Action = action; }
/// <summary>
/// The options name.
/// </summary>
public string Name { get; }

/// <summary>
/// The configuration action.
/// </summary>
public Action<TOptions> Action { get; }

/// <summary>
/// Invokes the registered configure <see cref="Action"/> if the <paramref name="name"/> matches.
/// </summary>
/// <param name="name">The name of the options instance being configured.</param>
/// <param name="options">The options instance to configure.</param>
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);
	}
}

/// <summary>
/// Invoked to configure a <typeparamref name="TOptions"/> instance with the <see cref="Options.DefaultName"/>.
/// </summary>
/// <param name="options">The options instance to configure.</param>
public void Configure(TOptions options) => Configure(Options.DefaultName, options);

}

</details>

而 NamedConfigureFromConfigurationOptions<TOptions> 類是 IConfigureNamedOptions<TOptions> 的另一個實現,繼承了ConfigureNamedOptions<TOptions> 類,重寫了一些行為,最終是通過之前講到的 ConfigurationBuilder的 Bind 方法將配置綁定到選項類而已。

<details>
<summary>點擊查看代碼 NamedConfigureFromConfigurationOptions<TOptions></summary>

```csharp
public class NamedConfigureFromConfigurationOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] 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>
	[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
	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>
	[RequiresUnreferencedCode(OptionsBuilderConfigurationExtensions.TrimmingRequiredUnreferencedCodeMessage)]
	public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
		: base(name, options => BindFromOptions(options, config, configureBinder))
	{
		if (config == null)
		{
			throw new ArgumentNullException(nameof(config));
		}
	}

	[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
		Justification = "The only call to this method is the constructor which is already annotated as RequiresUnreferencedCode.")]
	private static void BindFromOptions(TOptions options, IConfiguration config, Action<BinderOptions> configureBinder) => config.Bind(options, configureBinder);
}

其他的 IPostConfigureOptions 介面也是一樣套路,當我們通過相應的方法傳入委托對選項類進行配置的時候,會向容器中註入一個單例服務,將配置行為保存起來。

接著往下看 AddOptions 方法,AddOptions 方法有兩個重載:

點擊查看代碼 AddOptions ```csharp public static class OptionsServiceCollectionExtensions { public static IServiceCollection AddOptions(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(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;
}
public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name)
	where TOptions : class
{
	if (services == null)
	{
		throw new ArgumentNullException(nameof(services));
	}

	services.AddOptions();
	return new OptionsBuilder<TOptions>(services, name);
}

}

</details>

這裡可以看出兩者的返回值不同,而且第二個方法也調用了第一個方法,第一個方法中主要就是向容器中添加我們常用的IOptions<TOptions>、IOptionsSnapshot<TOptions>、IOptionsMonitor<TOptions> 服務介面,這裡也可以看到不同服務介面對於的生命周期。除此之外還有工廠服務IOptionsFactory<>和緩存服務IOptionsMonitorCache<>,這兩個就是選項體系的關鍵。每個選項進行配置的時候都會同時註入這些服務,所以每一個選項我們都能使用三個不同介面去解析。

# OptionsBuilder

上面第二個 AddOptions 方法返回 OptionsBuilder<TOptions> 對象。之前講過 OptionsBuilder<TOptions> 類中也有 Configure 方法,其實不止 Configure 方法,其他的 PostConfigure 方法等也有,它其實就是最終的選項系統配置類,我們所有的選項配置其實都可以通過調用第二個 AddOptions 方法,再通過 OptionsBuilder<TOptions> 對象中的方法來完成配置。其他各個擴展方法的配置方式不過是進行了使用簡化而已。

<details>
<summary>點擊查看代碼 OptionsBuilder<TOptions></summary>

```csharp
public class OptionsBuilder<TOptions> where TOptions : class
{
	private const string DefaultValidationFailureMessage = "A validation error has occurred.";
	
	public string Name { get; }
	
	public IServiceCollection Services { get; }
	
	public OptionsBuilder(IServiceCollection services, string name)
	{
		Services = services;
		Name = name ?? Options.DefaultName;
	}
	
	public virtual OptionsBuilder<TOptions> Configure(Action<TOptions> configureOptions)
	{
		Services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(Name, configureOptions));
		return this;
	}
	
	public virtual OptionsBuilder<TOptions> PostConfigure(Action<TOptions> configureOptions)
	{
		Services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(Name, configureOptions));
		return this;
	}
	
	public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation)
		=> Validate(validation: validation, failureMessage: DefaultValidationFailureMessage);
		
	public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation, string failureMessage)
	{
		Services.AddSingleton<IValidateOptions<TOptions>>(new ValidateOptions<TOptions>(Name, validation, failureMessage));
		return this;
	}
}

IValidateOptions

我們除了可以對選項進行配置綁定之外,還可以對選項進行驗證。驗證規則是通過上面的第二個 AddOptions 方法返回的 OptionsBuilder 方法進行添加的。

驗證規則配置有三種方式,最後其實都是通過 IValidateOptions 的實現類來完成。我們自己實現的自定義驗證類就不用說了,最後我們會將其註入到容器中,而從上面的代碼中可以看到,當我們通過委托的方式自定義驗證規則的時候,它會被構建成一個 ValidateOptions 類對象,並註入到容器中作為一個服務。

ValidateOptions 是 IValidateOptions 的一個實現類,構造函數中接收委托,通過委托返回的 bool 結構判斷驗證是否通過。

點擊查看代碼 ValidateOptions
public class ValidateOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class
{
	/// <summary>
	/// Constructor.
	/// </summary>
	/// <param name="name">Options name.</param>
	/// <param name="validation">Validation function.</param>
	/// <param name="failureMessage">Validation failure message.</param>
	public ValidateOptions(string name, Func<TOptions, bool> validation, string failureMessage)
	{
		Name = name;
		Validation = validation;
		FailureMessage = failureMessage;
	}

	/// <summary>
	/// The options name.
	/// </summary>
	public string Name { get; }

	/// <summary>
	/// The validation function.
	/// </summary>
	public Func<TOptions, bool> Validation { get; }

	/// <summary>
	/// The error to return when validation fails.
	/// </summary>
	public string FailureMessage { get; }

	/// <summary>
	/// Validates a specific named options instance (or all when <paramref name="name"/> is null).
	/// </summary>
	/// <param name="name">The name of the options instance being validated.</param>
	/// <param name="options">The options instance.</param>
	/// <returns>The <see cref="ValidateOptionsResult"/> result.</returns>
	public ValidateOptionsResult Validate(string name, TOptions options)
	{
		// null name is used to configure all named options
		if (Name == null || name == Name)
		{
			if ((Validation?.Invoke(options)).Value)
			{
				return ValidateOptionsResult.Success;
			}
			return ValidateOptionsResult.Fail(FailureMessage);
		}

		// ignored if not validating this instance
		return ValidateOptionsResult.Skip;
	}
}

我們可以通過重載方法傳入相應的驗證失敗提醒文本。

Options、UnnamedOptionsManager

接下來看選項使用相關的內容,其中 IOptions 中的選項類一經創建一直保持不變,預設實現類 UnnamedOptionsManager

點擊查看代碼 UnnamedOptionsManager
internal sealed class UnnamedOptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
	IOptions<TOptions>
	where TOptions : class
{
	private readonly IOptionsFactory<TOptions> _factory;
	private volatile object _syncObj;
	private volatile TOptions _value;

	public UnnamedOptionsManager(IOptionsFactory<TOptions> factory) => _factory = 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);
			}
		}
	}
}

IOptions 介面只有一個 Value 屬性,實現類中通過鎖確保創建的 Value 值不會因為線程問題導致不同,且該服務被註冊為單例生命周期,所以對象不銷毀,後續一直會讀取記憶體中的 Value 值。具體選項類對象的創建由工廠服務負責。

IOptionsSnapshot、OptionsManager

IOptionsSnapshot 的實現類是 OptionsManager,該類中有一個私有的 OptionsCache 屬性,每次對選項類進行讀取的時候,都是先嘗試從緩存讀取,如果沒有才創建。而由於 IOptionsSnapshot 被註冊為請求域生命周期,所以單次請求內相應對象不會銷毀,緩存不會清空,會一直保持一個。

點擊查看代碼 OptionsManager
public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
	IOptions<TOptions>,
	IOptionsSnapshot<TOptions>
	where TOptions : class
{
	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(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;

		if (!_cache.TryGetValue(name, out TOptions options))
		{
			// Store the options in our instance cache. Avoid closure on fast path by storing state into scoped locals.
			IOptionsFactory<TOptions> localFactory = _factory;
			string localName = name;
			options = _cache.GetOrAdd(name, () => localFactory.Create(localName));
		}

		return options;
	}
}

IOptionsMonitor、OptionsMonitor

IOptionsMonitor 每次獲取選項類都是最新的值,它實現類是 OptionsMonitor,實現類中使用了從容器中註入的單例緩存 IOptionsMonitorCache 來保存選項類,並且通過相應的 IOptionsChangeTokenSource 註冊了選項類綁定內容的監聽,例如上面講到的 ConfigurationChangeTokenSource,在選項類配置內容改變的時候會觸發事件,而在事件中會將緩存先情況並重新獲取創建類,並且執行註冊進來的額外的監聽事件,可以看看下麵的 InvokeChanged 方法。

點擊查看代碼 OptionsMonitor
public class OptionsMonitor<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
	IOptionsMonitor<TOptions>,
	IDisposable
	where TOptions : class
{
	private readonly IOptionsMonitorCache<TOptions> _cache;
	private readonly IOptionsFactory<TOptions> _factory;
	private readonly List<IDisposable> _registrations = new List<IDisposable>();
	internal event Action<TOptions, string> _onChange;

	/// <summary>
	/// Constructor.
	/// </summary>
	/// <param name="factory">The factory to use to create options.</param>
	/// <param name="sources">The sources used to listen for changes to the options instance.</param>
	/// <param name="cache">The cache used to store options.</param>
	public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
	{
		_factory = factory;
		_cache = cache;

		void RegisterSource(IOptionsChangeTokenSource<TOptions> source)
		{
			IDisposable registration = ChangeToken.OnChange(
					  () => source.GetChangeToken(),
					  (name) => InvokeChanged(name),
					  source.Name);

			_registrations.Add(registration);
		}

		// The default DI container uses arrays under the covers. Take advantage of this knowledge
		// by checking for an array and enumerate over that, so we don't need to allocate an enumerator.
		if (sources is IOptionsChangeTokenSource<TOptions>[] sourcesArray)
		{
			foreach (IOptionsChangeTokenSource<TOptions> source in sourcesArray)
			{
				RegisterSource(source);
			}
		}
		else
		{
			foreach (IOptionsChangeTokenSource<TOptions> source in sources)
			{
				RegisterSource(source);
			}
		}
	}

	private void InvokeChanged(string name)
	{
		name = name ?? Options.DefaultName;
		_cache.TryRemove(name);
		TOptions options = Get(name);
		if (_onChange != null)
		{
			_onChange.Invoke(options, name);
		}
	}

	/// <summary>
	/// The present value of the options.
	/// </summary>
	public TOptions CurrentValue
	{
		get => 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;
		return _cache.GetOrAdd(name, () => _factory.Create(name));
	}

	/// <summary>
	/// Registers a listener to be called whenever <typeparamref name="TOptions"/> changes.
	/// </summary>
	/// <param name="listener">The action to be invoked when <typeparamref name="TOptions"/> has changed.</param>
	/// <returns>An <see cref="IDisposable"/> which should be disposed to stop listening for changes.</returns>
	public IDisposable OnChange(Action<TOptions, string> listener)
	{
		var disposable = new ChangeTrackerDisposable(this, listener);
		_onChange += disposable.OnChange;
		return disposable;
	}

	/// <summary>
	/// Removes all change registration subscriptions.
	/// </summary>
	public void Dispose()
	{
		// Remove all subscriptions to the change tokens
		foreach (IDisposable registration in _registrations)
		{
			registration.Dispose();
		}

		_registrations.Clear();
	}

	internal sealed class ChangeTrackerDisposable : IDisposable
	{
		private readonly Action<TOptions, string> _listener;
		private readonly OptionsMonitor<TOptions> _monitor;

		public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener)
		{
			_listener = listener;
			_monitor = monitor;
		}

		public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);

		public void Dispose() => _monitor._onChange -= OnChange;
	}
}

OnChange 方法中傳入的委托本來可以可以直接追加到事件中的,這裡將其再包裝多一層,是為了 OptionsMonitor 對象銷毀的時候能夠將相應的事件釋放,如果不包裝多一層的話,委托只在方法作用域中,對象釋放的時候是獲取不到的。

IOptionsMonitorCache、OptionsCache

OptionsCache 是 IOptionsMonitorCache 介面的的實現類,從上面可以看到 OptionsMonitor 和 OptionsSnapshot 都使用到了這個,OptionsSnapshot 通過內部創建的私有的緩存屬性實現了請求域內選項類不變,而 OptionsMonitor 則通過它減少了每次都直接讀取配置來源(如文件、資料庫、配置中心api)的性能消耗,而是通過變更事件的方式進行更新。其實我們還可以在需要的時候註入IOptionsMonitorCache 服務自行對選項類進行更新。

OptionsCache 的具體實現比較簡單,主要就是通過 ConcurrentDictionary<string, Lazy> 對象作為記憶體緩存,其中為了性能還再使用了 Lazy 方式。

IOptionsFactory、OptionsFactory

OptionsFactory 類實現 IOptionsFactory 介面,是選項類的實際創建配置之處,其實就是將之前註冊到容器中與當前相關的各種配置、驗證的行為配置類註入進來,再通過放射創建對象之後,將選項類對象傳進去,逐一對相應的行為進行調用,最後得到一個成型的選項類。這裡選項類的創建方式很簡單,這也是要求選項類要有無參構造函數的原因。

點擊查看代碼 OptionsFactory
public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
	IOptionsFactory<TOptions>
	where TOptions : class
{
	private readonly IConfigureOptions<TOptions>[] _setups;
	private readonly IPostConfigureOptions<TOptions>[] _postConfigures;
	private readonly 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: Array.Empty<IValidateOptions<TOptions>>())
	{ }

	/// <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)
	{
		// The default DI container uses arrays under the covers. Take advantage of this knowledge
		// by checking for an array and enumerate over that, so we don't need to allocate an enumerator.
		// When it isn't already an array, convert it to one, but don't use System.Linq to avoid pulling Linq in to
		// small trimmed applications.

		_setups = setups as IConfigureOptions<TOptions>[] ?? new List<IConfigureOptions<TOptions>>(setups).ToArray();
		_postConfigures = postConfigures as IPostConfigureOptions<TOptions>[] ?? new List<IPostConfigureOptions<TOptions>>(postConfigures).ToArray();
		_validations = validations as IValidateOptions<TOptions>[] ?? new List<IValidateOptions<TOptions>>(validations).ToArray();
	}

	/// <summary>
	/// Returns a configured <typeparamref name="TOptions"/> instance with the given <paramref name="name"/>.
	/// </summary>
	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);
		}

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

		return options;
	}

	/// <summary>
	/// Creates a new instance of options type
	/// </summary>
	protected virtual TOptions CreateInstance(string name)
	{
		return Activator.CreateInstance<TOptions>();
	}
}

以上就是 .NET Core 下的選項系統,由於選項系統的源碼不多,這裡也就將大部分類都拿出來講了一下,相當於把這個框架的流程思路都講了一遍,不知不覺寫得字數又很多了,希望有童鞋能夠耐心地看到這裡。



參考文章:
ASP.NET Core 中的選項模式 | Microsoft Learn
選項模式 - .NET | Microsoft Learn
面向 .NET 庫創建者的選項模式指南 - .NET | Microsoft Learn
理解ASP.NET Core - 選項(Options)



ASP.NET Core 系列:

目錄:ASP.NET Core 系列總結
上一篇:ASP.NET Core - 選項系統之選項驗證


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 RocketMQ是阿裡巴巴旗下一款開源的MQ框架,經歷過雙十一考驗、Java編程語言實現,有非常好完整生態系統。RocketMQ作為一款純java、分散式、隊列模型的開源消息中間件,支持事務消息、順序消息、批量消息、定時消息、消息回溯等 本篇文章第一部分屬於一些核心概念和工作流程的講解;第二部 ...
  • 針對大量log日誌快速定位錯誤地方 動態查看日誌 tail -f catalina.ou 從頭打開日誌文件 cat catalina.ou 可以使用 >nanjiangtest.txt 輸出某個新日誌去查看 [root@yesky logs]# cat -n catalina.out |grep 7 ...
  • 近段時間忙於各種項目和對【易排平臺】的優化,沒顧得上分享APS相關的小技巧,回頭看看小公眾號的關註人數早已達1500+,在此爭取時間寫一下這段時間在項目上及平臺優化過程中遇到的一些小技巧,以感謝諸位的關註。過去數月的解決的問題中,涉及最多的是規劃模型中,實現各種時間維度的功能,目前在平臺上也稍有成果 ...
  • 呆了2個大屏行業的公司,對大屏幕有一些瞭解,所以整理下所瞭解的觸摸屏相關概念。方便自己以及進入這個行業的小伙伴們,能有個系統、快速的認知。 觸摸屏詳細的知識點,網上其實都有。整理資料過程中,我也瞭解了更多的觸摸屏知識,像聲波屏、光學屏之類的之前就沒接觸。下麵分不同的模塊,給大家介紹 交互觸摸屏類型 ...
  • C#-垃圾回收機制(GC) 什麼是GC 官網中有這麼一句話: The garbage collector is a common language runtime component that controls the allocation and release of managed memory ...
  • 在 IIS 上啟用 Websocket 在 Windows Server 2012 或更高版本上啟用對 WebSocket 協議的支持: 備註 使用 IIS Express 時無需執行這些步驟 通過“管理”菜單或“伺服器管理器”中的鏈接使用“添加角色和功能”嚮導。 選擇“基於角色或基於功能的安裝”。 ...
  • 一:背景 1. 講故事 前段時間有位朋友微信找到我,說他的程式使用 hsl 庫之後,採集 plc 時記憶體溢出,讓我幫忙看一下怎麼回事,哈哈,貌似是分析之旅中的第二次和 hsl 打交道,既然找到我,那就上 windbg 說話吧。 二:WinDbg 分析 1. 為什麼會記憶體溢出 簡單觀察程式的提交記憶體之 ...
  • 最近在工作中遇到一個問題,就是我有多個線程會調用bitmap對象,運行的時候報錯,對象當前正在其他地方使用。第一反應肯定是加鎖啊,於是我就在每個用到bitmap的地方都加了鎖,但是運行之後依然報這個錯 測試代碼如下 using System; using System.Drawing; using ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...