Options
package说明
ASP.NET Core 中的选项模式 | Microsoft Learn
Microsoft.Extensions.Options:选项的核心包,扩展IServiceCollection接口,只支持内存配置。
Microsoft.Extensions.Options.ConfigurationExtensions:配置文件的扩展,支持IConfiguration进行配置。
Microsoft.Extensions.DependencyInjection:选项必须配合容器使用
Microsoft.Extensions.Options.DataAnnotations:支持数据注解验证
IOptions
IOptionsSnapshot
IOptionsMonitor
IOptionsFactory
IConfigureOptions:配置选项的委托
IPostConfigureOptions:配置选项的委托,在IConfigureOptions委托执行之后执行
IOptionsChangeTokenSource:用于监听IConfiguration的更改通知
IValidateOptions:配置选项之后的验证
源码解读
注意:选项必须提供无参构造器,具体阅读OptionsFactory的源码底层采用的是反射。选项是.NET中的一个非常重要的概念,通过这节课程大家需要掌握如何灵活更改框架的选项。对于一些需要编写框架的同学也有很大的帮助。但凡以Options结尾的一般都是选项模式。
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
where TOptions : class
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.AddOptions();
services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
return services;
}
//注册Options核心组件
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;
}
注册过程简述:
1.调用Configure方法会将配置选项的委托IConfigureOptions(不是选项本身)添加到容器,同一个选项可以调用多个Configure,注册多个IConfigureOptions实列。
2.调用PostConfigure方法会将配置选项的委托IPostConfigureOptions(不是选项本身)添加到容器,同一个选项可以调用多个PostConfigure,注册多个IPostConfigureOptions实列。和Configure类型都是用于保存配置选项的委托。
3.IConfigureNamedOptions接口继承了IConfigureOptions接口,而ConfigureNamedOptions实现了IConfigureNamedOptions接口。用于保存配置选项的委托和名字。
3.调用Configure方法,会调用AddOptions,AddOptions注册了IOptions、IOptionsSnapshot、IOptionsMonitor、IOptionsFactory、IOptionsMonitorCache到容器里。
4.IOptionsFactory,负责管理,创建,配置、验证选项。源码可以知道IOptionsFactory依赖注入了所有的(同一个选项)的IConfigureOptions和IPostConfigureOptions的委托,先按顺序(注册的顺序)执行所有的Configure委托,然后在按顺序执行所有的PostConfigure委托。因此PostConfigure一定在所有的Configure之后执行。然后在执行所有的验证器。IOptionsFactory是通过反射创建选项的,因此选项不支持依赖注入,必须提供无参数构造器。
5.IOptions接口的实现类是单实列的。通过依赖IOptionsFactory完成选项的管理。不支持命名选项,不支持配置更改计算(单实例)。
6.IOptionsSnapshot是Scope级别的但是有缓存策略,通过依赖IOptionsFactory完成选项的管理。缓存由于是实列的,而实列的生命周期是scope级别,因此缓存也是scope级别。不要通过根容器解析scope级别的实列,因为不会根容器释放。支持命名选项,支持配置的更改计算,每次都会从配置文件读取并绑定到选项。
7.IOptionsMonitor是单实例的,通过依赖IOptionsFactory完成选项的管理。支持命名选项,支持配置更改计算,有缓存机制,可以注册选项更改通知回调。
public class MvcOptions
{
public string Uri { get; }
}
static void TestSort()
{
var services = new ServiceCollection();
//name为Empty
services.Configure<MvcOptions>(a =>
{
Console.WriteLine(11);
a.Url = "132";
});
services.PostConfigure<MvcOptions>(a =>
{
Console.WriteLine(33);
a.Url = "132";
});
services.Configure<MvcOptions>(a =>
{
Console.WriteLine(22);
a.Url = "132";
});
services.PostConfigure<MvcOptions>(a =>
{
Console.WriteLine(44);
a.Url = "132";
});
var sp = services.BuildServiceProvider();
var optionsFactory = sp.GetRequiredService<IOptionsFactory<MvcOptions>>();
var options = optionsFactory.Create(Options.DefaultName);
}
核心方法
注意:Configure和AddOptions都不是直接把选择注入到容器,而是把选项的配置委托注入到容器。因此你可以执行无数多个Configre和AddOptions。
//1.通过委托来配置选项,同一个选项可以按顺序执行多个Configure。
services.Configure<MvcOptions>(a =>
{
a.Url = "132";
});
//2.在所有的Configure执行之后配置选项。同一个选项可以按顺序执行多个PostConfigure。
services.PostConfigure<MvcOptions>(a =>
{
a.Url = "132";
});
//AddOptions返回一个OptionsBuilder,可以连续配置选项,本质还是执行Configure和PostConfigure
services.AddOptions<MvcOptions>()
.Bind(configuration.GetSection("MvcOptions"))
.Configure(a=>a.Url="123");
解析选项
var services = new ServiceCollection();
services.Configure<MvcOptions>(a =>
{
a.Url = "132";
});
var sp = services.BuildServiceProvider();
var optionsFactory = sp.GetRequiredService<IOptionsFactory<MvcOptions>>();
var options = sp.GetRequiredService<IOptions<MvcOptions>>();
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<MvcOptions>>();
var optionsSnapshot = sp.GetRequiredService<IOptionsSnapshot<MvcOptions>>();
注入选项
吧Options直接注入到容器,可以通过IOptions、IOptionsSnapshot、IOptionsMonitor、IOptionsFactory
static void TestDiOptions()
{
var services = new ServiceCollection();
services.Configure<MvcOptions>(a =>
{
Console.WriteLine(22);
a.Url = "132";
});
services.AddSingleton(sp =>
{
return sp.GetRequiredService<IOptions<MvcOptions>>().Value;
});
//必须使用Scoped,因为IOptionsSnapshot是Scoped的,如果是单实列的话,就丢失了自动更改的性质了
//services.AddScoped(sp =>
//{
// return sp.GetRequiredService<IOptionsSnapshot<MvcOptions>>().Value;
//});
var sp = services.BuildServiceProvider();
var options = sp.GetRequiredService<MvcOptions>();
}
监听更改
{
"MvcOptions": {
"Url": "123"
},
"WebOptions": {
"Url": 456
}
}
public class MvcOptions
{
public string? Url { get; set; }
}
static void OnChange()
{
var services = new ServiceCollection();
var configuration = GetConfiguration();
services.Configure<MvcOptions>(configuration.GetSection("MvcOptions"));
var container = services.BuildServiceProvider();
while (true)
{
Thread.Sleep(1000);
using (var scope = container.CreateScope())
{
var o1 = scope.ServiceProvider.GetRequiredService<IOptions<MvcOptions>>();
var o2 = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<MvcOptions>>();
var o3 = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<MvcOptions>>();
Console.WriteLine("==================");
Console.WriteLine($"IOptions:{o1.Value.Url}");
Console.WriteLine($"IOptionsSnapshot:{o2.Value.Url}");
Console.WriteLine($"IOptionsMonitor:{o3.CurrentValue.Url}");
o3.OnChange(o =>
{
Console.WriteLine("选项发生更改了");
});
}
}
}
static IConfiguration GetConfiguration()
{
var configuration = new ConfigurationManager();
configuration.SetBasePath(Directory.GetCurrentDirectory());
configuration.AddJsonFile("config.json", false, true);
return configuration;
}
命名选项
{
"MvcOptions": {
"Url": "123"
},
"WebOptions": {
"Url": 456
}
}
public class MvcOptions
{
public string? Url { get; set; }
}
static void TestNamed()
{
var services = new ServiceCollection();
var configuration = GetConfiguration();
//name=Options.DefaultName
services.Configure<MvcOptions>(configuration.GetSection("MvcOptions"));
//name="o1"
services.Configure<MvcOptions>("o1", configuration.GetSection("MvcOptions"));
//name="o2"
services.Configure<MvcOptions>("o2", configuration.GetSection("WebOptions"));
var container = services.BuildServiceProvider();
var o1 = container.GetRequiredService<IOptionsSnapshot<MvcOptions>>();
var o2 = container.GetRequiredService<IOptionsMonitor<MvcOptions>>();
//name="o1"
Console.WriteLine("IOptionsSnapshot:Named:" + o1.Get("o1").Url);
//name=Options.DefaultName
Console.WriteLine("IOptionsSnapshot:Value:" + o1.Value.Url);
//name="o2"
Console.WriteLine("IOptionsMonitor:Named:" + o2.Get("o2").Url);
//name=Options.DefaultName
Console.WriteLine("IOptionsMonitor:Value:" + o2.CurrentValue.Url);
var optionsFactory = sp.GetRequiredService<IOptionsFactory<MvcOptions>>();
var options = optionsFactory.Create(Options.DefaultName);
}
只有IOptionsFactory、IOptionsSnapshot、IOptionsMonitor支持命名配置
接口配置
//通过该源码发现,我们可以通过实现IPostConfigureOptions接口,并注入到容器来进行配置选项
public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
where TOptions : class
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.AddOptions();
services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(name, configureOptions));
return services;
}
public class MvcOptions
{
public string? Url { get; set; }
}
//你也可以实现IConfigureOptions但是IConfigureOptions的执行顺序优先级比较低(要学会举一反三)
internal class MvcOptionsPostConfigureOptions : IPostConfigureOptions<MvcOptions>
{
public void PostConfigure(string name, MvcOptions options)
{
options.Url = "789";
//可以编写验证逻辑
//if (options.Url == "123")
//{
// throw new InvalidDataException("Url不能等于123");
//}
}
}
public static void TestIPostConfigureOptions()
{
var services = new ServiceCollection();
services.Configure<MvcOptions>(s => s.Url = "111");
services.AddSingleton<IPostConfigureOptions<MvcOptions>, MvcOptionsPostConfigureOptions>();
var container = services.BuildServiceProvider();
var o2 = container.GetRequiredService<IOptions<MvcOptions>>();
}
服务配置
AddOptions会返回可以OptionsBuilder,OptionsBuilder中支持DI的方式来配置选项,最多支持5个。
internal class MvcOptionsDep
{
public void Configure(MvcOptions options)
{
options.Url = "6666";
}
}
public static void TestDep()
{
var services = new ServiceCollection();
//注册di服务
services.AddSingleton<MvcOptionsDep>();
services.AddOptions<MvcOptions>()//注册选项
.Configure(a=>a.Url="123")//第一个配置
.PostConfigure<MvcOptionsDep>((options, dep) => //后续配置
{
dep.Configure(options);//调用MvcOptionsDep中的方法来进行配置
});
var container = services.BuildServiceProvider();
var o2 = container.GetRequiredService<IOptions<MvcOptions>>();
}
验证选项
注意验证是一定是在Configure和PostConfigure之后执行的
1.委托验证
public static void TestValidateDelegate()
{
try
{
var services = new ServiceCollection();
//不是把选项注入到容器,只是注入了该选项的委托
services.Configure<MvcOptions>(a => a.Url = "123");
//得到OptionsBuilder,进行验证,本质还是执行Configure(可以执行无数多个Configre)
services.AddOptions<MvcOptions>()
.Validate(options =>
{
if (options.Url == null)
{
return false;
}
if (!options.Url.StartsWith("aa"))
{
return false;
}
return true;
}, "必须以aa开头");
var container = services.BuildServiceProvider();
var options = container.GetRequiredService<IOptions<MvcOptions>>();
Console.WriteLine(options.Value.Url);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
2.注解验证
需要安装Microsoft.Extensions.Options.DataAnnotations包
public class MvcOptions
{
[RegularExpression(@"^aa",ErrorMessage ="必须以aa开头")]
public string? Url { get; set; }
}
public static void TestValidateDataAnnotations()
{
try
{
var services = new ServiceCollection();
//不是把选项注入到容器,只是注入了该选项的委托
services.Configure<MvcOptions>(a => a.Url = "123");
//得到OptionsBuilder,进行验证,本质还是执行Configure(可以执行无数多个Configre)
services.AddOptions<MvcOptions>()
.ValidateDataAnnotations();
var container = services.BuildServiceProvider();
var options = container.GetRequiredService<IOptions<MvcOptions>>();
Console.WriteLine(options.Value.Url);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}