Options选项

  • Options选项已关闭评论
  • 80 次浏览
  • A+
所属分类:.NET技术
摘要

选项用来提供对相关设置的强类型访问,读取配置首选使用选项模式。选项无法脱离容器使用,依赖容器,实现了选项不同的访问方式。选项模式使用了泛型包装器,因此具备了如下优点:

选项用来提供对相关设置的强类型访问,读取配置首选使用选项模式。选项无法脱离容器使用,依赖容器,实现了选项不同的访问方式。选项模式使用了泛型包装器,因此具备了如下优点:

  • 不需要显示注册选项具体类型,只需要将泛型包装器注入到容器中;
  • 对于选项实例的评估推迟到获取IOptions.Value时进行,而不是在注入时进行,这样就可以获取不同生命周期的选项;
  • 可以对选项进行泛型约束;

选项注入

选项模式向容器中注入了三种类型的选项泛型包装器:IOptions<>,IOptionsSnapshot<>,IOptionsMonitor<>。其中IOptionsSnapshot<>被注册为Scoped。注入了IOptionsFactory<>泛型选项工厂,用来创建选项实例。注入了IOptionsMonitorCache<>,由IOptionsMonitor<>用于缓存泛型实例。

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; } 

选项不同生命周期

不同类型的选项接口功能不同:

  • IOptions<>:实现类为UnnamedOptionsManager,其中TOptions字段保存选项实例(不支持命名选项)。在首次获取Value时,会调用工厂创建选项实例。因为被注册为单例,因此无法识别配置修改。
  • IOptionsSnapshot<>:实现类为OptionsManager,其中OptionsCache字段(私有,非容器获取)保存选项实例(支持命名选项)。被注册为范围,在范围内首次获取Value时,会调用工厂创建选项实例,并将其保存到私有的OptionsCache中,在范围内选项值不变,不同范围内选项值根据获取时配置文件的不同而不同。
  • IOptionsMonitor<>:实现类为OptionsMonitor,其中IOptionsMonitorCache字段,该字段的值是从容器中解析的,用来缓存选项实例。OptionsMonitor还注入了IOptionsChangeTokenSource列表,可以监听配置源的修改。当监听到修改时,调用工厂重新创建选项以刷新选项值。
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);             }         }     } } 
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>();               public OptionsManager(IOptionsFactory<TOptions> factory)     {         _factory = factory;     }      public TOptions Value => Get(Options.DefaultName);      public virtual TOptions Get(string name)     {         name = name ?? Options.DefaultName;          if (!_cache.TryGetValue(name, out TOptions options))         {             IOptionsFactory<TOptions> localFactory = _factory;             string localName = name;             options = _cache.GetOrAdd(name, () => localFactory.Create(localName));         }          return options;     } } 
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;      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);         }                  // 此处简写         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);     }      public TOptions CurrentValue     {         get => Get(Options.DefaultName);     }      public virtual TOptions Get(string name)     {         name = name ?? Options.DefaultName;         return _cache.GetOrAdd(name, () => _factory.Create(name));     }      public IDisposable OnChange(Action<TOptions, string> listener)     {         var disposable = new ChangeTrackerDisposable(this, listener);         _onChange += disposable.OnChange;         return disposable;     }      public void Dispose()     {         foreach (IDisposable registration in _registrations)         {             registration.Dispose();         }          _registrations.Clear();     } } 

选项配置

选项模式提供了IConfigureOptions<>、IConfigureNamedOptions<>和IPostConfigureOptions<>接口,用来对选项进行配置。IConfigureNamedOptions<>继承了IConfigureOptions<>接口,增加了命名选项配置功能。这三个接口中都有一个对选项配置的方法,将接口注入到容器中,当调用工厂创建选项时,会调用接口中的配置方法对选项进行配置。首先会调用IConfigureOptions<>、IConfigureNamedOptions<>接口中的配置方法,然后调用IPostConfigureOptions<>接口中的配置方法。

// OptionsFactory<> 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; }  protected virtual TOptions CreateInstance(string name) {     return Activator.CreateInstance<TOptions>(); } 

选项验证

选项模式提供了IValidateOptions<>接口,包含一个Validate方法对选项进行验证。将接口注入容器中,当调用工厂创建选项时,会调用接口中的Validate方法对选项进行验证。

// OptionsFactory<> public TOptions Create(string name) {     TOptions options = CreateInstance(name);     // 选项配置...      if (_validations.Length > 0)     {         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; } 

如果需要为选项添加验证,实现IValidateOptions<>接口并注入到容器即可:

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton     <IValidateOptions<SettingsOptions>, ValidateSettingsOptions>()); 

也可以添加DataAnnotations验证,调用ValidateDataAnnotations扩展方法即可,该方法定义在 Microsoft.Extensions.Options.DataAnnotations中。需要先调用AddOptions<>扩展方法创建OptionsBuilder<>:

builder.Services.AddOptions<SettingsOptions>()     .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))     .ValidateDataAnnotations(); 

绑定配置

通过如下扩展方法将选项绑定到配置:

builder.Services.Configure<SettingsOptions>(builder.Configuration.GetSection("Settings")); 

绑定到配置是通过IConfiguration.Bind()扩展方法实现的,同时,也添加了对配置修改的监听:

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)); }