溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

ASP.NET Core3.0 配置Options模式的方法

發(fā)布時間:2020-08-15 11:37:33 來源:億速云 閱讀:222 作者:小新 欄目:開發(fā)技術

這篇文章主要介紹了ASP.NET Core3.0 配置Options模式的方法,具有一定借鑒價值,需要的朋友可以參考下。希望大家閱讀完這篇文章后大有收獲。下面讓小編帶著大家一起了解一下。

一、Options的使用

上一章有個配置的綁定的例子,可以將配置綁定到一個Theme實例中。也就是在使用對應配置的時候,需要進行一次綁定操作。而Options模式提供了更直接的方式,并且可以通過依賴注入的方式提供配置的讀取。下文中稱每一條Options配置為Option。

1.簡單的不為Option命名的方式

依然采用這個例子,在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文件中做注冊:

  public void ConfigureServices(IServiceCollection services)
  {
    services.Configure<Theme>(Configuration.GetSection("Theme"));

    services.AddControllersWithViews(); //3.0中啟用的新方法
  }

請求這個Action,獲取到的結果為:

options:Blue

這樣就可以在需要使用該配置的時候通過依賴注入的方式使用了。但有個疑問,這里將“Theme”類型綁定了這樣的配置,但如果有多個這樣的配置呢?就如同下面這樣的配置的時候:

 "Themes": [
  {
   "Name": "Blue",
   "Color": "#0921DC"
  },
  {
   "Name": "Red",
   "Color": "#FF4500"
  }
 ]

在這樣的情況下,存在多個Theme的配置,這樣對于之前這種依賴注入的方式就不行了。這時系統(tǒng)提供了將注入的Options進行命名的方法。

2.為Option命名的方式

首先需要在Startup文件中注冊的時候對其命名,添加如下兩條注冊代碼:

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}" };
    }

請求這個Action,獲取到的結果為:

options:Blue,optionsSnapshot:Red,optionsMonitor:Gray

新增的兩種注入方式通過Options的名稱獲取到了對應的Options。為什么是兩種呢?它們有什么區(qū)別?不知道有沒有讀者想到上一章配置的重新加載功能。在配置注冊的時候,有個reloadOnChange選項,如果它被設置為true的,當對應的數(shù)據(jù)源發(fā)生改變的時候,會進行重新加載。而Options怎么能少了這樣的特性呢。

3.Option的自動更新與生命周期

為了驗證這三種Options的讀取方式的特性,修改Theme類,添加一個Guid字段,并在構造方法中對其賦值,代碼如下:

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}"
  };
}

請求這個Action,返回結果如下:

options:Blue|ad328f15-254f-4505-a79f-4f27db4a393e,optionsSnapshot:Red|dba5f550-29ca-4779-9a02-781dd17f595a,optionsMonitor:Gray|a799fa41-9444-45dd-b51b-fcd15049f98f

刷新頁面,返回結果為:

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ā)生了改變,每次刷新頁面均會改變。這類似前面講依賴注入時做測試的例子,現(xiàn)在猜測Guid未改變的IOptions和IOptionsMonitor兩種方式是采用了Singleton模式,而Guid發(fā)生改變的IOptionsSnapshot方式是采用了Scoped或Transient模式。如果在這個Action中多次采用IOptionsSnapshot讀取_optionsSnapshot.Get("ThemeBlue").Guid的值,會發(fā)現(xiàn)同一次請求的值是相同的,不同請求之間的值是不同的,也就是IOptionsSnapshot方式使采用了Scoped模式(此驗證示例比較簡單,請讀者自行修改代碼驗證)。

在這樣的情況下,修改三種獲取方式對應的配置項的Name值,例如分別修改為“Blue1”、“Red1”和“Gray1”,再次多次刷新頁面查看返回值,會發(fā)現(xiàn)如下情況:

IOptions方式:Name和Guid的值始終未變。Name值仍為Blue。

IOptionsSnapshot方式:Name值變?yōu)镽ed1,Guid值單次請求內(nèi)相同,每次刷新之間不同。

IOptionsMonitor方式:只有修改配置值后第一次刷新的時候將Name值變?yōu)榱薌ray1,Guid未改變。之后多次刷新,這兩個值均未做改變。

總結:IOptions和IOptionsMonitor兩種方式采用了Singleton模式,但區(qū)別在于IOptionsMonitor會監(jiān)控對應數(shù)據(jù)源的變化,如果發(fā)生了變化則更新實例的配置值,但不會重新提供新的實例。IOptionsSnapshot方式采用了Scoped模式每次請求采用同一個實例,在下一次請求的時候獲取到的是一個新的實例,所以如果數(shù)據(jù)源發(fā)生了改變,會讀取到新的值。先大概記一下這一的情況,在下文剖析IOptions的內(nèi)部處理機制的時候就會明白為什么會這樣。

4.數(shù)據(jù)更新提醒

IOptionsMonitor方式還提供了一個OnChange方法,當數(shù)據(jù)源發(fā)生改變的時候會觸發(fā)它,所以如果想在這時候做點什么,可以利用這個方法實現(xiàn)。示例代碼:

_optionsMonitor.OnChange((theme,name)=> { Console.WriteLine(theme.Name +"-"+ name); });

5.不采用Configuration配置作為數(shù)據(jù)源的方式

上面的例子都是采用了讀取配置的方式,實際上Options模式和上一章的Configuration配置方式使分開的,讀取配置只不過是Options模式的一種實現(xiàn)方式,例如可以不使用Configuration中的數(shù)據(jù),直接通過如下代碼注冊:

services.Configure<Theme>("ThemeBlack", theme => {
  theme.Color = "#000000";
  theme.Name = "Black";
}); 

6.ConfigureAll方法

系統(tǒng)提供了一個ConfigureAll方法,可以將所有對應的實例統(tǒng)一設置。例如如下代碼:

services.ConfigureAll<Theme>(theme => {
   theme.Color = "#000000";
   theme.Name = "Black2";
});

此時無論通過什么名稱去獲取Theme的實例,包括不存在對應設置的名稱,獲取到的值都是本次通過ConfigureAll設置的值。

7.PostConfigure和PostConfigureAll方法

這兩個方法和Configure、ConfigureAll方法類似,只是它們會在Configure、ConfigureAll之后執(zhí)行。

8.多個Configure、ConfigureAll、PostConfigure和PostConfigureAll的執(zhí)行順序

可以這樣理解,每個Configure都是去修改一個名為其設置的名稱的變量,以如下代碼為例:

services.Configure<Theme>("ThemeBlack", theme => {
  theme.Color = "#000000";
  theme.Name = "Black";
}); 

這條設置就是去修改(注意是修改而不是替換)一個名為"ThemeBlack"的Theme類型的變量,如果該變量不存在,則創(chuàng)建一個Theme實例并賦值。這樣就生成了一些變量名為“空字符串、“ThemeBlue”、“ThemeBlack”的變量(只是舉例,忽略空字符串作為變量名不合法的顧慮)”。

依次按照代碼的順序執(zhí)行,這時候如果后面的代碼中出現(xiàn)同名的Configure,則修改對應名稱的變量的值。如果是ConfigureAll方法,則修改所有類型為Theme的變量的值。

而PostConfigure和PostConfigureAll則在Configure和ConfigureAll之后執(zhí)行,即使Configure的代碼寫在了PostConfigure之后也是一樣。

至于為什么會是這樣的規(guī)則,下一節(jié)會詳細介紹。

二、內(nèi)部處理機制解析

1. 系統(tǒng)啟動階段,依賴注入

上一節(jié)的例子中涉及到了三個接口IOptions、IOptionsSnapshot和IOptionsMonitor,那么就從這三個接口說起。既然Options模式是通過這三個接口的泛型方式注入提供服務的,那么在這之前系統(tǒng)就需要將它們對應的實現(xiàn)注入到依賴注入容器中。這發(fā)生在系統(tǒng)啟動階段創(chuàng)建IHost的時候,這時候HostBuilder的Build方法中調用了一個services.AddOptions()方法,這個方法定義在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
    {
      //省略非空驗證代碼

      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);
//省略部分代碼
  }

可見這個AddOptions方法的作用就是進行服務注入,IOptions<>、IOptionsSnapshot<>對應的實現(xiàn)是OptionsManager<>,只是分別采用了Singleton和Scoped兩種生命周期模式,IOptionsMonitor<>對應的實現(xiàn)是OptionsMonitor<>,同樣為Singleton模式,這也驗證了上一節(jié)例子中的猜想。除了上面提到的三個接口外,還有IOptionsFactory<>和IOptionsMonitorCache<>兩個接口,這也是Options模式中非常重要的兩個組成部分,接下來的內(nèi)容中會用到。

另外的兩個Configure方法就是上一節(jié)例子中用到的將具體的Theme注冊到Options中的方法了。二者的區(qū)別就是是否為配置的option命名,而第一個Configure方法就未命名的方法,通過上面的代碼可知它實際上是傳入了一個默認的Options.Options.DefaultName作為名稱,這個默認值是一個空字符串,也就是說,未命名的Option相當于是被命名為空字符串。最終都是按照已命名的方式也就是第二個Configure方法進行處理。還有一個ConfigureAll方法,它是傳入了一個null作為Option的名稱,也是交由第二個Configure處理。

在第二個Configure方法中仍調用了一次AddOptions方法,然后才是將具體的類型進行注入。AddOptions方法中采用的都是TryAdd方法進行注入,已被注入的不會被再次注入。接下來注冊了一個IConfigureOptions<TOptions>接口,對應的實現(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&#63;.Invoke(options);
    }
  }

  public void Configure(TOptions options) => Configure(Options.DefaultName, options);
}

它在構造方法中存儲了配置的名稱(Name)和創(chuàng)建方法(Action),它的兩個Configure方法用于在獲取Options的值的時候執(zhí)行對應的Action來創(chuàng)建實例(例如示例中的Theme)。在此時不會被執(zhí)行。所以在此會出現(xiàn)3中類型的ConfigureNamedOptions,分別是Name值為具體值的、Name值為為空字符串的和Name值為null的。這分別對應了第一節(jié)的例子中的為Option命名的Configure方法、不為Option命名的Configure方法、以及ConfigureAll方法。

此處用到的OptionsServiceCollectionExtensions和ConfigureNamedOptions對應的是通過代碼直接注冊Option的方式,例如第一節(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這兩個類的子類,分別為OptionsConfigurationServiceCollectionExtensions和NamedConfigureFromConfigurationOptions兩個類,通過它們的名字也可以知道是專門用于采用Configuration作為數(shù)據(jù)源用的,代碼類似,只是多了一條關于IOptionsChangeTokenSource的依賴注入,作用是將Configuration的關于數(shù)據(jù)源變化的監(jiān)聽和Options的關聯(lián)起來,當數(shù)據(jù)源發(fā)生改變的時候可以及時更新Options中的值,主要的Configure方法代碼如下:

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
  where TOptions : class
{
  //省略驗證代碼

  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值的獲取也就是從依賴注入容器中獲取相應實現(xiàn)的過程。通過依賴注入階段,已經(jīng)知道了IOptions<>和IOptionsSnapshot<>對應的實現(xiàn)是OptionsManager<>,就以OptionsManager<>為例看一下依賴注入后的服務提供過程。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 &#63;&#63; Options.DefaultName;
    return _cache.GetOrAdd(name, () => _factory.Create(name));
  }
}

它有IOptionsFactory<TOptions>和OptionsCache<TOptions>兩個重要的成員。如果直接獲取Value值,實際上是調用的另一個Get(string name)方法,傳入了空字符串作為name值。所以最終值的獲取還是在緩存中讀取,這里的代碼是_cache.GetOrAdd(name, () => _factory.Create(name)),即如果緩存中存在對應的值,則返回,如果不存在,則由_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)方法。這里會遍歷它的_setups集合,這個集合類型為IEnumerable<IConfigureOptions<TOptions>>,在講Options模式的依賴注入的時候已經(jīng)知道,每一個Configure、ConfigureAll實際上就是向依賴注入容器中注冊了一個IConfigureOptions<TOptions>,只是名稱可能不同。而PostConfigure和PostConfigureAll方法注冊的是IPostConfigureOptions<TOptions>類型,對應的就是_postConfigures集合。

首先會遍歷_setups集合,調用IConfigureOptions<TOptions>的Configure方法,這個方法的主要代碼就是:

 if (Name == null || name == Name)
 {
   Action&#63;.Invoke(options);
 }

如果Name值為null,即對應的是ConfigureAll方法,則執(zhí)行該Action?;蛘逳ame值和需要讀取的值相同,則執(zhí)行該Action。

_setups集合遍歷之后,同樣的機制遍歷_postConfigures集合。這就是上一節(jié)關于Configure、ConfigureAll、PostConfigure和PostConfigureAll的執(zhí)行順序的驗證。

最終返回對應的實例并寫入緩存。這就是IOptions和IOptionsSnapshot兩種模式的處理機制,接下來看一下IOptionsMonitor模式,它對應的實現(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 &#63;&#63; 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 &#63;&#63; 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ù)源改變并更新的模式。當通過Configuration作為數(shù)據(jù)源注冊Option的時候,多了一條IOptionsChangeTokenSource的依賴注入。當數(shù)據(jù)源發(fā)生改變的時候更新數(shù)據(jù)并觸發(fā)OnChange(Action<TOptions, string> listener)。

感謝你能夠認真閱讀完這篇文章,希望小編分享ASP.NET Core3.0 配置Options模式的方法內(nèi)容對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業(yè)資訊頻道,遇到問題就找億速云,詳細的解決方法等著你來學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI