本章将和大家分享ASP.NET Core中Options模式的使用及其源码解析。
在ASP.NET Core中引入了Options这一使用配置方式,其主要是为了解决依赖注入时需要传递指定数据问题(不是自行获取,而是能集中配置)。通常来讲我们会把所需要的配置通过IConfiguration对象配置成一个普通的类,并且习惯上我们会把这个类的名字后缀加上Options。所以我们在使用某一个中间件或者使用第三方类库时,经常会看到配置对应的Options代码,例如:关于Cookie的中间件就会配置CookiePolicyOptions这个对象。
1、Options模式的用法
向服务容器中注入TOptions配置(绑定配置):
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using IConfiguration_IOptions_Demo.Models; namespace IConfiguration_IOptions_Demo { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) #region Options模式 services.PostConfigureAll(options => options.Title = "PostConfigureAll"); services.Configure (Configuration); services.Configure (Configuration.GetSection("OtherConfig")); services.Configure (options => options.Title = "Default Name");//默认名称string.Empty services.Configure ("FromMemory", options => options.Title = "FromMemory"); services.AddOptions ("AddOption").Configure(options => options.Title = "AddOptions"); services.Configure ("FromConfiguration", Configuration.GetSection("OtherConfig")); services.ConfigureAll (options => options.Title = "ConfigureAll"); services.PostConfigure (options => options.Title = "PostConfigure"); #endregion Options模式 services .AddControllersWithViews() .AddRazorRuntimeCompilation() //修改cshtml后能自动编译 .AddNewtonsoftJson(options => { options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;//忽略循环引用 options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;//序列化忽略null和空字符串 options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore; options.SerializerSettings.ContractResolver = new DefaultContractResolver();//不使用驼峰样式的key }); // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else app.UseExceptionHandler("/Home/Error"); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); } }
从服务容器中获取TOptions对象(读取配置):
using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using IConfiguration_IOptions_Demo.Models; using Newtonsoft.Json; using System.Text; namespace IConfiguration_IOptions_Demo.Controllers { public class OptionsDemoController : Controller { protected readonly IOptions_options; //直接单例,不支持数据变化,性能高 protected readonly IOptionsMonitor _optionsMonitor; //支持数据修改,靠的是监听文件更新(onchange)数据(修改配置文件会更新缓存) protected readonly IOptionsSnapshot _optionsSnapshot; //一次请求数据不变的,但是不同请求可以不同的,每次生成 protected readonly IOptions _optionsOtherConfig; public OptionsDemoController( IOptions options, IOptionsMonitor optionsMonitor, IOptionsSnapshot optionsSnapshot, IOptions optionsOtherConfig) { _options = options; _optionsMonitor = optionsMonitor; _optionsSnapshot = optionsSnapshot; _optionsOtherConfig = optionsOtherConfig; } public IActionResult Index() var sb = new StringBuilder(); AppSettingsOptions options1 = _options.Value; OtherConfig otherConfigOptions1 = _optionsOtherConfig.Value; sb.AppendLine($"_options.Value => {JsonConvert.SerializeObject(options1)}"); sb.AppendLine(""); sb.AppendLine($"_optionsOtherConfig.Value => {JsonConvert.SerializeObject(otherConfigOptions1)}"); AppSettingsOptions options2 = _optionsMonitor.CurrentValue; //_optionsMonitor.Get(Microsoft.Extensions.Options.Options.DefaultName); AppSettingsOptions fromMemoryOptions2 = _optionsMonitor.Get("FromMemory"); //命名选项 sb.AppendLine($"_optionsMonitor.CurrentValue => {JsonConvert.SerializeObject(options2)}"); sb.AppendLine($"_optionsMonitor.Get(\"FromMemory\") => {JsonConvert.SerializeObject(fromMemoryOptions2)}"); AppSettingsOptions options3 = _optionsSnapshot.Value; //_optionsSnapshot.Get(Microsoft.Extensions.Options.Options.DefaultName); AppSettingsOptions fromMemoryOptions3 = _optionsSnapshot.Get("FromMemory"); //命名选项 sb.AppendLine($"_optionsSnapshot.Value => {JsonConvert.SerializeObject(options3)}"); sb.AppendLine($"_optionsSnapshot.Get(\"FromMemory\") => {JsonConvert.SerializeObject(fromMemoryOptions3)}"); return Content(sb.ToString()); } }
访问 “/OptionsDemo/Index” 运行结果如下所示:
2、Options模式源码解析
我们从下面的这条语句开始分析:
services.Configure(options => options.Title = "Default Name");
将光标移动到Configure 然后按 F12 转到定义,如下:
可以发现此Configure 是个扩展方法,位于OptionsServiceCollectionExtensions 类中,我们找到OptionsServiceCollectionExtensions 类的源码如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection { ////// Extension methods for adding options services to the DI container. /// public static class OptionsServiceCollectionExtensions { ////// Adds services required for using options. /// /// Theto add the services to. /// The public static IServiceCollection AddOptions(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(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; } /// Registers an action used to configure a particular type of options. /// Note: These are run before allso that additional calls can be chained. . /// The options type to be configured. /// The action used to configure the options. public static IServiceCollection Configure(this IServiceCollection services, Action configureOptions) where TOptions : class => services.Configure(Options.Options.DefaultName, configureOptions); /// The name of the options instance. public static IServiceCollection Configure (this IServiceCollection services, string name, Action configureOptions) where TOptions : class if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); services.AddOptions(); services.AddSingleton >(new ConfigureNamedOptions (name, configureOptions)); /// Registers an action used to configure all instances of a particular type of options. public static IServiceCollection ConfigureAll (this IServiceCollection services, Action configureOptions) where TOptions : class => services.Configure(name: null, configureOptions: configureOptions); /// Registers an action used to initialize a particular type of options. /// Note: These are run after all . public static IServiceCollection PostConfigure (this IServiceCollection services, Action configureOptions) where TOptions : class => services.PostConfigure(Options.Options.DefaultName, configureOptions); /// The options type to be configure. public static IServiceCollection PostConfigure(this IServiceCollection services, string name, Action configureOptions) services.AddSingleton >(new PostConfigureOptions (name, configureOptions)); /// Registers an action used to post configure all instances of a particular type of options. public static IServiceCollection PostConfigureAll (this IServiceCollection services, Action configureOptions) where TOptions : class => services.PostConfigure(name: null, configureOptions: configureOptions); /// Registers a type that will have all of its I[Post]ConfigureOptions registered. /// The type that will configure options. public static IServiceCollection ConfigureOptions(this IServiceCollection services) where TConfigureOptions : class => services.ConfigureOptions(typeof(TConfigureOptions)); private static bool IsAction(Type type) => (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Action<>)); private static IEnumerable FindIConfigureOptions(Type type) var serviceTypes = type.GetTypeInfo().ImplementedInterfaces .Where(t => t.GetTypeInfo().IsGenericType && (t.GetGenericTypeDefinition() == typeof(IConfigureOptions<>) || t.GetGenericTypeDefinition() == typeof(IPostConfigureOptions<>))); if (!serviceTypes.Any()) throw new InvalidOperationException( IsAction(type) ? Resources.Error_NoIConfigureOptionsAndAction : Resources.Error_NoIConfigureOptions); return serviceTypes; /// The type that will configure options. public static IServiceCollection ConfigureOptions(this IServiceCollection services, Type configureType) var serviceTypes = FindIConfigureOptions(configureType); foreach (var serviceType in serviceTypes) services.AddTransient(serviceType, configureType); /// Registers an object that will have all of its I[Post]ConfigureOptions registered. /// The instance that will configure options. public static IServiceCollection ConfigureOptions(this IServiceCollection services, object configureInstance) var serviceTypes = FindIConfigureOptions(configureInstance.GetType()); services.AddSingleton(serviceType, configureInstance); /// Gets an options builder that forwards Configure calls for the same to the underlying service collection. /// The public static OptionsBuilderso that configure calls can be chained in it. AddOptions (this IServiceCollection services) where TOptions : class => services.AddOptions (Options.Options.DefaultName); /// Gets an options builder that forwards Configure calls for the same named to the underlying service collection. public static OptionsBuilder AddOptions (this IServiceCollection services, string name) return new OptionsBuilder (services, name); } } Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions类源码
仔细阅读上面的源码后可以发现,Configure方法虽然有多个重载,但最终都会调用下面的这个方法:
////// Registers an action used to configure a particular type of options. /// Note: These are run before all ///. /// The options type to be configured. /// Theto add the services to. /// The name of the options instance. /// The action used to configure the options. /// The public static IServiceCollection Configureso that additional calls can be chained. (this IServiceCollection services, string name, Action configureOptions) where TOptions : class { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (configureOptions == null) { throw new ArgumentNullException(nameof(configureOptions)); } services.AddOptions(); services.AddSingleton >(new ConfigureNamedOptions (name, configureOptions)); return services; }
Configure方法的多个重载主要差异在于name参数值的不同。当不传递name参数值时,默认使用Microsoft.Extensions.Options.Options.DefaultName,它等于string.Empty,如下所示:
////// Registers an action used to configure a particular type of options. /// Note: These are run before all ///. /// The options type to be configured. /// Theto add the services to. /// The action used to configure the options. /// The public static IServiceCollection Configureso that additional calls can be chained. (this IServiceCollection services, Action configureOptions) where TOptions : class => services.Configure(Options.Options.DefaultName, configureOptions);
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace Microsoft.Extensions.Options { ////// Helper class. /// public static class Options { ////// The default name used for options instances: "". /// public static readonly string DefaultName = string.Empty; /// Creates a wrapper around an instance ofto return itself as an . /// Options type. /// Options object. ///Wrapped options object. public static IOptionsCreate (TOptions options) where TOptions : class, new() { return new OptionsWrapper (options); } } }
另外,我们可以看到ConfigureAll这个方法,此方法的内部也是调用Configure方法,只不过把name值设置成null,后续在创建TOptions时,会把name值为null的Action
////// Registers an action used to configure all instances of a particular type of options. /// ///The options type to be configured. /// Theto add the services to. /// The action used to configure the options. /// The public static IServiceCollection ConfigureAllso that additional calls can be chained. (this IServiceCollection services, Action configureOptions) where TOptions : class => services.Configure(name: null, configureOptions: configureOptions);
至此我们大概知道了,其实Configure方法主要就是做两件事:
1、调用services.AddOptions()方法。
2、将 new ConfigureNamedOptions
此外,从OptionsServiceCollectionExtensions类的源码中我们可以发现PostConfigure方法同样有多个重载,并且最终都会调用下面的这个方法:
////// Registers an action used to configure a particular type of options. /// Note: These are run after all ///. /// The options type to be configure. /// Theto add the services to. /// The name of the options instance. /// The action used to configure the options. /// The public static IServiceCollection PostConfigureso that additional calls can be chained. (this IServiceCollection services, string name, Action configureOptions) where TOptions : class { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); services.AddOptions(); services.AddSingleton >(new PostConfigureOptions (name, configureOptions)); return services; }
与Configure方法一样,PostConfigure方法的多个重载主要差异在于name参数值的不同。当不传递name参数值时,默认使用Microsoft.Extensions.Options.Options.DefaultName,它等于string.Empty,如下:
////// Registers an action used to initialize a particular type of options. /// Note: These are run after all ///. /// The options type to be configured. /// Theto add the services to. /// The action used to configure the options. /// The public static IServiceCollection PostConfigureso that additional calls can be chained. (this IServiceCollection services, Action configureOptions) where TOptions : class => services.PostConfigure(Options.Options.DefaultName, configureOptions);
同样的,PostConfigureAll方法的内部也是调用PostConfigure方法,只不过把name值设置成null,如下:
////// Registers an action used to post configure all instances of a particular type of options. /// Note: These are run after all ///. /// The options type to be configured. /// Theto add the services to. /// The action used to configure the options. /// The public static IServiceCollection PostConfigureAllso that additional calls can be chained. (this IServiceCollection services, Action configureOptions) where TOptions : class => services.PostConfigure(name: null, configureOptions: configureOptions);
至此我们可以发现,PostConfigure方法同样也是只做两件事:
1、调用services.AddOptions() 方法。
2、将new PostConfigureOptions
其实PostConfigure方法,它和Configure方法使用方式一模一样,也是在创建TOptions时调用。只不过先后顺序不一样,PostConfigure在Configure之后调用,该点在后面的讲解中会再次提到。
另外,还有一种AddOptions的用法,如下所示:
services.AddOptions("AddOption").Configure(options => options.Title = "AddOptions");
////// Gets an options builder that forwards Configure calls for the same ///to the underlying service collection. /// The options type to be configured. /// Theto add the services to. /// The public static OptionsBuilderso that configure calls can be chained in it. AddOptions (this IServiceCollection services) where TOptions : class => services.AddOptions (Options.Options.DefaultName); /// Gets an options builder that forwards Configure calls for the same named to the underlying service collection. /// The name of the options instance. public static OptionsBuilder AddOptions (this IServiceCollection services, string name) where TOptions : class { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddOptions(); return new OptionsBuilder (services, name); }
这种方式会创建一个OptionsBuilder对象,用来辅助配置TOptions对象,我们找到OptionsBuilder类的源码如下:
Microsoft.Extensions.Options.OptionsBuilder类源码
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.Options { ////// Used to configure ///instances. /// The type of options being requested. public class OptionsBuilderwhere TOptions : class { private const string DefaultValidationFailureMessage = "A validation error has occured."; /// /// The default name of the public string Name { get; } /// Theinstance. /// for the options being configured. public IServiceCollection Services { get; } /// Constructor. /// The for the options being configured. /// The default name of the instance, if null is used. public OptionsBuilder(IServiceCollection services, string name) { if (services == null) { throw new ArgumentNullException(nameof(services)); } Services = services; Name = name ?? Options.DefaultName; } /// Registers an action used to configure a particular type of options. /// Note: These are run before all . /// The action used to configure the options. /// The current public virtual OptionsBuilder. Configure(Action configureOptions) if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions)); Services.AddSingleton >(new ConfigureNamedOptions (Name, configureOptions)); return this; /// A dependency used by the action. public virtual OptionsBuilderConfigure (Action configureOptions) where TDep : class Services.AddTransient >(sp => new ConfigureNamedOptions (Name, sp.GetRequiredService (), configureOptions)); /// The first dependency used by the action. ///The second dependency used by the action. public virtual OptionsBuilderConfigure (Action configureOptions) where TDep1 : class where TDep2 : class new ConfigureNamedOptions (Name, sp.GetRequiredService (), sp.GetRequiredService (), configureOptions)); /// The third dependency used by the action. public virtual OptionsBuilderConfigure (Action configureOptions) where TDep3 : class Services.AddTransient >( sp => new ConfigureNamedOptions ( Name, sp.GetRequiredService (), sp.GetRequiredService (), sp.GetRequiredService (), configureOptions)); /// The fourth dependency used by the action. public virtual OptionsBuilderConfigure (Action configureOptions) where TDep4 : class sp => new ConfigureNamedOptions ( sp.GetRequiredService (), /// The fifth dependency used by the action. public virtual OptionsBuilderConfigure (Action configureOptions) where TDep5 : class sp => new ConfigureNamedOptions ( sp.GetRequiredService (), /// Note: These are run after all . public virtual OptionsBuilder PostConfigure(Action configureOptions) Services.AddSingleton >(new PostConfigureOptions (Name, configureOptions)); /// Registers an action used to post configure a particular type of options. /// The dependency used by the action. public virtual OptionsBuilderPostConfigure (Action configureOptions) Services.AddTransient >(sp => new PostConfigureOptions (Name, sp.GetRequiredService (), configureOptions)); public virtual OptionsBuilder PostConfigure (Action configureOptions) new PostConfigureOptions (Name, sp.GetRequiredService (), sp.GetRequiredService (), configureOptions)); public virtual OptionsBuilder PostConfigure (Action configureOptions) Services.AddTransient >( sp => new PostConfigureOptions ( public virtual OptionsBuilder PostConfigure (Action configureOptions) sp => new PostConfigureOptions ( public virtual OptionsBuilder PostConfigure (Action configureOptions) sp => new PostConfigureOptions ( /// Register a validation action for an options type using a default failure message. /// The validation function. public virtual OptionsBuilder Validate(Func validation) => Validate(validation: validation, failureMessage: DefaultValidationFailureMessage); /// Register a validation action for an options type. /// The failure message to use when validation fails. public virtual OptionsBuilder Validate(Func validation, string failureMessage) if (validation == null) throw new ArgumentNullException(nameof(validation)); Services.AddSingleton >(new ValidateOptions (Name, validation, failureMessage)); /// The dependency used by the validation function. public virtual OptionsBuilderValidate (Func validation) public virtual OptionsBuilder Validate (Func validation, string failureMessage) Services.AddTransient >(sp => new ValidateOptions (Name, sp.GetRequiredService (), validation, failureMessage)); /// The first dependency used by the validation function. ///The second dependency used by the validation function. public virtual OptionsBuilderValidate (Func validation) public virtual OptionsBuilder Validate (Func validation, string failureMessage) new ValidateOptions (Name, validation, failureMessage)); /// The third dependency used by the validation function. public virtual OptionsBuilderValidate (Func validation) public virtual OptionsBuilder Validate (Func validation, string failureMessage) new ValidateOptions (Name, /// The fourth dependency used by the validation function. public virtual OptionsBuilderValidate (Func validation) public virtual OptionsBuilder Validate (Func validation, string failureMessage) new ValidateOptions (Name, /// The fifth dependency used by the validation function. public virtual OptionsBuilderValidate (Func validation) public virtual OptionsBuilder Validate (Func validation, string failureMessage) new ValidateOptions (Name, } } Microsoft.Extensions.Options.OptionsBuilder类源码
我们重点来看其中的一个方法,如下所示:
////// Registers an action used to configure a particular type of options. /// Note: These are run before all /// The action used to configure the options. ///. /// The current public virtual OptionsBuilder. Configure(Action configureOptions) { if (configureOptions == null) { throw new ArgumentNullException(nameof(configureOptions)); } Services.AddSingleton >(new ConfigureNamedOptions (Name, configureOptions)); return this; }
可以发现其内部实现是和Configure方法一样的。
最后还有一种比较酷的用法,就是在调用Configure方法时并没有传递Action
services.Configure(Configuration); services.Configure (Configuration.GetSection("OtherConfig")); services.Configure ("FromConfiguration", Configuration.GetSection("OtherConfig"));
其实这是因为框架在内部帮我们转化了一下,最终传递的还是一个Action
我们将光标移动到对应的 Configure 然后按 F12 转到定义,如下所示:
可以发现此Configure 是个扩展方法,位于OptionsConfigurationServiceCollectionExtensions 类中,我们找到OptionsConfigurationServiceCollectionExtensions类的源码如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection { ////// Extension methods for adding configuration related options services to the DI container. /// public static class OptionsConfigurationServiceCollectionExtensions { ////// Registers a configuration instance which TOptions will bind against. /// ///The type of options being configured. /// Theto add the services to. /// The configuration being bound. /// The public static IServiceCollection Configureso that additional calls can be chained. (this IServiceCollection services, IConfiguration config) where TOptions : class => services.Configure (Options.Options.DefaultName, config); /// The name of the options instance. public static IServiceCollection Configure (this IServiceCollection services, string name, IConfiguration config) where TOptions : class => services.Configure (name, config, _ => { }); /// Used to configure the . public static IServiceCollection Configure (this IServiceCollection services, IConfiguration config, Action configureBinder) where TOptions : class => services.Configure (Options.Options.DefaultName, config, configureBinder); public static IServiceCollection Configure (this IServiceCollection services, string name, IConfiguration config, Action configureBinder) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (config == null) throw new ArgumentNullException(nameof(config)); services.AddOptions(); services.AddSingleton >(new ConfigurationChangeTokenSource (name, config)); return services.AddSingleton >(new NamedConfigureFromConfigurationOptions (name, config, configureBinder)); } } }
从此处我们可以看出最终它会将new NamedConfigureFromConfigurationOptions
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using Microsoft.Extensions.Configuration; namespace Microsoft.Extensions.Options { ////// Configures an option instance by using ///against an . /// The type of options to bind. public class NamedConfigureFromConfigurationOptions: ConfigureNamedOptions where TOptions : class { /// /// Constructor that takes the /// The name of the options instance. /// Theinstance to bind against. /// instance. public NamedConfigureFromConfigurationOptions(string name, IConfiguration config) : this(name, config, _ => { }) { } /// Used to configure the . public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action configureBinder) : base(name, options => config.Bind(options, configureBinder)) { if (config == null) { throw new ArgumentNullException(nameof(config)); } } } }
从中我们可以发现其实NamedConfigureFromConfigurationOptions
Microsoft.Extensions.Options.ConfigureNamedOptions类源码
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; namespace Microsoft.Extensions.Options { ////// Implementation of ///. /// Options type being configured. public class ConfigureNamedOptions: IConfigureNamedOptions where TOptions : class { /// /// Constructor. /// /// The name of the options. /// The action to register. public ConfigureNamedOptions(string name, Actionaction) { Name = name; Action = action; } /// The options name. public string Name { get; } /// The configuration action. public Action Action { get; } /// Invokes the registered configure if the matches. /// The name of the options instance being configured. /// The options instance to configure. public virtual void Configure(string name, TOptions options) if (options == null) { throw new ArgumentNullException(nameof(options)); } // Null name is used to configure all named options. if (Name == null || name == Name) Action?.Invoke(options); /// Invoked to configure a instance with the . public void Configure(TOptions options) => Configure(Options.DefaultName, options); } /// Dependency type. public class ConfigureNamedOptions: IConfigureNamedOptions where TOptions : class where TDep : class /// A dependency. public ConfigureNamedOptions(string name, TDep dependency, Action action) Dependency = dependency; public Action Action { get; } /// The dependency. public TDep Dependency { get; } Action?.Invoke(options, Dependency); /// First dependency type. ///Second dependency type. public class ConfigureNamedOptions: IConfigureNamedOptions where TDep1 : class where TDep2 : class /// A second dependency. public ConfigureNamedOptions(string name, TDep1 dependency, TDep2 dependency2, Action action) Dependency1 = dependency; Dependency2 = dependency2; public Action Action { get; } /// The first dependency. public TDep1 Dependency1 { get; } /// The second dependency. public TDep2 Dependency2 { get; } Action?.Invoke(options, Dependency1, Dependency2); /// Third dependency type. public class ConfigureNamedOptions: IConfigureNamedOptions where TDep3 : class /// A third dependency. public ConfigureNamedOptions(string name, TDep1 dependency, TDep2 dependency2, TDep3 dependency3, Action action) Dependency3 = dependency3; public Action Action { get; } /// The third dependency. public TDep3 Dependency3 { get; } Action?.Invoke(options, Dependency1, Dependency2, Dependency3); /// Fourth dependency type. public class ConfigureNamedOptions: IConfigureNamedOptions where TDep4 : class /// A dependency. /// A fourth dependency. public ConfigureNamedOptions(string name, TDep1 dependency1, TDep2 dependency2, TDep3 dependency3, TDep4 dependency4, Action action) Dependency1 = dependency1; Dependency4 = dependency4; public Action Action { get; } /// The fourth dependency. public TDep4 Dependency4 { get; } Action?.Invoke(options, Dependency1, Dependency2, Dependency3, Dependency4); /// Fifth dependency type. public class ConfigureNamedOptions: IConfigureNamedOptions where TDep5 : class /// A fifth dependency. public ConfigureNamedOptions(string name, TDep1 dependency1, TDep2 dependency2, TDep3 dependency3, TDep4 dependency4, TDep5 dependency5, Action action) Dependency5 = dependency5; public Action Action { get; } /// The fifth dependency. public TDep5 Dependency5 { get; } Action?.Invoke(options, Dependency1, Dependency2, Dependency3, Dependency4, Dependency5); } Microsoft.Extensions.Options.ConfigureNamedOptions类源码
其中我们重点来看下面的这部分:
////// Implementation of ///. /// Options type being configured. public class ConfigureNamedOptions: IConfigureNamedOptions where TOptions : class { /// /// Constructor. /// /// The name of the options. /// The action to register. public ConfigureNamedOptions(string name, Actionaction) { Name = name; Action = action; } /// The options name. public string Name { get; } /// The configuration action. public Action Action { get; } /// Invokes the registered configure if the matches. /// The name of the options instance being configured. /// The options instance to configure. public virtual void Configure(string name, TOptions options) if (options == null) { throw new ArgumentNullException(nameof(options)); } // Null name is used to configure all named options. if (Name == null || name == Name) Action?.Invoke(options); /// Invoked to configure a instance with the . public void Configure(TOptions options) => Configure(Options.DefaultName, options); }
结合NamedConfigureFromConfigurationOptions
1、 调用 services.Configure
2、 调用 Configure(string name, TOptions options) 方法时会把Name值为null的Action
我们找到config.Bind(options, configureBinder) 这个方法,它是个扩展方法,位于ConfigurationBinder静态类中,如下:
Microsoft.Extensions.Configuration.ConfigurationBinder类源码
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using Microsoft.Extensions.Configuration.Binder; namespace Microsoft.Extensions.Configuration { ////// Static helper class that allows binding strongly typed objects to configuration values. /// public static class ConfigurationBinder { ////// Attempts to bind the configuration instance to a new instance of type T. /// If this configuration section has a value, that will be used. /// Otherwise binding by matching property names against configuration keys recursively. /// ///The type of the new instance to bind. /// The configuration instance to bind. ///The new instance of T if successful, default(T) otherwise. public static T Get(this IConfiguration configuration) => configuration.Get (_ => { }); /// Configures the binder options. public static T Get (this IConfiguration configuration, Action configureOptions) { if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } var result = configuration.Get(typeof(T), configureOptions); if (result == null) return default(T); return (T)result; } /// The type of the new instance to bind. /// The new instance if successful, null otherwise. public static object Get(this IConfiguration configuration, Type type) => configuration.Get(type, _ => { }); public static object Get(this IConfiguration configuration, Type type, ActionconfigureOptions) var options = new BinderOptions(); configureOptions?.Invoke(options); return BindInstance(type, instance: null, config: configuration, options: options); /// Attempts to bind the given object instance to the configuration section specified by the key by matching property names against configuration keys recursively. /// The key of the configuration section to bind. /// The object to bind. public static void Bind(this IConfiguration configuration, string key, object instance) => configuration.GetSection(key).Bind(instance); /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively. public static void Bind(this IConfiguration configuration, object instance) => configuration.Bind(instance, o => { }); public static void Bind(this IConfiguration configuration, object instance, Action configureOptions) if (instance != null) var options = new BinderOptions(); configureOptions?.Invoke(options); BindInstance(instance.GetType(), instance, configuration, options); /// Extracts the value with the specified key and converts it to type T. /// The type to convert the value to. /// The configuration. /// The key of the configuration section's value to convert. ///The converted value. public static T GetValue(this IConfiguration configuration, string key) return GetValue(configuration, key, default(T)); /// The default value to use if no value is found. public static T GetValue (this IConfiguration configuration, string key, T defaultValue) return (T)GetValue(configuration, typeof(T), key, defaultValue); /// Extracts the value with the specified key and converts it to the specified type. /// The type to convert the value to. public static object GetValue(this IConfiguration configuration, Type type, string key) return GetValue(configuration, type, key, defaultValue: null); public static object GetValue(this IConfiguration configuration, Type type, string key, object defaultValue) var section = configuration.GetSection(key); var value = section.Value; if (value != null) return ConvertValue(type, value, section.Path); return defaultValue; private static void BindNonScalar(this IConfiguration configuration, object instance, BinderOptions options) foreach (var property in GetAllProperties(instance.GetType().GetTypeInfo())) { BindProperty(property, instance, configuration, options); } private static void BindProperty(PropertyInfo property, object instance, IConfiguration config, BinderOptions options) // We don't support set only, non public, or indexer properties if (property.GetMethod == null || (!options.BindNonPublicProperties && !property.GetMethod.IsPublic) || property.GetMethod.GetParameters().Length > 0) return; var propertyValue = property.GetValue(instance); var hasSetter = property.SetMethod != null && (property.SetMethod.IsPublic || options.BindNonPublicProperties); if (propertyValue == null && !hasSetter) // Property doesn't have a value and we cannot set it so there is no // point in going further down the graph propertyValue = BindInstance(property.PropertyType, propertyValue, config.GetSection(property.Name), options); if (propertyValue != null && hasSetter) property.SetValue(instance, propertyValue); private static object BindToCollection(TypeInfo typeInfo, IConfiguration config, BinderOptions options) var type = typeof(List<>).MakeGenericType(typeInfo.GenericTypeArguments[0]); var instance = Activator.CreateInstance(type); BindCollection(instance, type, config, options); return instance; // Try to create an array/dictionary instance to back various collection interfaces private static object AttemptBindToCollectionInterfaces(Type type, IConfiguration config, BinderOptions options) var typeInfo = type.GetTypeInfo(); if (!typeInfo.IsInterface) return null; var collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyList<>), type); if (collectionInterface != null) // IEnumerable is guaranteed to have exactly one parameter return BindToCollection(typeInfo, config, options); collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type); var dictionaryType = typeof(Dictionary<,>).MakeGenericType(typeInfo.GenericTypeArguments[0], typeInfo.GenericTypeArguments[1]); var instance = Activator.CreateInstance(dictionaryType); BindDictionary(instance, dictionaryType, config, options); return instance; collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); var instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeInfo.GenericTypeArguments[0], typeInfo.GenericTypeArguments[1])); BindDictionary(instance, collectionInterface, config, options); collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type); // IReadOnlyCollection is guaranteed to have exactly one parameter collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); // ICollection is guaranteed to have exactly one parameter collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type); return null; private static object BindInstance(Type type, object instance, IConfiguration config, BinderOptions options) // if binding IConfigurationSection, break early if (type == typeof(IConfigurationSection)) return config; var section = config as IConfigurationSection; var configValue = section?.Value; object convertedValue; Exception error; if (configValue != null && TryConvertValue(type, configValue, section.Path, out convertedValue, out error)) if (error != null) throw error; // Leaf nodes are always reinitialized return convertedValue; if (config != null && config.GetChildren().Any()) // If we don't have an instance, try to create one if (instance == null) // We are already done if binding to a new collection instance worked instance = AttemptBindToCollectionInterfaces(type, config, options); if (instance != null) { return instance; } instance = CreateInstance(type); // See if its a Dictionary var collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); if (collectionInterface != null) BindDictionary(instance, collectionInterface, config, options); else if (type.IsArray) instance = BindArray((Array)instance, config, options); else // See if its an ICollection collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); if (collectionInterface != null) BindCollection(instance, collectionInterface, config, options); // Something else else BindNonScalar(config, instance, options); private static object CreateInstance(Type type) if (typeInfo.IsInterface || typeInfo.IsAbstract) throw new InvalidOperationException(Resources.FormatError_CannotActivateAbstractOrInterface(type)); if (type.IsArray) if (typeInfo.GetArrayRank() > 1) throw new InvalidOperationException(Resources.FormatError_UnsupportedMultidimensionalArray(type)); return Array.CreateInstance(typeInfo.GetElementType(), 0); var hasDefaultConstructor = typeInfo.DeclaredConstructors.Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); if (!hasDefaultConstructor) throw new InvalidOperationException(Resources.FormatError_MissingParameterlessConstructor(type)); try return Activator.CreateInstance(type); catch (Exception ex) throw new InvalidOperationException(Resources.FormatError_FailedToActivate(type), ex); private static void BindDictionary(object dictionary, Type dictionaryType, IConfiguration config, BinderOptions options) var typeInfo = dictionaryType.GetTypeInfo(); // IDictionary is guaranteed to have exactly two parameters var keyType = typeInfo.GenericTypeArguments[0]; var valueType = typeInfo.GenericTypeArguments[1]; var keyTypeIsEnum = keyType.GetTypeInfo().IsEnum; if (keyType != typeof(string) && !keyTypeIsEnum) // We only support string and enum keys var setter = typeInfo.GetDeclaredProperty("Item"); foreach (var child in config.GetChildren()) var item = BindInstance( type: valueType, instance: null, config: child, options: options); if (item != null) if (keyType == typeof(string)) var key = child.Key; setter.SetValue(dictionary, item, new object[] { key }); else if (keyTypeIsEnum) var key = Enum.Parse(keyType, child.Key); private static void BindCollection(object collection, Type collectionType, IConfiguration config, BinderOptions options) var typeInfo = collectionType.GetTypeInfo(); // ICollection is guaranteed to have exactly one parameter var itemType = typeInfo.GenericTypeArguments[0]; var addMethod = typeInfo.GetDeclaredMethod("Add"); foreach (var section in config.GetChildren()) try var item = BindInstance( type: itemType, instance: null, config: section, options: options); if (item != null) addMethod.Invoke(collection, new[] { item }); catch private static Array BindArray(Array source, IConfiguration config, BinderOptions options) var children = config.GetChildren().ToArray(); var arrayLength = source.Length; var elementType = source.GetType().GetElementType(); var newArray = Array.CreateInstance(elementType, arrayLength + children.Length); // binding to array has to preserve already initialized arrays with values if (arrayLength > 0) Array.Copy(source, newArray, arrayLength); for (int i = 0; i < children.Length; i++) type: elementType, config: children[i], newArray.SetValue(item, arrayLength + i); return newArray; private static bool TryConvertValue(Type type, string value, string path, out object result, out Exception error) error = null; result = null; if (type == typeof(object)) result = value; return true; if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) if (string.IsNullOrEmpty(value)) return true; return TryConvertValue(Nullable.GetUnderlyingType(type), value, path, out result, out error); var converter = TypeDescriptor.GetConverter(type); if (converter.CanConvertFrom(typeof(string))) result = converter.ConvertFromInvariantString(value); catch (Exception ex) error = new InvalidOperationException(Resources.FormatError_FailedBinding(path, type), ex); return false; private static object ConvertValue(Type type, string value, string path) object result; TryConvertValue(type, value, path, out result, out error); if (error != null) throw error; return result; private static Type FindOpenGenericInterface(Type expected, Type actual) var actualTypeInfo = actual.GetTypeInfo(); if(actualTypeInfo.IsGenericType && actual.GetGenericTypeDefinition() == expected) return actual; var interfaces = actualTypeInfo.ImplementedInterfaces; foreach (var interfaceType in interfaces) if (interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == expected) return interfaceType; private static IEnumerable GetAllProperties(TypeInfo type) var allProperties = new List (); do allProperties.AddRange(type.DeclaredProperties); type = type.BaseType.GetTypeInfo(); while (type != typeof(object).GetTypeInfo()); return allProperties; } } Microsoft.Extensions.Configuration.ConfigurationBinder类源码
仔细阅读上面的源码你会发现,其实它是通过反射的方式为TOptions实例赋值的,而它的值则是通过IConfiguration方式获取的,如果通过IConfiguration方式没有获取到指定的值则不做任何处理还是保留原来的默认值。对于使用IConfiguration获取配置值的实现原理,这在上一篇我们已经详细的讲解过了,此处就不再做过多的介绍了。
从上文的分析中我们可以知道,不管是采用哪种绑定配置的方式其内部都会调用 services.AddOptions() 这个方法,该方法位于OptionsServiceCollectionExtensions静态类中,我们找到该方法,如下所示:
////// Adds services required for using options. /// /// Theto add the services to. /// The public static IServiceCollection AddOptions(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(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; }so that additional calls can be chained.
从中我们大概能猜出 IOptions
1、IOptions在注册到容器时是以单例的形式,这种方式数据全局唯一,不支持数据变化。
2、IOptionsSnapshot在注册到容器时是以Scoped的形式,这种方式单次请求数据是不变的,但是不同的请求数据有可能是不一样的,它能觉察到配置源的改变。
3、IOptionsMonitor在注册到容器时虽然也是以单例的形式,但是它多了一个IOptionsMonitorCache缓存,它也是能觉察到配置源的改变,一旦发生改变就会告知OptionsMonitor从缓存中移除相应的对象。
下面我们继续往下分析,首先找到OptionsManager 类的源码,如下所示:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace Microsoft.Extensions.Options { ////// Implementation of ///and . /// Options type. public class OptionsManager: IOptions , IOptionsSnapshot where TOptions : class, new() { private readonly IOptionsFactory _factory; private readonly OptionsCache _cache = new OptionsCache (); // Note: this is a private cache /// /// Initializes a new instance with the specified options configurations. /// /// The factory to use to create options. public OptionsManager(IOptionsFactoryfactory) { _factory = factory; } /// The default configured instance, equivalent to Get(Options.DefaultName). public TOptions Value get { return Get(Options.DefaultName); } /// Returns a configured instance with the given . public virtual TOptions Get(string name) name = name ?? Options.DefaultName; // Store the options in our instance cache return _cache.GetOrAdd(name, () => _factory.Create(name)); } }
可以发现TOptions对象是通过IOptionsFactory工厂产生的,我们继续找到OptionsFactory 类的源码,如下:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; namespace Microsoft.Extensions.Options { ////// Implementation of ///. /// The type of options being requested. public class OptionsFactory: IOptionsFactory where TOptions : class, new() { private readonly IEnumerable > _setups; private readonly IEnumerable > _postConfigures; private readonly IEnumerable > _validations; /// /// Initializes a new instance with the specified options configurations. /// /// The configuration actions to run. /// The initialization actions to run. public OptionsFactory(IEnumerable> setups, IEnumerable > postConfigures) : this(setups, postConfigures, validations: null) { } /// The validations to run. public OptionsFactory(IEnumerable > setups, IEnumerable > postConfigures, IEnumerable > validations) { _setups = setups; _postConfigures = postConfigures; _validations = validations; } /// Returns a configured instance with the given . public TOptions Create(string name) var options = new TOptions(); foreach (var setup in _setups) { if (setup is IConfigureNamedOptions namedSetup) { namedSetup.Configure(name, options); } else if (name == Options.DefaultName) setup.Configure(options); } foreach (var post in _postConfigures) post.PostConfigure(name, options); if (_validations != null) var failures = new List (); foreach (var validate in _validations) var 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; } }
从此处我们可以看出,在调用 Create(string name) 方法时,首先它会去先创建一个 TOptions 对象,接着再遍历_setups 集合去依次调用所有的 Configure 方法,最后再遍历 _postConfigures 集合去依次调用所有的 PostConfigure 方法。而从上文的分析中我们知道此时_setups 集合中存放的是 ConfigureNamedOptions
////// Implementation of ///. /// Options type being configured. public class ConfigureNamedOptions: IConfigureNamedOptions where TOptions : class { /// /// Constructor. /// /// The name of the options. /// The action to register. public ConfigureNamedOptions(string name, Actionaction) { Name = name; Action = action; } /// The options name. public string Name { get; } /// The configuration action. public Action Action { get; } /// Invokes the registered configure if the matches. /// The name of the options instance being configured. /// The options instance to configure. public virtual void Configure(string name, TOptions options) if (options == null) { throw new ArgumentNullException(nameof(options)); } // Null name is used to configure all named options. if (Name == null || name == Name) Action?.Invoke(options); /// Invoked to configure a instance with the . public void Configure(TOptions options) => Configure(Options.DefaultName, options); }
////// Implementation of ///. /// Options type being configured. public class PostConfigureOptions: IPostConfigureOptions where TOptions : class { /// /// Creates a new instance of /// The name of the options. /// The action to register. public PostConfigureOptions(string name, Action. /// action) { Name = name; Action = action; } /// The options name. public string Name { get; } /// The initialization action. public Action Action { get; } /// Invokes the registered initialization if the matches. /// The name of the action to invoke. /// The options to use in initialization. public virtual void PostConfigure(string name, TOptions options) if (options == null) { throw new ArgumentNullException(nameof(options)); } // Null name is used to initialize all named options. if (Name == null || name == Name) Action?.Invoke(options); }
至此,我们整个流程都串起来了,最后我们来看下OptionsMonitor 类的源码,如下所示:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using Microsoft.Extensions.Primitives; namespace Microsoft.Extensions.Options { ////// Implementation of ///. /// Options type. public class OptionsMonitor: IOptionsMonitor , IDisposable where TOptions : class, new() { private readonly IOptionsMonitorCache _cache; private readonly IOptionsFactory _factory; private readonly IEnumerable > _sources; private readonly List _registrations = new List (); internal event Action _onChange; /// /// Constructor. /// /// The factory to use to create options. /// The sources used to listen for changes to the options instance. /// The cache used to store options. public OptionsMonitor(IOptionsFactoryfactory, IEnumerable > sources, IOptionsMonitorCache cache) { _factory = factory; _sources = sources; _cache = cache; foreach (var source in _sources) { var 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); var options = Get(name); if (_onChange != null) _onChange.Invoke(options, name); /// The present value of the options. public TOptions CurrentValue get => Get(Options.DefaultName); /// Returns a configured instance with the given . public virtual TOptions Get(string name) return _cache.GetOrAdd(name, () => _factory.Create(name)); /// Registers a listener to be called whenever changes. /// The action to be invoked when has changed. /// An public IDisposable OnChange(Actionwhich should be disposed to stop listening for changes. listener) var disposable = new ChangeTrackerDisposable(this, listener); _onChange += disposable.OnChange; return disposable; /// Removes all change registration subscriptions. public void Dispose() // Remove all subscriptions to the change tokens foreach (var registration in _registrations) registration.Dispose(); _registrations.Clear(); internal class ChangeTrackerDisposable : IDisposable private readonly Action _listener; private readonly OptionsMonitor _monitor; public ChangeTrackerDisposable(OptionsMonitor monitor, Action listener) _listener = listener; _monitor = monitor; public void OnChange(TOptions options, string name) => _listener.Invoke(options, name); public void Dispose() => _monitor._onChange -= OnChange; } } Microsoft.Extensions.Options.OptionsMonitor类源码
Microsoft.Extensions.Options.OptionsMonitor类源码
3、最佳实践
既然有如此多的获取方式,那我们应该如何选择呢?
1、如果TOption不需要监控且整个程序就只有一个同类型的TOption,那么强烈建议使用IOptions
2、如果TOption需要监控或者整个程序有多个同类型的TOption,那么只能选择IOptionsMonitor
3、当IOptionsMonitor
4、如果需要对配置源的更新做出反应时(不仅仅是配置对象TOptions本身的更新),那么只能使用IOptionsMonitor
本文部分内容参考博文:https://www.cnblogs.com/zhurongbo/p/10856073.html
选项模式微软官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/configuration/options?view=aspnetcore-6.0
aspnetcore源码:
链接: https://pan.baidu.com/s/1BLLjRsvTEBmeVRX68eX7qg?pwd=mju5
提取码: mju5
Demo源码:
链接: https://pan.baidu.com/s/1P9whG1as62gkZEvgz7eSwg?pwd=d6zm
提取码: d6zm
版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!
到此这篇关于ASP.NET Core中Options模式的使用及其源码解析的文章就介绍到这了,更多相关ASP.NET Core Options模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!