在 .NET 中,配置与选项模式其实有联系的(这些功能现在不仅限于 ASP.NET Core,而是作为平台扩展来提供,在其他.NET 项目中都能用)。配置一般从多个来源(上一篇水文中的例子,记得否?)来读取数据,最后以 Key - Value 的方式加载到应用程序中,然后应用程序可以读取配置。这些来源有 JSON文件、XML文件等。上次老周还演示了 CSV 文件。
而选项模式呢,说直白些就是一些简单的类,多数情况下只定义些公共属性,可以称为选项类(从泛型约束而言,选项类的要求一般就是 class,也就是类型是类的就行)。这些类其根本作用也是用来配置应用程序的,只是它们以面向对象的方式把配置信息封装起来。也就是说,选项类可以与配置信息做绑定。
咱们在定义选项类的时候,习惯让类名以“Options”结尾。这也不是什么硬规则,只不过易于理解罢了。你看到某个类以“Options”结尾,你就可以猜到它的用处——设定选项参数用的,其实也就是配置。
假设有这么个类。
public class TestOptions { public int Key1 { get; set; } public string? Key2 { get; set; } }
看着很是简单,就两个属性。
在 appsettings.json 文件中,我们可以加上这样一个节点:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "myConfig": { "key1": 5055, "key2": "Speaking Chinglish" } }
如你所见,“myConfig” 节点对应的那个 JObject 的属性结构和 TestOptions 类相同。
appsettings.json 在应用程序初始化时是自动添加的,我们不需要再配置,直接改文件就行,不用再添代码。IConfiguration 接口有个扩展方法 Bind,可以将配置信息与某个类型对象直接绑定。
app.MapGet("/", () => { IConfigurationRoot config = (IConfigurationRoot)app.Configuration; // 找出我们要的节 IConfigurationSection myconfig = config.GetSection("myConfig"); TestOptions options = new(); // 直接绑定 myconfig.Bind(options); // 看看结果 return $"Key1 = {options.Key1}\nKey2 = {options.Key2}"; });
从 JSON 文件读出的配置信息可以直接与 TestOptions 实例绑定。这样,在代码运行后,能看到咱们刚刚在 JSON 文件中设置的内容。

上面这种方法虽然能做到了绑定,但用起来还费劲。为啥不直接弄进服务容器中,来个依赖注入,岂不美哉?
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.Configure(builder.Configuration.GetSection("myConfig" )); var app = builder.Build();
Configure
这里注意在依赖注入时,我们不是直接用 TestOptions 类型,而是 IOptions
public class HomeController : Controller { readonly TestOptions _options; // 构造函数,获取注入的对象 public HomeController(IOptionswrapper) { _options = wrapper.Value; } public IActionResult Index() { // 这里访问选项 string s = $"Key1: {_options.Key1}\n"; s += $"Key2: {_options.Key2}"; return Content(s); } }
其实,咱们可以用的接口类型并不只有 IOptions<>,不妨看看 AddOptions 扩展方法的源代码(OptionsServiceCollectionExtensions.cs)
public static IServiceCollection AddOptions(this IServiceCollection services) { ThrowHelper.ThrowIfNull(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<> 接口,在容器中注册时用的就是这个接口嘛。其实,你可以用 IOptionsMonitor
AddOptions 方法我们一般不需要调用,ASP.NET Core 应用程序初始化时自动调用了。
为了使选项模式有更好的适用性,我们也可以通过委托来配置选项类。
builder.Services.Configure(opt => { opt.Key1 = 999; opt.Key2 = "Hi, baby"; });
至此,Configure 方法的两种用法就出来了:
1、用 IConfigure 对象,从配置信息源中加载,并填充选项类的属性值(这个挺像反序列化操作);
2、直接用委托。
不管是配置信息还是委托,Configure 方法只是向服务容器添加了一组 IConfigureOptions
我们以 ConfigureOptions
public class ConfigureOptions: IConfigureOptions where TOptions : class { public ConfigureOptions(Action ? action) { Action = action; } public Action? Action { get; } public virtual void Configure(TOptions options) { …… Action?.Invoke(options); } }
看到构造函数时,你有没有发现点什么?看,它是不是有个委托 Action
builder.Services.Configure(opt => { opt.Key1 = 999; opt.Key2 = "Hi, baby"; });
对的,我们传给它的委托就是传到了 ConfigureOptions 的构造函数里,ConfigureOptions 实现的就是 IConfigureOptions
这只是放进了容器中罢了,并没有马上执行我们写的委托。
那,它们在哪里被调用呢?在工厂里面——IOptionsFactory
public interface IOptionsFactorywhere TOptions : class { TOptions Create(string name); }
别小看这货,它可是核心角色。选项类的实例就是由它来创建的(实现 Create 方法)。你会注意到,这里有个 name 参数,干吗的?这个是为选项的命名分组准备。一般可以不理会它,除非你要实现不同分组产生不太一样的选项类实例,这样就用得上了。
其实,如果需要,我们不妨为 TestOptions 类写个小工厂(家庭小作坊)。
public class TestOptionsFactory : IOptionsFactory{ public TestOptions Create(string name) { return new TestOptions(); } }
简单吧,然后咱用呢,不用管它咋用,你只要注册到服务容器中就行了。
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddTransient, TestOptionsFactory>() ;
这里我注册为暂时性服务,你也可以考虑注册为其他的,不冲突就行。
你要是有疑问,我自定义的工厂它会执行吗?会的,不信打个断点看看。

然后运行程序,瞧,这不,停下来了。

可,可,可是,这TNND,为什么是默认值,我刚刚不是 Configure 用委托设置了吗?

builder.Services.Configure(opt => { opt.Key1 = 999; opt.Key2 = "Hi, baby"; });
无效?出啥故障了?等等,刚刚是不是说了,Configure 方法每调用一次就会注册一个 IConfigureOptions
想起来了,所以我们的工厂要改造一下,通过依赖注入把这一堆 IConfigureOptions 弄进来,然后逐个调用,这样就可以为选项类的属性赋值了。
public class TestOptionsFactory : IOptionsFactory{ readonly IEnumerable > _configs; public TestOptionsFactory(IEnumerable> configs) { _configs = configs; } public TestOptions Create(string name) { TestOptions opt = new(); foreach(var cfg in _configs) { cfg.Configure(opt); } return opt; } }
看,这就成了。

其实,工厂所需要的依赖对象并不只有 IConfigureOptions,还有两个接口,需要了解一下:
1、IPostConfigureOptions:这货史称“后期配置”。其用法和 IConfigureOptions 一样的。对应的服务容器扩展方法是 PostConfigure
public class ProgressOptions { public int Max { get; set; } public int Min { get; set; } public int Current { get; set; } }
就算我在工厂类中实例化时分配 Max = 100, Min = 0, Current = 50。那,如果我配置的时候是这样的呢:
builder.Services.Configure(o => { o.Max = 60; o.Min = 0; });
假如我没设置 Current 属性,默认规则是它取中值,那这时候取中值 50 显然就不行了。而是得根据 Max 和 Min 的值来决定。这时候用后期配置就很必要了。
builder.Services.PostConfigure(o => { o.Current = (o.Max - o.Min) / 2; });
2、IValidateOptions:这货用来做验证用的。这个和上面的“修修补补”不同。验证是检查选项类的属性值是否符合要求。如果不符合,直接给你来个异常——回炉重造。
我们不妨看看默认的工作是怎么实现的。
public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : IOptionsFactorywhere TOptions : class { private readonly IConfigureOptions [] _setups; private readonly IPostConfigureOptions [] _postConfigures; private readonly IValidateOptions [] _validations; public OptionsFactory(IEnumerable > setups, IEnumerable > postConfigures) : this(setups, postConfigures, validations: Array.Empty >()) { } public OptionsFactory(IEnumerable > setups, IEnumerable validations) { _setups = setups as IConfigureOptions> postConfigures, IEnumerable > [] ?? new List >(setups).ToArray(); _postConfigures = postConfigures as IPostConfigureOptions [] ?? new List >(postConfigures).ToArray(); _validations = validations as IValidateOptions [] ?? new List >(validations).ToArray(); } public TOptions Create(string name) { TOptions options = CreateInstance(name); foreach (IConfigureOptions setup in _setups) { if (setup is IConfigureNamedOptionsnamedSetup) { namedSetup.Configure(name, options); } else if (name == Options.DefaultName) { setup.Configure(options); } } foreach (IPostConfigureOptions post in _postConfigures) { post.PostConfigure(name, options); } if (_validations.Length > 0) { var failures = new List<string>(); foreach (IValidateOptionsvalidate 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; } ////// 这里是创建选项类实例的地方,为了通用化,它用了 Activator /// protected virtual TOptions CreateInstance(string name) { return Activator.CreateInstance (); } }
从其源码我们知道它们的执行顺序:
N多个IConfigureOptions ---> N多个IPostConfigureOptions ---> N多个IValidateOptions。
好了,下面咱们解决最后一个疑问:选项类的实例怎么跑到 IOptions
这里老周只说一对服务类型,其他的实现类似。在 AddOptions 扩展方法中,通过注册的服务类型得知,IOptions<> 对应的是 UnnamedOptionsManager。这个类没有公开,它的源码如下:
internal sealed class UnnamedOptionsManager: IOptions where TOptions : class { private readonly IOptionsFactory _factory; private volatile object? _syncObj; private volatile TOptions? _value; public UnnamedOptionsManager(IOptionsFactory 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); } } } }
现在知道它们怎么关联起来了吧——还是一样的套路,用依赖注入。
经过老周上文一系列胡说八道,这些与选项模式有关的接口之间的关系就清晰了。
