.NET Core 3.0之建立基於Consul的Configuration擴充套件元件

艾心❤發表於2019-05-25

寫在前面

經過前面三篇關於.NET Core Configuration的文章之後,本篇文章主要討論如何擴充套件一個Configuration元件出來。如果前面三篇文章沒有看到,可以點選如下地址訪問

瞭解了Configuration的原始碼後,再去擴充套件一個元件就會比較簡單,接下來我們將在.NET Core 3.0-preview5的基礎上建立一個基於Consul的配置元件。

相信大家對Consul已經比較瞭解了,很多專案都會使用Consul作為配置中心,此處也不做其他闡述了,主要是講一下,建立Consul配置擴充套件的一些思路。使用Consul配置功能時,我們可以將資訊轉成JSON格式後再儲存,那麼我們在讀取的時候,在體驗上就像是從讀取JSON檔案中讀取一樣。

開發前的準備

初始化Consul

假設你已經安裝並啟動了Consul,我們開啟Key/Value功能介面,建立兩組配置選項出來,分別是commonservice和userservice,如下圖所示

Consul-key-value-dashboard

配置值採用JSON格式

Consul-key-value-commonservice-json

實現思路

我們知道在Configuration整個的設計框架裡,比較重要的類ConfigurationRoot,內部又有一個IConfigurationProvider集合屬性,也就是說我們追加IConfigurationProvider例項最終也會被放到到該集合中,如下圖所示

rootproviders

該專案中,我使用到了一個已經封裝好的Consul(V0.7.2.6)類庫,同時基於.NET Core關於Configuration的設計風格,做如下的框架設計

consul

考慮到我會在該元件內部建立ConsulClient例項,所以對ConsulClient建構函式的一部分引數做了抽象提取,並新增到了IConsulConfigurationSource中,以增強該元件的靈活性。

之前說過,Consul中的配置資訊是以JSON格式儲存的,所以此處使用到了Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser,用以將JSON格式的資訊轉換為Configuration的通用格式Key/Value。

核心程式碼

IConsulConfigurationSource

   1:  /// <summary>
   2:  /// ConsulConfigurationSource
   3:  /// </summary>
   4:  public interface IConsulConfigurationSource : IConfigurationSource
   5:  {
   6:      /// <summary>
   7:      /// CancellationToken
   8:      /// </summary>
   9:      CancellationToken CancellationToken { get; }
  10:   
  11:      /// <summary>
  12:      /// Consul建構函式例項,可自定義傳入
  13:      /// </summary>
  14:      Action<ConsulClientConfiguration> ConsulClientConfiguration { get; set; }
  15:   
  16:      /// <summary>
  17:      ///  Consul建構函式例項,可自定義傳入
  18:      /// </summary>
  19:      Action<HttpClient> ConsulHttpClient { get; set; }
  20:   
  21:      /// <summary>
  22:      ///  Consul建構函式例項,可自定義傳入
  23:      /// </summary>
  24:      Action<HttpClientHandler> ConsulHttpClientHandler { get; set; }
  25:   
  26:      /// <summary>
  27:      /// 服務名稱
  28:      /// </summary>
  29:      string ServiceKey { get; }
  30:   
  31:      /// <summary>
  32:      /// 可選項
  33:      /// </summary>
  34:      bool Optional { get; set; }
  35:   
  36:      /// <summary>
  37:      /// Consul查詢選項
  38:      /// </summary>
  39:      QueryOptions QueryOptions { get; set; }
  40:   
  41:      /// <summary>
  42:      /// 重新載入延遲時間,單位是毫秒
  43:      /// </summary>
  44:      int ReloadDelay { get; set; }
  45:   
  46:      /// <summary>
  47:      /// 是否在配置改變的時候重新載入
  48:      /// </summary>
  49:      bool ReloadOnChange { get; set; }
  50:  }

ConsulConfigurationSource

該類提供了一個建構函式,用於接收ServiceKey和CancellationToken例項

   1:  public ConsulConfigurationSource(string serviceKey, CancellationToken cancellationToken)
   2:  {
   3:      if (string.IsNullOrWhiteSpace(serviceKey))
   4:      {
   5:          throw new ArgumentNullException(nameof(serviceKey));
   6:      }
   7:   
   8:      this.ServiceKey = serviceKey;
   9:      this.CancellationToken = cancellationToken;
  10:  }

其build()方法也比較簡單,主要是初始化ConsulConfigurationParser例項

   1:  public IConfigurationProvider Build(IConfigurationBuilder builder)
   2:  {
   3:      ConsulConfigurationParser consulParser = new ConsulConfigurationParser(this);
   4:   
   5:      return new ConsulConfigurationProvider(this, consulParser);
   6:  }

ConsulConfigurationParser

該類比較複雜,主要實現Consul配置的獲取、監控以及容錯處理,公共方法原始碼如下

   1:  /// <summary>
   2:  /// 獲取並轉換Consul配置資訊
   3:  /// </summary>
   4:  /// <param name="reloading"></param>
   5:  /// <param name="source"></param>
   6:  /// <returns></returns>
   7:  public async Task<IDictionary<string, string>> GetConfig(bool reloading, IConsulConfigurationSource source)
   8:  {
   9:      try
  10:      {
  11:          QueryResult<KVPair> kvPair = await this.GetKvPairs(source.ServiceKey, source.QueryOptions, source.CancellationToken).ConfigureAwait(false);
  12:          if ((kvPair?.Response == null) && !source.Optional)
  13:          {
  14:              if (!reloading)
  15:              {
  16:                  throw new FormatException(Resources.Error_InvalidService(source.ServiceKey));
  17:              }
  18:   
  19:              return new Dictionary<string, string>();
  20:          }
  21:   
  22:          if (kvPair?.Response == null)
  23:          {
  24:              throw new FormatException(Resources.Error_ValueNotExist(source.ServiceKey));
  25:          }
  26:   
  27:          this.UpdateLastIndex(kvPair);
  28:   
  29:          return JsonConfigurationFileParser.Parse(source.ServiceKey, new MemoryStream(kvPair.Response.Value));
  30:      }
  31:      catch (Exception exception)
  32:      {
  33:          throw exception;
  34:      }
  35:  }
  36:   
  37:  /// <summary>
  38:  /// Consul配置資訊監控
  39:  /// </summary>
  40:  /// <param name="key"></param>
  41:  /// <param name="cancellationToken"></param>
  42:  /// <returns></returns>
  43:  public IChangeToken Watch(string key, CancellationToken cancellationToken)
  44:  {
  45:      Task.Run(() => this.RefreshForChanges(key, cancellationToken), cancellationToken);
  46:   
  47:      return this.reloadToken;
  48:  }

另外,關於Consul的監控主要利用了QueryResult.LastIndex屬性,該類快取了該屬性的值,並與實獲取的值進行比較,以判斷是否需要重新載入記憶體中的快取配置

ConsulConfigurationProvider

該類除了實現Load方法外,還會根據ReloadOnChange屬性,在建構函式中註冊OnChange事件,用於重新載入配置資訊,原始碼如下:

   1:  public sealed class ConsulConfigurationProvider : ConfigurationProvider
   2:  {
   3:      private readonly ConsulConfigurationParser configurationParser;
   4:      private readonly IConsulConfigurationSource source;
   5:   
   6:      public ConsulConfigurationProvider(IConsulConfigurationSource source, ConsulConfigurationParser configurationParser)
   7:      {
   8:          this.configurationParser = configurationParser;
   9:          this.source = source;
  10:   
  11:          if (source.ReloadOnChange)
  12:          {
  13:              ChangeToken.OnChange(
  14:                  () => this.configurationParser.Watch(this.source.ServiceKey, this.source.CancellationToken),
  15:                  async () =>
  16:                  {
  17:                      await this.configurationParser.GetConfig(true, source).ConfigureAwait(false);
  18:   
  19:                      Thread.Sleep(source.ReloadDelay);
  20:   
  21:                      this.OnReload();
  22:                  });
  23:          }
  24:      }
  25:   
  26:      public override void Load()
  27:      {
  28:          try
  29:          {
  30:              this.Data = this.configurationParser.GetConfig(false, this.source).ConfigureAwait(false).GetAwaiter().GetResult();
  31:          }
  32:          catch (AggregateException aggregateException)
  33:          {
  34:              throw aggregateException.InnerException;
  35:          }
  36:      }
  37:  }

呼叫及執行結果

此處呼叫在Program中實現

   1:  public class Program
   2:  {
   3:      public static void Main(string[] args)
   4:      {
   5:          CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
   6:   
   7:          WebHost.CreateDefaultBuilder(args).ConfigureAppConfiguration(
   8:              (hostingContext, builder) =>
   9:              {
  10:                  builder.AddConsul("userservice", cancellationTokenSource.Token, source =>
  11:                  {
  12:                      source.ConsulClientConfiguration = cco => cco.Address = new Uri("http://localhost:8500");
  13:                      source.Optional = true;
  14:                      source.ReloadOnChange = true;
  15:                      source.ReloadDelay = 300;
  16:                      source.QueryOptions = new QueryOptions
  17:                      {
  18:                          WaitIndex = 0
  19:                      };
  20:                  });
  21:   
  22:                  builder.AddConsul("commonservice", cancellationTokenSource.Token, source =>
  23:                  {
  24:                      source.ConsulClientConfiguration = cco => cco.Address = new Uri("http://localhost:8500");
  25:                      source.Optional = true;
  26:                      source.ReloadOnChange = true;
  27:                      source.ReloadDelay = 300;
  28:                      source.QueryOptions = new QueryOptions
  29:                      {
  30:                          WaitIndex = 0
  31:                      };
  32:                  });
  33:              }).UseStartup<Startup>().Build().Run();
  34:      }
  35:  }

執行結果,如下圖所示,我們已經載入到了兩個ConsulProvider例項,這與我們在Program中新增的兩個Consul配置一致,其中所載入到的值也和.NET Core Configuration的Key/Value風格相一致,所載入到的值也會Consul中所儲存的相一致

image

image

image

總結

基於原始碼擴充套件一個配置元件出來,還是比較簡單的,另外需要說明的是,該元件關於JSON的處理主要基於.NET Core原生元件,位於名稱空間內的System.Text.Json中,所以該元件無法在.NET Core 3.0之前的版本中執行,需要引入額外的JSON元件輔助處理。

原始碼已經託管於GitHub,地址:https://github.com/littlehorse8/Navyblue.Extensions.Configuration.Consul,記得點個小星星哦

相關文章