Net6 Configuration & Options 源码分析 Part2 Options

第二部分主要记录Options 模型

OptionsConfigurationServiceCollectionExtensions类提供了对Options 模型配置系统的Configure方法的扩展

1. 直接使用Options

直接使用Options

在Starup ConfigService中经常会看到把一个拉姆达注册成配置项例如:.Configure<Profile>(it ->it.age = 18),我们称这个拉姆达为Configure Action,其实这是使用了一个包装类,包装你的Configure Action委托,并把这个类的实例注册到Service容器中。它实现IOptions与拉姆达如何映射的,这一切由OptionsServiceCollectionExtensionsOptionsFacotry等实现的。你也可以直接像下面这样使用

var profile = new Servicecollection ().Addoptions().Configure<Profile>(it ->it.age = 18).BuildServiceProvider().GetRequiredService<IOptions<Profile>>().Value;

配置服务注册源码分析/Configure Action包装类的注册

OptionsServiceCollectionExtensions 作为配置服务的扩展类下面有三种类型的扩展方法分别是Configure、PostConfigure、AddOptions,前两所对应的服务为IConfigureOptions与IPostConfigureOptions 区别仅仅是为了实现配置Configure Action的执行时机,IPostConfigureOptions会后执行,而AddOptions本质上还是注册前两种,注册成面上看起来AddOptions注册的Configure Action具有了参数可以访问其它DI内服务。

注意:就算你使用了三个注册方式注册一次或多次对同一个TOptions进行注册,他们其实是操作的同一个TOptions给你。这体现在OptionsFactory.Create上,也是我们想要的效果。

以下代码为Configure、PostConfigure的服务注册逻辑。

Configure、PostConfigure、扩展方法注册的 Configure Action会由IConfigureOptions与IPostConfigureOptions 接口对应的包装类进行包装。属性均是Action,Configure Action的执行是在Configure方法中。IConfigureOptions与IPostConfigureOptions的大量泛型类是为了实现使用AddOptions注册的Configure Action访问其它DI内服务。会在下面单独记录。

值得注意的是ConfigureAll如果你此方法去注入一个Name 为null 的 Configure Action包装类逻辑体现在ConfigureNamedOptions.Configure/PostConfigureOptions.Configure方法上。,官方说法:“Configure ALL options instances, both named and default”翻译后扩展方法将配置应用于所有选项,包括命名实例和默认实例。

Configure -> ConfigureNamedOptions

PostConfigure -> PostConfigureOptions

public static class OptionsServiceCollectionExtensions
{
...
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;
} public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services!!, string? name, Action<TOptions> configureOptions!!)
where TOptions : class
{
services.AddOptions();
services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(name, configureOptions));
return services;
}
...
}
public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
{
public ConfigureNamedOptions(string? name, Action<TOptions>? action)
{
Name = name;
Action = action;
}
public virtual void Configure(string? name, TOptions options!!)
{
// Null name is used to configure all named options.// Name的过滤以及Configure 逻辑就是在这里体现的
if (Name == null || name == Name)
{
Action?.Invoke(options);
}
}
}
public class PostConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : class
{
public PostConfigureOptions(string? name, Action<TOptions>? action)
{
Name = name;
Action = action;
} public virtual void PostConfigure(string? name, TOptions options!!)
{
if (Name == null || name == Name)
{
Action?.Invoke(options);
}
}
}
以下代码为AddOptions 的服务注册逻辑。

AddOptions:此方法会帮你构建一个OptionsBuilder,并非向Service容器注入,而是利用其builder类的Configure方法向Service容器具体注入。其下面的大量重载 Configure方法会帮你创建基于IConfigureNamedOptions/IPostConfigureOptions不同数量的泛型类. 其目的就是为了解决在“ Configure Action”中使用其它服务。

设计思路很好可以参考

整体思路大概是,先用AddOptions扩展方法创建了一个OptionsBuilder对象,然后调用它重载方法Configure<TService...>去创建具有多个泛型的ConfigureNamedOptions对象。ConfigureNamedOptions的Configure在执行Action委托时会用serviceProvider获取到TService泛型服务。作为参数传入Action委托。这样委托在真正被执行时就会拿到对应的服务。

public static class OptionsServiceCollectionExtensions
{
...
public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services!!, string? name)
where TOptions : class
{
services.AddOptions();
return new OptionsBuilder<TOptions>(services, name);
}
...
}
public class OptionsBuilder<TOptions> where TOptions : class
{
...
public virtual OptionsBuilder<TOptions> Configure<TDep>(Action<TOptions, TDep> configureOptions!!) where TDep : class
{
Services.AddTransient<IConfigureOptions<TOptions>>(sp =>
new ConfigureNamedOptions<TOptions, TDep>(Name, sp.GetRequiredService<TDep>(), configureOptions));
return this;
}
...
} public class ConfigureNamedOptions<TOptions, TDep> : IConfigureNamedOptions<TOptions> {
...
public ConfigureNamedOptions(string? name, TDep dependency, Action<TOptions, TDep>? action)
{
Name = name;
Action = action;
Dependency = dependency;
} public virtual void Configure(string? name, TOptions options!!)
{
// Null name is used to configure all named options.
if (Name == null || name == Name)
{
Action?.Invoke(options, Dependency);
}
}
...
}

总结

注入到服务的扩展方法(OptionsServiceCollectionExtensions) 服务类 服务实现类 使用 生命周期 备注
Configure IConfigureOptions ConfigureNamedOptions 被OptonsFactory使用 Singleton ConfigureAll,在IPostConfigureOptions前执行Configure Action
PostConfigure IPostConfigureOptions PostConfigureOptions 被OptonsFactory使用 Singleton ConfigureAll 在IConfigureOptions后执行Configure Action
AddOptions IConfigureOptions/IPostConfigureOptions ConfigureNamedOptions/PostConfigureOptions 被OptonsFactory使用 Singleton 辅助注入一个可以访问其它服务的Configure Action

配置Otpns服务的使用

首先在固有想法上注入的服务直接会拿来使用。而在这里注入的均为IConfigureOptions/IPostConfigureOptions服务我们管他们叫Configure Action的包装类,而要使用这些服务是通过IOptions/IOptionsSnapshot/IOptionsMonitor去获得。我们称这三个服务为OptionsManger类

注册的基础服务(OptionsServiceCollectionExtensions.AddOptions)

这里整理出来IOptions/IOptionsSnapshot/IOptionsMonitor三种OptionsManger的区别。

服务类|服务实现类|使用|生命周期|备注|

---|:--:--:--:----:

IOptions|UnnamedOptionsManager|直接在DI|Singleton|在应用启动后读取配置数据且配置更新后获取不到新的变更。实现逻辑:直接调用OptionsFactory.Create(Options.DefaultName)拿到所有Configure Action的包装类 执行包装类配置方法|

IOptionsSnapshot|OptionsManager|同上|Scoped:|区别,通过使用 IOptionsSnapshot,针对请求生存期访问和缓存选项时,每个请求都会计算一次选项。 当使用支持读取已更新的配置值的配置提供程序时,将在应用启动后读取对配置所做的更改。实现逻辑:用Factory创建完后得缓存下。非常的简单。|

IOptionsMonitor|OptionsMonitor|同上|Singleton|区别,当配置发生改变是由他提供的配置会实时更新。|

IOptions 使用Demo

var source = new Dictionary<string, string>{
{"TestOptions:Key1" ,"TestOptions key1"},
};
var config = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
ServiceCollection services = new ServiceCollection();
services.AddOptions();
services.Configure<TestOptions>(config.GetSection("TestOptions")); // Import the "Microsoft.Extensions.Options.ConfigurationExtensions" package.
var serviceProvider = services.BuildServiceProvider();
IOptions<TestOptions> options = serviceProvider.GetService<IOptions<TestOptions>>();
Console.WriteLine(options.Value.Key1);
Console.ReadLine();
public class TestOptions
{
public string Key1 { get; set; }
}

IOptions 使用源码分析

以第一个为例:UnnamedOptionsManager 的value属性的get 访问器其内部直接调用OptionsFactory.Craete Create就更简单了。构造方法就把所有的 IConfigureOptions/IPostConfigureOptions拿到了。直接循环两个集合,并调用Configure方法就Ok了 Caonfigure内部执行注入的Action 对TOptions实例一顿操作。

internal sealed class UnnamedOptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> : IOptions<TOptions>where TOptions : class
{
private volatile TOptions? _value;
public UnnamedOptionsManager(IOptionsFactory<TOptions> factory) => _factory = factory;
public TOptions Value
{
get
{
if (_value is TOptions value)
{
return value;
}
return _value ??= _factory.Create(Options.DefaultName);
}
}
}
public class OptionsFactory<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptionsFactory<TOptions>
where TOptions : class
{
... public TOptions Create(string name)
{
// 创建Toptions 实例对象
TOptions options = CreateInstance(name);
// 依次调用注册的 Configure Action
// 先执行IConfigureOptions包装的Configure Action.
foreach (IConfigureOptions<TOptions> setup in _setups)
{
if (setup is IConfigureNamedOptions<TOptions> namedSetup)
{
namedSetup.Configure(name, options);
}
else if (name == Options.DefaultName)
{
setup.Configure(options);
}
}
// 在执行 IPostConfigureOptions包装的Configure Action.
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;
}
...
}

IOptionsSnapshot 使用Demo

因实现逻辑基本与IOptons相同这里就不多做记录。

var source = new Dictionary<string, string>{
{"TestOptions:Key1" ,"TestOptions key1"},
};
var config = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
ServiceCollection services = new ServiceCollection();
services.AddOptions();
services.Configure<TestOptions>("TestOptions", config.GetSection("TestOptions")); // Import the "Microsoft.Extensions.Options.ConfigurationExtensions" package.
var serviceProvider = services.BuildServiceProvider();
IOptionsSnapshot<TestOptions> optionsAccessor = serviceProvider.GetRequiredService<IOptionsSnapshot<TestOptions>>();
Console.WriteLine(optionsAccessor.Get("TestOptions").Key1);
Console.ReadLine();
public class TestOptions
{
public string Key1 { get; set; }
}

配置源的同步 IOptionsMonitor 的使用 Demo

源码在part3 单独分析。

var configuration = new ConfigurationBuilder().AddJsonFile(path: "profile.json",
optional: false,
reloadOnChange: true).Build();
new ServiceCollection().AddOptions().Configure<Profile>(configuration).BuildServiceProvider().GetRequiredService<IOptionsMonitor<Profile>>().OnChange(profile => Console.WriteLine($"changed: {profile.Age}"));
Console.Read(); public class Profile
{
public int Age { get; set; }
}

"Options模型"“配置系统”结合。

2. 将配置绑定为Options对象

如下两个demo分别演示了"Options模型"“配置系统”结合的结合使用。

Demo1

var configuration = new ConfigurationBuilder ().AddJsonFile ("profile.json").Build ();
var profile = new ServiceCollection().AddOptions().Configure<Profile>(configuration).BuildServiceProvider().GetRequiredService<IOptions<Profile>>().Value;

Demo2

var source = new Dictionary<string, string>{
{"TestOptions:Key1" ,"TestOptions key1"},
{"TestOptions:Key2" ,"TestOptions key2"},
{"UserInfo:key1" ,"UserInfo"},
}; var config = new ConfigurationBuilder().Add(new MemoryConfigurationSource() { InitialData = source }).Build();
ServiceCollection services = new ServiceCollection();
services.AddOptions();
services.Configure<TestOpetion>(config.GetSection("TestOptions")); // Import the "Microsoft.Extensions.Options.ConfigurationExtensions" package.
var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<TestOpetion>>();
Console.WriteLine(options.Value.Key1);
Console.ReadLine();
public class TestOpetion{
public string Key1{ get; set; }
public string Key2 { get; set; }
}

以上操作步骤为OptionsConfigurationServiceCollectionExtensions类定义了对 Configure的扩展,有三个参数string name、config(IConfiguration),configureBinder的委托,第一个参数是TOptions的name, 第二个表示配置系统的IConfiguration,第三个configureBinder 是配置系统在映射Toptions时候的一些配置

原理很简单,因为有了ServiceCollection的支持,那么就往里面帮我们注入一个类型为IConfigureOptions 实际为new NamedConfigureFromConfigurationOptions的类。那么NamedConfigureFromConfigurationOptions类构造函数里面直接把BindFromOptions方法作为Configure Action传给父类ConfigureNamedOptions 也就是说此类是帮我们提供了一个调用了configureBinder的Configure Action。(config.Bind(options, configureBinder)这个Bing是扩展方法,ConfigurationBinder是个帮助类)。这样看来NamedConfigureFromConfigurationOptions唯一的作用就是帮帮我们组织了一个Configure Action免得你去自己写了。

关于configureBinder基本逻辑基本是根据TOptions的Type 对象反射出信息,然后第二个参数config(配置系统提供数据的接口)拿数据,在把对应的数据绑定在TOptions 对象上。

public static IServiceCollection Configure<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TOptions>(this IServiceCollection services!!, string? name, IConfiguration config!!, Action<BinderOptions>? configureBinder)
where TOptions : class
{
services.AddOptions();
// 用于支持**“配置系统”**与 **"Options模型"**结合后当配置系统发生更新时回调options时注册的回调函数。后面会说到
services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
// 注册NamedConfigureFromConfigurationOptions
return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
} /// Configures an option instance by using <see cref="ConfigurationBinder.Bind(IConfiguration, object)"/> against an <see cref="IConfiguration"/>.
public class NamedConfigureFromConfigurationOptions<TOptions> : ConfigureNamedOptions<TOptions> where TOptions : class
{
public NamedConfigureFromConfigurationOptions(string? name, IConfiguration config!!, Action<BinderOptions>? configureBinder)
: base(name, options => BindFromOptions(options, config, configureBinder)){
} private static void BindFromOptions(TOptions options, IConfiguration config, Action<BinderOptions>? configureBinder) => config.Bind(options, configureBinder);
} public class BinderOptions
{
// true 会对TOptons的私有属性也赋值
public bool BindNonPublicProperties { get; set; } public bool ErrorOnUnknownConfiguration { get; set; }
}

验证Options的有效性

Options 扩展方法注册Microsoft.Extensions.Options向Service容器注入认证服务,其原理是OptionsFactory.Create拿到所有注入的服务。将TOptons作为参数传入实例的验证方法。

services.AddOptions<DateTimeFormatOptions>().Configure(options =>options. DatePattern = datePattern;options.TimePattern = timePattern;).Validate(options => Validate (options.DatePattern) && Validate(options. TimePattern), "Invalid Date or Time pattern.");

其它

OptionsServiceCollectionExtensions Options 模型依赖的服务

public static IServiceCollection AddOptions(this IServiceCollection 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<>)));
}

Net6 Configuration & Options 源码分析 Part2 Options的更多相关文章

  1. Net6 Configuration & Options 源码分析 Part1

    Net6 Configuration & Options 源码分析 Part1 在Net6中配置系统一共由两个部分组成Options 模型与配置系统.它们是两个完全独立的系统. 第一部分主要记 ...

  2. Net6Configuration & Options 源码分析 Part3 IOptionsMonitor 是如何接收到配置文件变更并同步数据源的

    配置源的同步 IOptionsMonitor 使用 //以下demo演示使用IOptionsMonitor重新加载配置并当重新加载配置是执行回调函数 var configuration = new C ...

  3. Netty源码分析--初始化Options,添加处理器(四)

    接上篇,我们继续进入AbstractBootstrap类的 initAndRegister() 方法 进入init()方法 设置父级Channel的options, 进入到上节提到的NioServer ...

  4. Net6 DI源码分析Part2 Engine,ServiceProvider

    ServiceProvider ServiceProvider是对IServiceProvider实现,它有一个internal的访问修饰符描述的构造,并需要两个参数IServiceCollectio ...

  5. vuex源码分析3.0.1(原创)

    前言 chapter1 store构造函数 1.constructor 2.get state和set state 3.commit 4.dispatch 5.subscribe和subscribeA ...

  6. zepto源码分析·ajax模块

    准备知识 在看ajax实现的时候,如果对ajax技术知识不是很懂的话,可以参看下ajax基础,以便读分析时不会那么迷糊 全局ajax事件 默认$.ajaxSettings设置中的global为true ...

  7. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  8. ASP.NET Core 源码学习之 Options[4]:IOptionsMonitor

    前面我们讲到 IOptions 和 IOptionsSnapshot,他们两个最大的区别便是前者注册的是单例模式,后者注册的是 Scope 模式.而 IOptionsMonitor 则要求配置源必须是 ...

  9. ASP.NET Core 2.1 源码学习之 Options[3]:IOptionsMonitor

    前面我们讲到 IOptions 和 IOptionsSnapshot,他们两个最大的区别便是前者注册的是单例模式,后者注册的是 Scope 模式.而 IOptionsMonitor 则要求配置源必须是 ...

随机推荐

  1. mysql表查询、多表查询(增强查询的使用)子查询、合并查询,外连接,mysql5种约束,自增长

    一.查询加强 1.在mysql中,日期类型可以直接比较,需要注意格式 2.%:表示0到多个字符, _:表示单个字符 exp:显示第二个字符为大写O的所有员工的姓名和工资 select  name fr ...

  2. 取代 Mybatis Generator,这款代码生成神器配置更简单,开发效率更高!

    作为一名 Java 后端开发,日常工作中免不了要生成数据库表对应的持久化对象 PO,操作数据库的接口 DAO,以及 CRUD 的 XML,也就是 mapper. Mybatis Generator 是 ...

  3. B快速导航

    GETTING STARTED If you are new to Selenium, we have a few resources that can help you get up to spee ...

  4. 【琉忆分享】新手如何学习PHP?附上PHP知识导图。

    你好,是我--琉忆.PHP程序员面试系列图书作者. 作为一名PHP开发者过来人,也是经历了菜鸟到老手的过程,在此给那些想学PHP的同学指条路,即使你是转行学PHP一样可以学会PHP. (如果觉得下面这 ...

  5. VS Code开发TypeScript

    TypeScript是JaveScript的超集,为JavaScript增加了很多特性,它可以编译成纯JavaScript在浏览器上运行.TypeScript已经成为各种流行框架和前端应用开发的首选. ...

  6. Linux CPU信息说明

    命令 [root@*** ~]# lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian ...

  7. linux系统中实用shell脚本,请收藏!

    1.Dos攻击防范(自动屏蔽攻击 IP) #!/bin/bashDATE=$(date +%d/%b/%Y:%H:%M)LOG_FILE=/usr/local/nginx/logs/demo2.acc ...

  8. AFNetworking 修改

    相比大家刚刚拿到AFNetworking  post  和 get 请求数据的时候都会有些小问题吧 NSLocalizedDescription=Request failed: unacceptabl ...

  9. C#的in/out关键字与协变逆变

    C#提供了一组关键字in&out,在泛型接口和泛型委托中,若不使用关键字修饰类型参数T,则该类型参数是不可变的(即不允许协变/逆变转换),若使用in修饰类型参数T,保证"只将T用于输 ...

  10. Linux下/目录 、/home目录 、~目录的区别

    / :根目录 ,所有目录的最顶层目录,从任何用户执行该命令都会进入同一个目录,即所有用户共享.如下图: /home:/下面的home目录,名为家目录,但是很多人叫为用户列表目录,因为/home下是这台 ...