注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

Options绑定

上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊Options,中文译为“选项”,该功能用于实现以强类型的方式对程序配置信息进行访问。

既然是强类型的方式,那么就需要定义一个Options类,该类:

  • 推荐命名规则:{Object}Options
  • 特点:
    • 非抽象类
    • 必须包含公共无参的构造函数
    • 类中的所有公共读写属性都会与配置项进行绑定
    • 字段不会被绑定

接下来,为了便于理解,先举个例子:

首先在 appsetting.json 中添加如下配置:

{
"Book": {
"Id": 1,
"Name": "三国演义",
"Author": "罗贯中"
}
}

然后定义Options类:

public class BookOptions
{
public const string Book = "Book"; public int Id { get; set; } public string Name { get; set; } public string Author { get; set; }
}

最后进行绑定(有BindGet两种方式):

public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services)
{
// 方式 1:
var bookOptions1 = new BookOptions();
Configuration.GetSection(BookOptions.Book).Bind(bookOptions1); // 方式 2:
var bookOptions2 = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
}
}

其中,属性IdTitleAuthor均会与配置进行绑定,但是字段Book并不会被绑定,该字段只是用来让我们避免在程序中使用“魔数”。另外,一定要确保配置项能够转换到其绑定的属性类型(你该不会想把string绑定到int类型上吧)。

如果中文读取出来是乱码,那么你可以按照.L.net core 读取appsettings.json 文件中文乱码的问题来配置一下。

当然,这样写代码还不够完美,还是要将Options添加到依赖注入服务容器中,例如通过IServiceCollection的扩展方法Configure

public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
}
}

Options读取

通过Options接口,我们可以读取依赖注入容器中的Options。常用的有三个接口:

  • IOptions<TOptions>
  • IOptionsSnapshot<TOptions>
  • IOptionsMonitor<TOptions>

接下来,我们看看它们的区别。

IOptions

  • 该接口对象实例生命周期为 Singleton,因此能够将该接口注入到任何生命周期的服务中
  • 当该接口被实例化后,其中的选项值将永远保持不变,即使后续修改了与选项进行绑定的配置,也永远读取不到修改后的配置值
  • 不支持命名选项(Named Options),这个下面会说
public class ValuesController : ControllerBase
{
private readonly BookOptions _bookOptions; public ValuesController(IOptions<BookOptions> bookOptions)
{
// bookOptions.Value 始终是程序启动时加载的配置,永远不会改变
_bookOptions = bookOptions.Value;
}
}

IOptionsSnapshot

  • 该接口被注册为 Scoped,因此该接口无法注入到 Singleton 的服务中,只能注入到 Transient 和 Scoped 的服务中。
  • 在作用域中,创建IOptionsSnapshot<TOptions>对象实例时,会从配置中读取最新选项值作为快照,并在作用域中始终使用该快照。
  • 支持命名选项
public class ValuesController : ControllerBase
{
private readonly BookOptions _bookOptions; public ValuesController(IOptionsSnapshot<BookOptions> bookOptionsSnapshot)
{
// bookOptions.Value 是 Options 对象实例创建时读取的配置快照
_bookOptions = bookOptionsSnapshot.Value;
}
}

IOptionsMonitor

  • 该接口除了可以查看TOptions的值,还可以监控TOptions配置的更改。
  • 该接口被注册为 Singleton,因此能够将该接口注入到任何生命周期的服务中
  • 每次读取选项值时,都是从配置中读取最新选项值(具体读取逻辑查看下方三种接口对比测试)。
  • 支持:
    • 命名选项
    • 重新加载配置(CurrentValue),并当配置发生更改时,进行通知(OnChange
    • 缓存与缓存失效 (IOptionsMonitorCache<TOptions>)
public class ValuesController : ControllerBase
{
private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor; public ValuesController(IOptionsMonitor<BookOptions> bookOptionsMonitor)
{
// _bookOptionsMonitor.CurrentValue 的值始终是最新配置的值
_bookOptionsMonitor = bookOptionsMonitor;
}
}

三种接口对比测试

IOptions<TOptions>就不说了,主要说一下IOptionsSnapshot<TOptions>IOptionsMonitor<TOptions>的不同:

  • IOptionsSnapshot<TOptions> 注册为 Scoped,在创建其实例时,会从配置中读取最新选项值作为快照,并在作用域中使用该快照
  • IOptionsMonitor<TOptions> 注册为 Singleton,每次调用实例的 CurrentValue 时,会先检查缓存(IOptionsMonitorCache<TOptions>)是否有值,如果有值,则直接用,如果没有,则从配置中读取最新选项值,并记入缓存。当配置发生更改时,会将缓存清空。

搞个测试小程序:

[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
private readonly IOptions<BookOptions> _bookOptions;
private readonly IOptionsSnapshot<BookOptions> _bookOptionsSnapshot;
private readonly IOptionsMonitor<BookOptions> _bookOptionsMonitor; public ValuesController(
IOptions<BookOptions> bookOptions,
IOptionsSnapshot<BookOptions> bookOptionsSnapshot,
IOptionsMonitor<BookOptions> bookOptionsMonitor)
{
_bookOptions = bookOptions;
_bookOptionsSnapshot = bookOptionsSnapshot;
_bookOptionsMonitor = bookOptionsMonitor; } [HttpGet]
public dynamic Get()
{
var bookOptionsValue1 = _bookOptions.Value;
var bookOptionsSnapshotValue1 = _bookOptionsSnapshot.Value;
var bookOptionsMonitorValue1 = _bookOptionsMonitor.CurrentValue; Console.WriteLine("请修改配置文件 appsettings.json");
Task.Delay(TimeSpan.FromSeconds(10)).Wait(); var bookOptionsValue2 = _bookOptions.Value;
var bookOptionsSnapshotValue2 = _bookOptionsSnapshot.Value;
var bookOptionsMonitorValue2 = _bookOptionsMonitor.CurrentValue; return new
{
bookOptionsValue1,
bookOptionsSnapshotValue1,
bookOptionsMonitorValue1,
bookOptionsValue2,
bookOptionsSnapshotValue2,
bookOptionsMonitorValue2
};
}
}

运行2次,并按照指示修改两次配置文件(初始是“三国演义”,第一次修改为“水浒传”,第二次修改为“红楼梦”)

  • 第1次输出:
{
"bookOptionsValue1": {
"id": 1,
"name": "三国演义",
"author": "罗贯中"
},
"bookOptionsSnapshotValue1": {
"id": 1,
"name": "三国演义",
"author": "罗贯中"
},
"bookOptionsMonitorValue1": {
"id": 1,
"name": "三国演义",
"author": "罗贯中"
},
"bookOptionsValue2": {
"id": 1,
"name": "三国演义",
"author": "罗贯中"
},
// 注意 OptionsSnapshot 的值在当前作用域内没有进行更新
"bookOptionsSnapshotValue2": {
"id": 1,
"name": "三国演义",
"author": "罗贯中"
}, // 注意 OptionsMonitor 的值变成最新的
"bookOptionsMonitorValue2": {
"id": 1,
"name": "水浒传",
"author": "施耐庵"
}
}
  • 第2次输出:
{
// Options 的值始终没有变化
"bookOptionsValue1": {
"id": 1,
"name": "三国演义",
"author": "罗贯中"
}, // 注意 OptionsSnapshot 的值变成当前最新值了
"bookOptionsSnapshotValue1": {
"id": 1,
"name": "水浒传",
"author": "施耐庵"
},
// 注意 OptionsMonitor 的值始终是最新的
"bookOptionsMonitorValue1": {
"id": 1,
"name": "水浒传",
"author": "施耐庵"
}, // Options 的值始终没有变化
"bookOptionsValue2": {
"id": 1,
"name": "三国演义",
"author": "罗贯中"
},
// 注意 OptionsSnapshot 的值在当前作用域内没有进行更新
"bookOptionsSnapshotValue2": {
"id": 1,
"name": "水浒传",
"author": "施耐庵"
}, // 注意 OptionsMonitor 的值始终是最新的
"bookOptionsMonitorValue2": {
"id": 1,
"name": "红楼梦",
"author": "曹雪芹"
}
}

通过测试我相信你应该能深刻理解它们之间的区别了。

命名选项(Named Options)

上面我们提到了命名选项,命名选项常用于多个配置节点绑定同一属性的情况,举个例子你就明白了:

在 appsettings.json 中添加如下配置

{
"DateTime": {
"Beijing": {
"Year": 2021,
"Month": 1,
"Day":1,
"Hour":12,
"Minute":0,
"Second":0
},
"Tokyo": {
"Year": 2021,
"Month": 1,
"Day":1,
"Hour":13,
"Minute":0,
"Second":0
},
}
}

很显然,虽然“Beijing”和“Tokyo”是两个配置项,但是属性都是一样的,我们没必要创建两个Options类,只需要创建一个就好了:

public class DateTimeOptions
{
public const string Beijing = "Beijing";
public const string Tokyo = "Tokyo"; public int Year { get; set; }
public int Month { get; set; }
public int Day { get; set; }
public int Hour { get; set; }
public int Minute { get; set; }
public int Second { get; set; }
}

然后,通过对选项进行指定命名的方式,一个叫做“Beijing”,一个叫做“Tokyo”,将选项添加到DI容器中:

public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
services.Configure<DateTimeOptions>(DateTimeOptions.Beijing, Configuration.GetSection($"DateTime:{DateTimeOptions.Beijing}"));
services.Configure<DateTimeOptions>(DateTimeOptions.Tokyo, Configuration.GetSection($"DateTime:{DateTimeOptions.Tokyo}"));
}
}

最后,通过构造函数的方式将选项注入到Controller中。需要注意的是,因为DateTimeOptions类绑定了两个选项类,所以当我们获取时选项值时,需要指定选项的名字。

public class ValuesController : ControllerBase
{
private readonly DateTimeOptions _beijingDateTimeOptions;
private readonly DateTimeOptions _tockyoDateTimeOptions; public ValuesController(IOptionsSnapshot<DateTimeOptions> dateTimeOptions)
{
_beijingDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Beijing);
_tockyoDateTimeOptions = dateTimeOptions.Get(DateTimeOptions.Tokyo);
}
}

程序运行后,你会发现变量 _beijingDateTimeOptions 绑定的配置是“Beijing”配置节点,变量 _tockyoDateTimeOptions 绑定的配置是“Tokyo” 配置节点,但它们绑定的都是同一个类DateTimeOptions

事实上,.NET Core 中所有 Options 都是命名选项,当没有显式指定名字时,使用的名字默认是Options.DefaultName,即string.Empty

使用 DI 服务配置选项

在某些场景下,选项的配置需要依赖DI中的服务,这时可以借助OptionsBuilderConfigure方法(注意这个Configure不是上面提到的IServiceCollection的扩展方法Configure,这是两个不同的方法),该方法支持最多5个服务来配置选项:

services.AddOptions<BookOptions>()
.Configure<Service1, Service2, Service3, Service4, Service5>((o, s, s2, s3, s4, s5) =>
{
o.Authors = DoSomethingWith(s, s2, s3, s4, s5);
});

Options 验证

配置毕竟是我们手动进行文本输入的,难免会出现错误,这种情况下,就需要使用程序来帮助进行校验了。

DataAnnotations

Install-Package Microsoft.Extensions.Options.DataAnnotations

我们先升级一下BookOptions,增加一些数据校验:

public class BookOptions
{
public const string Book = "Book"; [Range(1,1000,
ErrorMessage = "必须 {1} <= {0} <= {2}")]
public int Id { get; set; } [StringLength(10, MinimumLength = 1,
ErrorMessage = "必须 {2} <= {0} Length <= {1}")]
public string Name { get; set; } public string Author { get; set; }
}

然后我们在添加到DI容器时,增加数据注解验证:

public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<BookOptions>()
.Bind(Configuration.GetSection(BookOptions.Book))
.ValidateDataAnnotations();
.Validate(options =>
{
// 校验通过 return true
// 校验失败 return false if (options.Author.Contains("A"))
{
return false;
} return true;
});
}

ValidateDataAnnotations会根据你添加的特性进行数据校验,当特性无法实现想要的校验逻辑时,则使用Validate进行较为复杂的校验,如果过于复杂,则就要用到IValidateOptions了(实质上,Validate方法内部也是通过注入一个IValidateOptions实例来实现选项验证的)。

IValidateOptions

通过实现IValidateOptions<TOptions>接口,增加数据校验规则,例如:

public class BookValidation : IValidateOptions<BookOptions>
{
public ValidateOptionsResult Validate(string name, BookOptions options)
{
var failures = new List<string>();
if(!(options.Id >= 1 && options.Id <= 1000))
{
failures.Add($"必须 1 <= {nameof(options.Id)} <= {1000}");
}
if(!(options.Name.Length >= 1 && options.Name.Length <= 10))
{
failures.Add($"必须 1 <= {nameof(options.Name)} <= 10");
} if (failures.Any())
{
return ValidateOptionsResult.Fail(failures);
} return ValidateOptionsResult.Success;
}
}

然后我们将其注入到DI容器 Singleton,这里使用了TryAddEnumerable扩展方法添加该服务,是因为我们可以注入多个针对同一Options的IValidateOptions,这些IValidateOptions实例都会被执行:

public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(Configuration.GetSection(BookOptions.Book));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<BookOptions>, BookValidation>());
}

Options后期配置

介绍两个方法,分别是PostConfigurePostConfigureAll,他们用来对选项进行后期配置。

  • 在所有的OptionsServiceCollectionExtensions.Configure方法运行后执行
  • ConfigureConfigureAll类似,PostConfigure仅用于对指定名称的选项进行后期配置(默认名称为string.Empty),PostConfigureAll则用于对所有选项实例进行后期配置
  • 每当选项更改时,均会触发相应的方法
public void ConfigureServices(IServiceCollection services)
{
services.PostConfigure<DateTimeOptions>(options =>
{
Console.WriteLine($"我只对名称为{Options.DefaultName}的{nameof(DateTimeOptions)}实例进行后期配置");
}); services.PostConfigure<DateTimeOptions>(DateTimeOptions.Beijing, options =>
{
Console.WriteLine($"我只对名称为{DateTimeOptions.Beijing}的{nameof(DateTimeOptions)}实例进行后期配置");
}); services.PostConfigureAll<DateTimeOptions>(options =>
{
Console.WriteLine($"我对{nameof(DateTimeOptions)}的所有实例进行后期配置");
});
}

Options 体系

IConfigureOptions

该接口用于包装对选项的配置。默认实现为ConfigureOptions<TOptions>

public interface IConfigureOptions<in TOptions> where TOptions : class
{
void Configure(TOptions options);
}

ConfigureOptions

public class ConfigureOptions<TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
public ConfigureOptions(Action<TOptions> action)
{
Action = action;
} public Action<TOptions> Action { get; } // 配置 TOptions 实例
public virtual void Configure(TOptions options)
{
Action?.Invoke(options);
}
}

ConfigureFromConfigurationOptions

该类通过继承类ConfigureOptions<TOptions>,对选项的配置进行了扩展,允许通过ConfigurationBinder.Bind扩展方法将IConfiguration实例绑定到选项上:

public class ConfigureFromConfigurationOptions<TOptions> : ConfigureOptions<TOptions>
where TOptions : class
{
public ConfigureFromConfigurationOptions(IConfiguration config)
: base(options => ConfigurationBinder.Bind(config, options))
{ }
}

IConfigureNamedOptions

该接口用于包装对命名选项的配置,该接口同时继承了接口IConfigureOptions<TOptions>的行为,默认实现为ConfigureNamedOptions<TOptions>,另外为了实现“使用 DI 服务配置选项”的功能,还提供了一些泛型类重载。

public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
void Configure(string name, TOptions options);
}

ConfigureNamedOptions

public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
{
public ConfigureNamedOptions(string name, Action<TOptions> action)
{
Name = name;
Action = action;
} public string Name { get; } public Action<TOptions> Action { get; } public virtual void Configure(string name, TOptions options)
{
// Name == null 表示针对 TOptions 的所有实例进行配置
if (Name == null || name == Name)
{
Action?.Invoke(options);
}
} public void Configure(TOptions options) => Configure(Options.DefaultName, options);
}

NamedConfigureFromConfigurationOptions

该类通过继承类ConfigureNamedOptions<TOptions>,对命名选项的配置进行了扩展,允许通过ConfigurationBinder.Bind扩展方法将IConfiguration实例绑定到命名选项上:

public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions>
where TOptions : class
{
public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
: this(name, config, _ => { })
{ } public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action<BinderOptions> configureBinder)
: base(name, options => config.Bind(options, configureBinder))
{ }
}

IPostConfigureOptions

该接口用于包装对命名选项的后期配置,将在所有IConfigureOptions<TOptions>执行完毕后才会执行,默认实现为PostConfigureOptions<TOptions>,同样的,为了实现“使用 DI 服务对选项进行后期配置”的功能,也提供了一些泛型类重载:

public interface IPostConfigureOptions<in TOptions> where TOptions : class
{
void PostConfigure(string name, TOptions options);
} public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class
{
public PostConfigureOptions(string name, Action<TOptions> action)
{
Name = name;
Action = action;
} public string Name { get; } public Action<TOptions> Action { get; } public virtual void PostConfigure(string name, TOptions options)
{
// Name == null 表示针对 TOptions 的所有实例进行后期配置
if (Name == null || name == Name)
{
Action?.Invoke(options);
}
}
}

AddOptions & AddOptions & OptionsBuilder

public static class OptionsServiceCollectionExtensions
{
// 该方法帮我们把一些常用的与 Options 相关的服务注入到 DI 容器
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 名称时,默认使用 Options.DefaultName
public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services) where TOptions : class
=> services.AddOptions<TOptions>(Options.Options.DefaultName); // 由于后续还要对 TOptions 进行配置,所以返回一个 OptionsBuilder 出去
public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name)
where TOptions : class
{
services.AddOptions();
return new OptionsBuilder<TOptions>(services, name);
}
}

那我们看看OptionsBuilder<TOptions>可以配置哪些东西,由于该类中有大量重载方法,我只挑选最基础的方法来看一看:

public class OptionsBuilder<TOptions> where TOptions : class
{
private const string DefaultValidationFailureMessage = "A validation error has occurred."; // TOptions 实例的名字
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;
}
}

OptionsServiceCollectionExtensions.Configure

OptionsServiceCollectionExtensions.Configure<TOptions>实际上就是对选项的一般配置方式进行了封装,免去了OptionsBuilder<TOptions>

public static class OptionsServiceCollectionExtensions
{
// 没有指定 Options 名称时,默认使用 Options.DefaultName
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
=> services.Configure(Options.Options.DefaultName, configureOptions); // 等同于做了 AddOptions<TOptions> 和 OptionsBuilder<TOptions>.Configure 两件事
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;
} // 由于 ConfigureAll 是针对 TOptions 的所有实例进行配置,所以不需要指定名字
public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
=> services.Configure(name: null, configureOptions: configureOptions);
}

OptionsConfigurationServiceCollectionExtensions.Configure

请注意,该Configure<TOptions>方法与上方提及的Configure<TOptions>不是同一个。该扩展方法针对配置(IConfiguration)绑定到选项(Options)上进行了扩展

Install-Package Microsoft.Extensions.Options.ConfigurationExtensions

public static class OptionsConfigurationServiceCollectionExtensions
{
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
=> services.Configure<TOptions>(Options.Options.DefaultName, config); public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class
=> services.Configure<TOptions>(name, config, _ => { }); public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config, Action<BinderOptions> configureBinder)
where TOptions : class
=> services.Configure<TOptions>(Options.Options.DefaultName, config, configureBinder); 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));
}
}

IOptionsFactory

IOptionsFactory<TOptions>负责创建命名选项实例,默认实现为OptionsFactory<TOptions>

public interface IOptionsFactory<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOptions> where TOptions : class
{
TOptions Create(string name);
} public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions>
: IOptionsFactory<TOptions> where TOptions : class
{
private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
private readonly IEnumerable<IValidateOptions<TOptions>> _validations; // 这里通过依赖注入的的方式将与 TOptions 相关的配置、验证服务列表解析出来
public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures)
: this(setups, postConfigures, validations: null)
{ } public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
{
_setups = setups;
_postConfigures = postConfigures;
_validations = validations;
} public TOptions Create(string name)
{
// 1. 创建并配置 Options
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);
}
} // 2. 对 Options 进行后期配置
foreach (IPostConfigureOptions<TOptions> post in _postConfigures)
{
post.PostConfigure(name, options);
} // 3. 执行 Options 校验
if (_validations != null)
{
var failures = new List<string>();
foreach (IValidateOptions<TOptions> validate in _validations)
{
ValidateOptionsResult 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;
} protected virtual TOptions CreateInstance(string name)
{
return Activator.CreateInstance<TOptions>();
}
}

OptionsManager

通过AddOptions扩展方法的实现,可以看到,IOptions<TOptions>IOptionsSnapshot<TOptions>的实现都是OptionsManager<TOptions>,只不过一个是 Singleton,一个是 Scoped。我们通过前面的分析也知道了,当源中的配置改变时,IOptions<TOptions>始终维持初始值,IOptionsSnapshot<TOptions>在每次请求时会读取最新配置值,并在同一个请求中是不变的。接下来就来看看OptionsManager<TOptions>是如何实现的:

public class OptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptions<TOptions>,
IOptionsSnapshot<TOptions>
where TOptions : class
{
private readonly IOptionsFactory<TOptions> _factory;
// 将已创建的 TOptions 实例缓存到该私有变量中
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; // 若缓存不存在,则通过工厂新建 Options 实例,否则直接读取缓存
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
}

OptionsMonitor

同样,通过前面的分析,我们知道OptionsMonitor<TOptions>读取的始终是配置的最新值,它的实现在OptionsManager<TOptions>的基础上,除了使用缓存将创建的 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 IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
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;
_sources = sources;
_cache = cache; // 监听更改
foreach (IOptionsChangeTokenSource<TOptions> source in _sources)
{
IDisposable registration = ChangeToken.OnChange(
() => source.GetChangeToken(),
(name) => InvokeChanged(name),
source.Name); _registrations.Add(registration);
}
} // 当发生更改时,移除缓存
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(Options.DefaultName); public virtual TOptions Get(string name)
{
name = name ?? Options.DefaultName;
return _cache.GetOrAdd(name, () => _factory.Create(name));
} // 通过该方法绑定 OnChange 事件
public IDisposable OnChange(Action<TOptions, string> listener)
{
var disposable = new ChangeTrackerDisposable(this, listener);
_onChange += disposable.OnChange;
return disposable;
} public void Dispose()
{
// 移除所有 change token 的订阅
foreach (IDisposable registration in _registrations)
{
registration.Dispose();
} _registrations.Clear();
}
}

总结

  • 所有选项均为命名选项,默认名称为Options.DefaultName,即string.Empty
  • 通过ConfigurationBinder.GetConfigurationBinder.Bind手动获取选项实例。
  • 通过Configure方法进行选项配置:
    • OptionsBuilder<TOptions>.Configure:通过包含DI服务的委托来进行选项配置
    • OptionsServiceCollectionExtensions.Configure<TOptions>:通过简单委托来进行选项配置
    • OptionsConfigurationServiceCollectionExtensions.Configure<TOptions>:直接将IConfiguration实例绑定到选项上
  • 通过OptionsServiceCollectionExtensions.ConfigureAll<TOptions>方法针对某个选项类型的所有实例(不同名称)统一进行配置。
  • 通过PostConfigure方法进行选项后期配置:
    • OptionsBuilder<TOptions>.PostConfigure:通过包含DI服务的委托来进行选项后期配置
    • OptionsServiceCollectionExtensions.PostConfigure<TOptions>:通过简单委托来进行选项后期配置
  • 通过PostConfigureAll<TOptions>方法针对某个选项类型的所有实例(不同名称)统一进行配置。
  • 通过Validate进行选项验证:
    • OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations:通过数据注解进行选项验证
    • OptionsBuilder<TOptions>.Validate:通过委托进行选项验证
    • IValidateOptions<TOptions>:通过实现该接口并注入实现来进行选项验证
  • 通过依赖注入读取选项:
    • IOptions<TOptions>:Singleton,值永远是该接口被实例化时的选项配置初始值
    • IOptionsSnapshot<TOptions>:Scoped,每一次Http请求开始时会读取选项配置的最新值,并在当前请求中保持不变
    • IOptionsMonitor<TOptions>:Singleton,每次读取都是选项配置的最新值

理解ASP.NET Core - 选项(Options)的更多相关文章

  1. asp.net core选项Options模块的笔记

    这篇博客是写给自己看的.已经不止一次看到AddOptions的出现,不管是在.net core源码还是别人的框架里面,都充斥着AddOptions.于是自己大概研究了下,没有深入,因为,我的功力还是不 ...

  2. 理解ASP.NET Core - 配置(Configuration)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 配置提供程序 在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种: 文件配置提供程 ...

  3. 理解ASP.NET Core - 文件服务器(File Server)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 提供静态文件 静态文件默认存放在 Web根目录(Web Root) 中,路径为 项目根目录(C ...

  4. 理解ASP.NET Core - 基于Cookie的身份认证(Authentication)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 概述 通常,身份认证(Authentication)和授权(Authorization)都会放 ...

  5. 理解ASP.NET Core - 基于JwtBearer的身份认证(Authentication)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 在开始之前,如果你还不了解基于Cookie的身份认证,那么建议你先阅读<基于Cookie ...

  6. 理解ASP.NET Core - 授权(Authorization)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 之前,我们已经了解了ASP.NET Core中的身份认证,现在,我们来聊一下授权. 老规矩,示 ...

  7. 理解ASP.NET Core - 发送Http请求(HttpClient)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 前言 在.NET中,我们有很多发送Http请求的手段,如HttpWebRequest.WebC ...

  8. 理解ASP.NET Core - [01] Startup

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...

  9. 目录-理解ASP.NET Core

    <理解ASP.NET Core>基于.NET5进行整理,旨在帮助大家能够对ASP.NET Core框架有一个清晰的认识. 目录 [01] Startup [02] Middleware [ ...

随机推荐

  1. TCP请求连接与断开

    TCP连接的三次握手:

  2. jQuery中ajax请求的六种方法(三、五):$.getScript()方法

    5.$.getScript()方法 <!DOCTYPE html> <html> <head> <meta charset="UTF-8" ...

  3. Git修改历史commit的author信息

    前言 "嘀嗒嘀嗒",抬头看向墙上的钟表,此时已是凌晨1点.小明终于把Go语言圣经第二章的笔记写完,保存commit,提交,然后睡觉. 额,等等,不对,小明发现他用的是公司的git账 ...

  4. Gogs (Go git server) 使用笔记

    issue: 话题,一个新特性,BUG或其他关注的任何话题,都可创建issure,便于讨论,明确目标. label: 标签,一般用于描述issue的类型,如:bug.feature.enhanceme ...

  5. 办公室文员必备python神器,将PDF文件表格转换成excel表格!

    [阅读全文] 第三方库说明 # PDF读取第三方库 import pdfplumber # DataFrame 数据结果处理 import pandas as pd 初始化DataFrame数据对象 ...

  6. 使用Keepalived实现Nginx的双机主备高可用

    1.概述 前面我们聊过使用 Nginx 为 后端Tomcat 做负载均衡.高可用,但是这时Nginx又成了单点,如果Nginx不幸挂掉,整个网站便无法访问. 此时我们就会用到另一个软件 -- Keep ...

  7. 你的 SQL 还在回表查询吗?快给它安排覆盖索引

    什么是回表查询 小伙伴们可以先看这篇文章了解下什么是聚集索引和辅助索引:Are You OK?主键.聚集索引.辅助索引,简单回顾下,聚集索引的叶子节点包含完整的行数据,而非聚集索引的叶子节点存储的是每 ...

  8. Mybatis-plus<二>通用CRUD,分页

    Mybatis-plus<二>通用CRUD,分页 与博客Mybatis-plus<一>为同一个Springboot项目. Demo GitHub下载地址:https://git ...

  9. 性能测试工具JMeter 基础(四)—— 录制脚本

    对于JMeter中HTTP请求除了手动添加以为还可以进行脚本录制,有两个方法: 使用badboy录制,录制完成后,将录制的文件导入JMeter中 使用JMeter自带的录制原件进行录制(HTTP(S) ...

  10. openswan IPSec专栏目录锦集

    为了方便查阅现有的文章,特准备一个目录页供后续查询使用 专栏序言 1. 基础知识 openswan任务调度基础知识之信号 2. openswan环境搭建 openswan框架和编译时说明 opensw ...