在 .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<TestOptions>(builder.Configuration.GetSection("myConfig"));
var app = builder.Build();

Configure<TOptions> 是个扩展方法,它用来对选项类进行配置——就是为选项类的属性赋值。选项模式不仅限于把配置信息强类型化,它通用于应用程序内各种功能配置。如日志怎么记录、验证策略、Cookie 策略等。

这里注意在依赖注入时,我们不是直接用 TestOptions 类型,而是 IOptions<TOptions> 接口。比如,我们可以在 MVC 控制器中这样玩:

    public class HomeController : Controller
{
readonly TestOptions _options; // 构造函数,获取注入的对象
public HomeController(IOptions<TestOptions> wrapper)
{
_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<TestOptions>。至于这几个接口有啥区别,我们先不管,下一个世纪再告诉你。

AddOptions 方法我们一般不需要调用,ASP.NET Core 应用程序初始化时自动调用了。

为了使选项模式有更好的适用性,我们也可以通过委托来配置选项类。

builder.Services.Configure<TestOptions>(opt =>
{
opt.Key1 = 999;
opt.Key2 = "Hi, baby";
});

至此,Configure 方法的两种用法就出来了:

1、用 IConfigure 对象,从配置信息源中加载,并填充选项类的属性值(这个挺像反序列化操作);

2、直接用委托。

不管是配置信息还是委托,Configure 方法只是向服务容器添加了一组 IConfigureOptions<TOptions> 对象。它的实现类型有 ConfigureOptions<TOptions>、ConfigureNamedOptions<TOptions> 等。

我们以 ConfigureOptions<TOptions> 类为例分析一下,因为这个类比较简单

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

看到构造函数时,你有没有发现点什么?看,它是不是有个委托 Action<TOptions>,我们再回头看看刚刚我们在服务容器调用的 Configure 扩展方法,它是不是有一个重载版本也有个 Action<TOptions> 的参数。

builder.Services.Configure<TestOptions>(opt =>
{
opt.Key1 = 999;
opt.Key2 = "Hi, baby";
});

对的,我们传给它的委托就是传到了 ConfigureOptions 的构造函数里,ConfigureOptions 实现的就是 IConfigureOptions<TOptions> 接口。哦,原来是籍样子滴。我们每调用一次 Configure 方法,它就向容器注册一个 IConfigureOptions。

这只是放进了容器中罢了,并没有马上执行我们写的委托。

那,它们在哪里被调用呢?在工厂里面——IOptionsFactory<TOptions>。

    public interface IOptionsFactory<TOptions>
where TOptions : class
{
TOptions Create(string name);
}

别小看这货,它可是核心角色。选项类的实例就是由它来创建的(实现 Create 方法)。你会注意到,这里有个 name 参数,干吗的?这个是为选项的命名分组准备。一般可以不理会它,除非你要实现不同分组产生不太一样的选项类实例,这样就用得上了。

其实,如果需要,我们不妨为 TestOptions 类写个小工厂(家庭小作坊)。

    public class TestOptionsFactory : IOptionsFactory<TestOptions>
{
public TestOptions Create(string name)
{
return new TestOptions();
}
}

简单吧,然后咱用呢,不用管它咋用,你只要注册到服务容器中就行了。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddTransient<IOptionsFactory<TestOptions>, TestOptionsFactory>();

这里我注册为暂时性服务,你也可以考虑注册为其他的,不冲突就行。

你要是有疑问,我自定义的工厂它会执行吗?会的,不信打个断点看看。

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

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

builder.Services.Configure<TestOptions>(opt =>
{
opt.Key1 = 999;
opt.Key2 = "Hi, baby";
});

无效?出啥故障了?等等,刚刚是不是说了,Configure 方法每调用一次就会注册一个 IConfigureOptions<TOptions> 吗?

想起来了,所以我们的工厂要改造一下,通过依赖注入把这一堆 IConfigureOptions 弄进来,然后逐个调用,这样就可以为选项类的属性赋值了。

    public class TestOptionsFactory : IOptionsFactory<TestOptions>
{
readonly IEnumerable<IConfigureOptions<TestOptions>> _configs;
public TestOptionsFactory(IEnumerable<IConfigureOptions<TestOptions>> 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<TOptions>,用法和 Configure<TOptions> 扩展方法完全一样的。它的用途是进行选项配置后做一些“修修补补”。比如,看看你还有哪些属性没赋值的,给它安排个默认值。有人会说,那我在创建选项类实例时就为每个属性分配默认值不就行了吗?还要啥后期配置?举个例子:

    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<ProgressOptions>(o =>
{
o.Max = 60;
o.Min = 0;
});

假如我没设置 Current 属性,默认规则是它取中值,那这时候取中值 50 显然就不行了。而是得根据 Max 和 Min 的值来决定。这时候用后期配置就很必要了。

builder.Services.PostConfigure<ProgressOptions>(o =>
{
o.Current = (o.Max - o.Min) / 2;
});

2、IValidateOptions:这货用来做验证用的。这个和上面的“修修补补”不同。验证是检查选项类的属性值是否符合要求。如果不符合,直接给你来个异常——回炉重造。

我们不妨看看默认的工作是怎么实现的。

    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; public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, validations: Array.Empty<IValidateOptions<TOptions>>())
{ } public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
{
_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();
} 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.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;
} /// <summary>
/// 这里是创建选项类实例的地方,为了通用化,它用了 Activator
/// </summary>
protected virtual TOptions CreateInstance(string name)
{
return Activator.CreateInstance<TOptions>();
}
}

从其源码我们知道它们的执行顺序:

N多个IConfigureOptions ---> N多个IPostConfigureOptions ---> N多个IValidateOptions。

好了,下面咱们解决最后一个疑问:选项类的实例怎么跑到 IOptions<TOptions> 里面的?通过前面的例子,咱们都知道,通过依赖注入访问选项类时,是通过 IOptions<> 等接口的。咋关联起来的?还是跟 Factory 有关,不然就不会说它是核心角色。

这里老周只说一对服务类型,其他的实现类似。在 AddOptions 扩展方法中,通过注册的服务类型得知,IOptions<> 对应的是 UnnamedOptionsManager。这个类没有公开,它的源码如下:

    internal sealed class UnnamedOptionsManager<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);
}
}
}
}

现在知道它们怎么关联起来了吧——还是一样的套路,用依赖注入。

经过老周上文一系列胡说八道,这些与选项模式有关的接口之间的关系就清晰了。

【ASP.NET Core】选项模式的相关接口的更多相关文章

  1. ASP.NET Core 选项模式源码学习Options Configure(一)

    前言 ASP.NET Core 后我们的配置变得更加轻量级了,在ASP.NET Core中,配置模型得到了显著的扩展和增强,应用程序配置可以存储在多环境变量配置中,appsettings.json用户 ...

  2. Asp.Net Core 选项模式的三种注入方式

    前言 记录下最近在成都的面试题, 选项模式的热更新, 没答上来 正文 选项模式的依赖注入共有三种接口, 分别是 IOptions<>, IOptionsSnapshot<>, ...

  3. ASP.NET Core 选项模式源码学习Options IOptionsMonitor(三)

    前言 IOptionsMonitor 是一种单一示例服务,可随时检索当前选项值,这在单一实例依赖项中尤其有用.IOptionsMonitor用于检索选项并管理TOption实例的选项通知, IOpti ...

  4. ASP.NET Core 选项模式源码学习Options IOptions(二)

    前言 上一篇文章介绍IOptions的注册,本章我们继续往下看 IOptions IOptions是一个接口里面只有一个Values属性,该接口通过OptionsManager实现 public in ...

  5. ASP.NET Core WebApi基于JWT实现接口授权验证

    一.ASP.Net Core WebApi JWT课程前言 我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再 ...

  6. .NET Core 选项模式【Options】的使用

    ASP.NET Core引入了Options模式,使用类来表示相关的设置组.简单的来说,就是用强类型的类来表达配置项,这带来了很多好处.利用了系统的依赖注入,并且还可以利用配置系统.它使我们可以采用依 ...

  7. 理解ASP.NET Core - 选项(Options)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Options绑定 上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊O ...

  8. Asp.Net Core Options模式的知识总结

    Options模式是Asp.Net Core中用于配置的一种模式,它利用了系统的依赖注入,并且还可以利用配置系统.它使我们可以采用依赖注入的方法直接使用绑定的一个POCO对象,这个POCO对象就叫做O ...

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

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

随机推荐

  1. 打造一款高逼格的Vim神器

    点击上方"开源Linux",选择"设为星标" 回复"学习"获取独家整理的学习资料! 作者:枫上雾棋 链接:https://segmentfa ...

  2. 8 种常见 SQL 错误用法

    点击上方"开源Linux",选择"设为星标"回复"学习"获取独家整理的学习资料! 1.LIMIT 语句 分页查询是最常用的场景之一,但也通常 ...

  3. APP应用前端开发

    1.开发手机APP前端要重视meta标签的编写: 2.注意HTML5标签在前端开发中的使用: 3.前端制作要舍弃CSS float属性(可flex布局),用绝对定位不利于页面布局的扩展: 4.APP前 ...

  4. (Bezier)贝塞尔曲在路径规划的运用

    前言 之前被安排了活,一个局部区域机器运动控制的工作,大致是一个机器位于一个极限区域时候,机器要进入一个特殊的机制,使得机器可以安全的走出来.其中用到了bezier曲线进行优化路径,今天写一下,正好也 ...

  5. 程序包 applets.user.service.UserService 不存在-2022新项目

    一.问题由来 接上一篇文章使用maven进行打包时报中文乱码错误,经过多次尝试后最终解决问题,显示出真正的错误信息如下: 程序包 applets.user.service.UserService 不存 ...

  6. 6.Docker网络

    什么是 Docker网络 docker 不启动,默认网络情况 ens33 lo virbr0 在 CentOS7 的安装过程中如果有选择相关虚拟化的的服务安装系统后,启动网卡时会发现有一个以网桥连接的 ...

  7. 虚拟机:KVM

    1. KVM 介绍 1.0 虚拟化简史 其中,KVM 全称是 基于内核的虚拟机(Kernel-based Virtual Machine),它是Linux 的一个内核模块,该内核模块使得 Linux ...

  8. MMDeploy安装笔记

    MMDeploy的TensorRT教程 Step1: 创建虚拟环境并且安装MMDetection conda create -n openmmlab python=3.7 -y conda activ ...

  9. HTML行内元素与块级元素有哪些及区别详解

    转自 https://www.jb51.net/web/724286.html   这篇文章主要介绍了HTML行内元素与块级元素有哪些及区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具 ...

  10. Dubbo3 源码系列 -- 环境准备

    Dubbo3 源码系列 -- 环境准备 前言 工作中一直使用Dubbo项目,借着这次机会通过源码的方式来学习下Dubbo的源码内容.目前市面上很多都是的Dubbo2系列的教程:就连目前的Dubbo的官 ...