詳解ASP.NET Core3.0 配置的Options模式
上一章講到了配置的用法及內(nèi)部處理機(jī)制,對(duì)于配置,ASP.NET Core還提供了一種Options模式。
一、Options的使用
上一章有個(gè)配置的綁定的例子,可以將配置綁定到一個(gè)Theme實(shí)例中。也就是在使用對(duì)應(yīng)配置的時(shí)候,需要進(jìn)行一次綁定操作。而Options模式提供了更直接的方式,并且可以通過依賴注入的方式提供配置的讀取。下文中稱每一條Options配置為Option。
1.簡(jiǎn)單的不為Option命名的方式
依然采用這個(gè)例子,在appsettings.json中存在這樣的配置:
{ "Theme": { "Name": "Blue", "Color": "#0921DC" } }
修改一下ValueController,代碼如下:
public class ValuesController : Controller { private IOptions<Theme> _options = null; public ValuesController(IOptions<Theme> options) { _options = options; } public ContentResult GetOptions() { return new ContentResult() { Content = $"options:{ _options.Value.Name}" }; } }
依然需要在Startup文件中做注冊(cè):
public void ConfigureServices(IServiceCollection services) { services.Configure<Theme>(Configuration.GetSection("Theme")); services.AddControllersWithViews(); //3.0中啟用的新方法 }
請(qǐng)求這個(gè)Action,獲取到的結(jié)果為:
options:Blue
這樣就可以在需要使用該配置的時(shí)候通過依賴注入的方式使用了。但有個(gè)疑問,這里將“Theme”類型綁定了這樣的配置,但如果有多個(gè)這樣的配置呢?就如同下面這樣的配置的時(shí)候:
"Themes": [ { "Name": "Blue", "Color": "#0921DC" }, { "Name": "Red", "Color": "#FF4500" } ]
在這樣的情況下,存在多個(gè)Theme的配置,這樣對(duì)于之前這種依賴注入的方式就不行了。這時(shí)系統(tǒng)提供了將注入的Options進(jìn)行命名的方法。
2.為Option命名的方式
首先需要在Startup文件中注冊(cè)的時(shí)候?qū)ζ涿砑尤缦聝蓷l注冊(cè)代碼:
services.Configure<Theme>("ThemeBlue", Configuration.GetSection("Themes:0")); services.Configure<Theme>("ThemeRed" , Configuration.GetSection("Themes:1"));
修改ValueController代碼,添加IOptionsMonitor<Theme>和IOptionsSnapshot<Theme>兩種新的注入方式如下:
private IOptions<Theme> _options = null; private IOptionsMonitor<Theme> _optionsMonitor = null; private IOptionsSnapshot<Theme> _optionsSnapshot = null; public ValuesController(IOptions<Theme> options, IOptionsMonitor<Theme> optionsMonitor, IOptionsSnapshot<Theme> optionsSnapshot) { _options = options; _optionsMonitor = optionsMonitor; _optionsSnapshot = optionsSnapshot; } public ContentResult GetOptions() { return new ContentResult() { Content = $"options:{_options.Value.Name}," + $"optionsSnapshot:{ _optionsSnapshot.Get("ThemeBlue").Name }," + $"optionsMonitor:{_optionsMonitor.Get("ThemeRed").Name}" }; }
請(qǐng)求這個(gè)Action,獲取到的結(jié)果為:
options:Blue,optionsSnapshot:Red,optionsMonitor:Gray
新增的兩種注入方式通過Options的名稱獲取到了對(duì)應(yīng)的Options。為什么是兩種呢?它們有什么區(qū)別?不知道有沒有讀者想到上一章配置的重新加載功能。在配置注冊(cè)的時(shí)候,有個(gè)reloadOnChange選項(xiàng),如果它被設(shè)置為true的,當(dāng)對(duì)應(yīng)的數(shù)據(jù)源發(fā)生改變的時(shí)候,會(huì)進(jìn)行重新加載。而Options怎么能少了這樣的特性呢。
3.Option的自動(dòng)更新與生命周期
為了驗(yàn)證這三種Options的讀取方式的特性,修改Theme類,添加一個(gè)Guid字段,并在構(gòu)造方法中對(duì)其賦值,代碼如下:
public class Theme { public Theme() { Guid = Guid.NewGuid(); } public Guid Guid { get; set; } public string Name { get; set; } public string Color { get; set; } }
修改上例中的名為GetOptions的Action的代碼如下:
public ContentResult GetOptions() { return new ContentResult() { Content = $"options:{_options.Value.Name}|{_options.Value.Guid}," + $"optionsSnapshot:{ _optionsSnapshot.Get("ThemeBlue").Name }|{_optionsSnapshot.Get("ThemeBlue").Guid}," + $"optionsMonitor:{_optionsMonitor.Get("ThemeRed").Name}|{_optionsMonitor.Get("ThemeRed").Guid}" }; }
請(qǐng)求這個(gè)Action,返回結(jié)果如下:
options:Blue|ad328f15-254f-4505-a79f-4f27db4a393e,optionsSnapshot:Red|dba5f550-29ca-4779-9a02-781dd17f595a,optionsMonitor:Gray|a799fa41-9444-45dd-b51b-fcd15049f98f
刷新頁面,返回結(jié)果為:
options:Blue|ad328f15-254f-4505-a79f-4f27db4a393e,optionsSnapshot:Red|a2350cb3-c156-4f71-bb2d-25890fe08bec,optionsMonitor:Gray|a799fa41-9444-45dd-b51b-fcd15049f98f
可見IOptions和IOptionsMonitor兩種方式獲取到的Name值和Guid值均未改變,而通過IOptionsSnapshot方式獲取到的Name值未改變,但Guid值發(fā)生了改變,每次刷新頁面均會(huì)改變。這類似前面講依賴注入時(shí)做測(cè)試的例子,現(xiàn)在猜測(cè)Guid未改變的IOptions和IOptionsMonitor兩種方式是采用了Singleton模式,而Guid發(fā)生改變的IOptionsSnapshot方式是采用了Scoped或Transient模式。如果在這個(gè)Action中多次采用IOptionsSnapshot讀取_optionsSnapshot.Get("ThemeBlue").Guid的值,會(huì)發(fā)現(xiàn)同一次請(qǐng)求的值是相同的,不同請(qǐng)求之間的值是不同的,也就是IOptionsSnapshot方式使采用了Scoped模式(此驗(yàn)證示例比較簡(jiǎn)單,請(qǐng)讀者自行修改代碼驗(yàn)證)。
在這樣的情況下,修改三種獲取方式對(duì)應(yīng)的配置項(xiàng)的Name值,例如分別修改為“Blue1”、“Red1”和“Gray1”,再次多次刷新頁面查看返回值,會(huì)發(fā)現(xiàn)如下情況:
IOptions方式:Name和Guid的值始終未變。Name值仍為Blue。
IOptionsSnapshot方式:Name值變?yōu)镽ed1,Guid值單次請(qǐng)求內(nèi)相同,每次刷新之間不同。
IOptionsMonitor方式:只有修改配置值后第一次刷新的時(shí)候?qū)ame值變?yōu)榱薌ray1,Guid未改變。之后多次刷新,這兩個(gè)值均未做改變。
總結(jié):IOptions和IOptionsMonitor兩種方式采用了Singleton模式,但區(qū)別在于IOptionsMonitor會(huì)監(jiān)控對(duì)應(yīng)數(shù)據(jù)源的變化,如果發(fā)生了變化則更新實(shí)例的配置值,但不會(huì)重新提供新的實(shí)例。IOptionsSnapshot方式采用了Scoped模式每次請(qǐng)求采用同一個(gè)實(shí)例,在下一次請(qǐng)求的時(shí)候獲取到的是一個(gè)新的實(shí)例,所以如果數(shù)據(jù)源發(fā)生了改變,會(huì)讀取到新的值。先大概記一下這一的情況,在下文剖析IOptions的內(nèi)部處理機(jī)制的時(shí)候就會(huì)明白為什么會(huì)這樣。
4.數(shù)據(jù)更新提醒
IOptionsMonitor方式還提供了一個(gè)OnChange方法,當(dāng)數(shù)據(jù)源發(fā)生改變的時(shí)候會(huì)觸發(fā)它,所以如果想在這時(shí)候做點(diǎn)什么,可以利用這個(gè)方法實(shí)現(xiàn)。示例代碼:
_optionsMonitor.OnChange((theme,name)=> { Console.WriteLine(theme.Name +"-"+ name); });
5.不采用Configuration配置作為數(shù)據(jù)源的方式
上面的例子都是采用了讀取配置的方式,實(shí)際上Options模式和上一章的Configuration配置方式使分開的,讀取配置只不過是Options模式的一種實(shí)現(xiàn)方式,例如可以不使用Configuration中的數(shù)據(jù),直接通過如下代碼注冊(cè):
services.Configure<Theme>("ThemeBlack", theme => { theme.Color = "#000000"; theme.Name = "Black"; });
6.ConfigureAll方法
系統(tǒng)提供了一個(gè)ConfigureAll方法,可以將所有對(duì)應(yīng)的實(shí)例統(tǒng)一設(shè)置。例如如下代碼:
services.ConfigureAll<Theme>(theme => { theme.Color = "#000000"; theme.Name = "Black2"; });
此時(shí)無論通過什么名稱去獲取Theme的實(shí)例,包括不存在對(duì)應(yīng)設(shè)置的名稱,獲取到的值都是本次通過ConfigureAll設(shè)置的值。
7.PostConfigure和PostConfigureAll方法
這兩個(gè)方法和Configure、ConfigureAll方法類似,只是它們會(huì)在Configure、ConfigureAll之后執(zhí)行。
8.多個(gè)Configure、ConfigureAll、PostConfigure和PostConfigureAll的執(zhí)行順序
可以這樣理解,每個(gè)Configure都是去修改一個(gè)名為其設(shè)置的名稱的變量,以如下代碼為例:
services.Configure<Theme>("ThemeBlack", theme => { theme.Color = "#000000"; theme.Name = "Black"; });
這條設(shè)置就是去修改(注意是修改而不是替換)一個(gè)名為"ThemeBlack"的Theme類型的變量,如果該變量不存在,則創(chuàng)建一個(gè)Theme實(shí)例并賦值。這樣就生成了一些變量名為“空字符串、“ThemeBlue”、“ThemeBlack”的變量(只是舉例,忽略空字符串作為變量名不合法的顧慮)”。
依次按照代碼的順序執(zhí)行,這時(shí)候如果后面的代碼中出現(xiàn)同名的Configure,則修改對(duì)應(yīng)名稱的變量的值。如果是ConfigureAll方法,則修改所有類型為Theme的變量的值。
而PostConfigure和PostConfigureAll則在Configure和ConfigureAll之后執(zhí)行,即使Configure的代碼寫在了PostConfigure之后也是一樣。
至于為什么會(huì)是這樣的規(guī)則,下一節(jié)會(huì)詳細(xì)介紹。
二、內(nèi)部處理機(jī)制解析
1. 系統(tǒng)啟動(dòng)階段,依賴注入
上一節(jié)的例子中涉及到了三個(gè)接口IOptions、IOptionsSnapshot和IOptionsMonitor,那么就從這三個(gè)接口說起。既然Options模式是通過這三個(gè)接口的泛型方式注入提供服務(wù)的,那么在這之前系統(tǒng)就需要將它們對(duì)應(yīng)的實(shí)現(xiàn)注入到依賴注入容器中。這發(fā)生在系統(tǒng)啟動(dòng)階段創(chuàng)建IHost的時(shí)候,這時(shí)候HostBuilder的Build方法中調(diào)用了一個(gè)services.AddOptions()方法,這個(gè)方法定義在OptionsServiceCollectionExtensions中,代碼如下:
public static class OptionsServiceCollectionExtensions { 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; } public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.Configure(Options.Options.DefaultName, configureOptions); public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class { //省略非空驗(yàn)證代碼 services.AddOptions(); services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions)); return services; } public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class => services.Configure(name: null, configureOptions: configureOptions); //省略部分代碼 }
可見這個(gè)AddOptions方法的作用就是進(jìn)行服務(wù)注入,IOptions<>、IOptionsSnapshot<>對(duì)應(yīng)的實(shí)現(xiàn)是OptionsManager<>,只是分別采用了Singleton和Scoped兩種生命周期模式,IOptionsMonitor<>對(duì)應(yīng)的實(shí)現(xiàn)是OptionsMonitor<>,同樣為Singleton模式,這也驗(yàn)證了上一節(jié)例子中的猜想。除了上面提到的三個(gè)接口外,還有IOptionsFactory<>和IOptionsMonitorCache<>兩個(gè)接口,這也是Options模式中非常重要的兩個(gè)組成部分,接下來的內(nèi)容中會(huì)用到。
另外的兩個(gè)Configure方法就是上一節(jié)例子中用到的將具體的Theme注冊(cè)到Options中的方法了。二者的區(qū)別就是是否為配置的option命名,而第一個(gè)Configure方法就未命名的方法,通過上面的代碼可知它實(shí)際上是傳入了一個(gè)默認(rèn)的Options.Options.DefaultName作為名稱,這個(gè)默認(rèn)值是一個(gè)空字符串,也就是說,未命名的Option相當(dāng)于是被命名為空字符串。最終都是按照已命名的方式也就是第二個(gè)Configure方法進(jìn)行處理。還有一個(gè)ConfigureAll方法,它是傳入了一個(gè)null作為Option的名稱,也是交由第二個(gè)Configure處理。
在第二個(gè)Configure方法中仍調(diào)用了一次AddOptions方法,然后才是將具體的類型進(jìn)行注入。AddOptions方法中采用的都是TryAdd方法進(jìn)行注入,已被注入的不會(huì)被再次注入。接下來注冊(cè)了一個(gè)IConfigureOptions<TOptions>接口,對(duì)應(yīng)的實(shí)現(xiàn)是ConfigureNamedOptions<TOptions>(name, configureOptions),它的代碼如下:
public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class { public ConfigureNamedOptions(string name, Action<TOptions> action) { Name = name; Action = action; } public string Name { get; } public Action<TOptions> Action { get; } 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); } } public void Configure(TOptions options) => Configure(Options.DefaultName, options); }
它在構(gòu)造方法中存儲(chǔ)了配置的名稱(Name)和創(chuàng)建方法(Action),它的兩個(gè)Configure方法用于在獲取Options的值的時(shí)候執(zhí)行對(duì)應(yīng)的Action來創(chuàng)建實(shí)例(例如示例中的Theme)。在此時(shí)不會(huì)被執(zhí)行。所以在此會(huì)出現(xiàn)3中類型的ConfigureNamedOptions,分別是Name值為具體值的、Name值為為空字符串的和Name值為null的。這分別對(duì)應(yīng)了第一節(jié)的例子中的為Option命名的Configure方法、不為Option命名的Configure方法、以及ConfigureAll方法。
此處用到的OptionsServiceCollectionExtensions和ConfigureNamedOptions對(duì)應(yīng)的是通過代碼直接注冊(cè)O(shè)ption的方式,例如第一節(jié)例子中的如下方式:
services.Configure<Theme>("ThemeBlack", theme => { new Theme { Color = "#000000", Name = "Black" }; });
如果是以Configuration作為數(shù)據(jù)源的方式,例如如下代碼
services.Configure<Theme>("ThemeBlue", Configuration.GetSection("Themes:0"));
用到的是OptionsServiceCollectionExtensions和ConfigureNamedOptions這兩個(gè)類的子類,分別為OptionsConfigurationServiceCollectionExtensions和NamedConfigureFromConfigurationOptions兩個(gè)類,通過它們的名字也可以知道是專門用于采用Configuration作為數(shù)據(jù)源用的,代碼類似,只是多了一條關(guān)于IOptionsChangeTokenSource的依賴注入,作用是將Configuration的關(guān)于數(shù)據(jù)源變化的監(jiān)聽和Options的關(guān)聯(lián)起來,當(dāng)數(shù)據(jù)源發(fā)生改變的時(shí)候可以及時(shí)更新Options中的值,主要的Configure方法代碼如下:
public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder) where TOptions : class { //省略驗(yàn)證代碼 services.AddOptions(); services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config)); return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder)); }
同樣還有PostConfigure和PostConfigureAll方法,和Configure、ConfigureAll方法類似,只不過注入的類型為IPostConfigureOptions<TOptions>。
2. Options值的獲取
Option值的獲取也就是從依賴注入容器中獲取相應(yīng)實(shí)現(xiàn)的過程。通過依賴注入階段,已經(jīng)知道了IOptions<>和IOptionsSnapshot<>對(duì)應(yīng)的實(shí)現(xiàn)是OptionsManager<>,就以O(shè)ptionsManager<>為例看一下依賴注入后的服務(wù)提供過程。OptionsManager<>代碼如下:
public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new() { private readonly IOptionsFactory<TOptions> _factory; private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>(); public OptionsManager(IOptionsFactory<TOptions> factory) { _factory = factory; } public TOptions Value { get { return Get(Options.DefaultName); } } public virtual TOptions Get(string name) { name = name ?? Options.DefaultName; return _cache.GetOrAdd(name, () => _factory.Create(name)); } }
它有IOptionsFactory<TOptions>和OptionsCache<TOptions>兩個(gè)重要的成員。如果直接獲取Value值,實(shí)際上是調(diào)用的另一個(gè)Get(string name)方法,傳入了空字符串作為name值。所以最終值的獲取還是在緩存中讀取,這里的代碼是_cache.GetOrAdd(name, () => _factory.Create(name)),即如果緩存中存在對(duì)應(yīng)的值,則返回,如果不存在,則由_factory去創(chuàng)建。OptionsFactory<TOptions>的代碼如下:
public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class, new() { private readonly IEnumerable<IConfigureOptions<TOptions>> _setups; private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures; private readonly IEnumerable<IValidateOptions<TOptions>> _validations; public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, validations: null) { } public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations) { _setups = setups; _postConfigures = postConfigures; _validations = validations; } public TOptions Create(string name) { var options = new TOptions(); foreach (var setup in _setups) { if (setup is IConfigureNamedOptions<TOptions> 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<string>(); 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; } }
主要看它的TOptions Create(string name)方法。這里會(huì)遍歷它的_setups集合,這個(gè)集合類型為IEnumerable<IConfigureOptions<TOptions>>,在講Options模式的依賴注入的時(shí)候已經(jīng)知道,每一個(gè)Configure、ConfigureAll實(shí)際上就是向依賴注入容器中注冊(cè)了一個(gè)IConfigureOptions<TOptions>,只是名稱可能不同。而PostConfigure和PostConfigureAll方法注冊(cè)的是IPostConfigureOptions<TOptions>類型,對(duì)應(yīng)的就是_postConfigures集合。
首先會(huì)遍歷_setups集合,調(diào)用IConfigureOptions<TOptions>的Configure方法,這個(gè)方法的主要代碼就是:
if (Name == null || name == Name) { Action?.Invoke(options); }
如果Name值為null,即對(duì)應(yīng)的是ConfigureAll方法,則執(zhí)行該Action?;蛘逳ame值和需要讀取的值相同,則執(zhí)行該Action。
_setups集合遍歷之后,同樣的機(jī)制遍歷_postConfigures集合。這就是上一節(jié)關(guān)于Configure、ConfigureAll、PostConfigure和PostConfigureAll的執(zhí)行順序的驗(yàn)證。
最終返回對(duì)應(yīng)的實(shí)例并寫入緩存。這就是IOptions和IOptionsSnapshot兩種模式的處理機(jī)制,接下來看一下IOptionsMonitor模式,它對(duì)應(yīng)的實(shí)現(xiàn)是OptionsMonitor。代碼如下:
public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions> where TOptions : class, new() { private readonly IOptionsMonitorCache<TOptions> _cache; private readonly IOptionsFactory<TOptions> _factory; private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources; internal event Action<TOptions, string> _onChange; public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> 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); } } public TOptions CurrentValue { get => Get(Options.DefaultName); } public virtual TOptions Get(string name) { name = name ?? Options.DefaultName; return _cache.GetOrAdd(name, () => _factory.Create(name)); } public IDisposable OnChange(Action<TOptions, string> listener) { var disposable = new ChangeTrackerDisposable(this, listener); _onChange += disposable.OnChange; return disposable; } internal class ChangeTrackerDisposable : IDisposable { private readonly Action<TOptions, string> _listener; private readonly OptionsMonitor<TOptions> _monitor; public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener) { _listener = listener; _monitor = monitor; } public void OnChange(TOptions options, string name) => _listener.Invoke(options, name); public void Dispose() => _monitor._onChange -= OnChange; } }
大部分功能和OptionsManager類似,只是由于它是采用了Singleton模式,所以它是采用監(jiān)聽數(shù)據(jù)源改變并更新的模式。當(dāng)通過Configuration作為數(shù)據(jù)源注冊(cè)O(shè)ption的時(shí)候,多了一條IOptionsChangeTokenSource的依賴注入。當(dāng)數(shù)據(jù)源發(fā)生改變的時(shí)候更新數(shù)據(jù)并觸發(fā)OnChange(Action<TOptions, string> listener),在第一節(jié)的數(shù)據(jù)更新提醒中有相關(guān)的例子。
到此這篇關(guān)于詳解ASP.NET Core3.0 配置的Options模式的文章就介紹到這了,更多相關(guān)ASP.NET Core3.0 配置Options模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET中實(shí)現(xiàn)jQuery Validation-Engine的Ajax驗(yàn)證實(shí)現(xiàn)代碼
在jQuery的表變驗(yàn)證插件中Validation-Engine是一款高質(zhì)量的產(chǎn)品,提示效果非常精美,而且里面包含了AJAX驗(yàn)證功能2012-05-05ASP.NET 2.0中預(yù)設(shè)的cookie
ASP.NET 2.0中預(yù)設(shè)的cookie...2006-09-09Asp.Mvc 2.0用戶的編輯與刪除實(shí)例講解(5)
這篇文章主要介紹了Asp.Mvc 2.0用戶的編輯與刪除功能,需要的朋友可以參考下2015-08-08.NET?6開發(fā)TodoList應(yīng)用之實(shí)現(xiàn)Repository模式
這篇文章主要介紹了如何實(shí)現(xiàn)一個(gè)可重用的Repository模塊。文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2021-12-12.NET讀寫Excel工具Spire.Xls使用入門教程(1)
這篇文章主要為大家詳細(xì)介紹了.NET讀寫Excel工具Spire.Xls使用入門教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11this connector is disabled錯(cuò)誤的解決方法
打開editor/filemanager/connectors/aspx/config.ascx修改CheckAuthentication()方法,返回true2008-11-11win2003服務(wù)器.NET+IIS環(huán)境常見問題排障總結(jié)
在使用iis運(yùn)行asp.net環(huán)境的時(shí)候,總是會(huì)或多或少的碰到各種各樣的.net運(yùn)行錯(cuò)誤,這里特別從網(wǎng)絡(luò)整理了下,方便需要的朋友。2011-08-08