Written in front

After the previous three articles on .NET Core Configuration, this article focuses on how to extend a Configuration component. If the previous three articles are not seen, you can click on the address below to access

After understanding the source code of Configuration, it is relatively simple to extend a component. Next we will create a Consul-based configuration component based on .NET Core 3.0-preview5.

I believe that everyone has a better understanding of Consul, many projects will use Consul as a configuration center, and there is no other explanation here, mainly to talk about some ideas for creating a Consul configuration extension. When using the Consul configuration function, we can convert the information into JSON format and then store it, then we read it as if it were read from a JSON file.

Preparation before development

Initialize Consul

Assuming you have installed and started Consul, we open the Key/Value function interface and create two sets of configuration options, namely commonservice and userservice, as shown below.


DotNET_Core_3.0_source-Configuration_extension_component_0.png

 

Configuration values ​​are in JSON format


DotNET_Core_3.0_source-Configuration_extension_component_1.png

 

Implementation ideas

We know that in the entire design framework of the Configuration, the more important class ConfigurationRoot, there is an IConfigurationProvider collection property inside, which means that we append the IConfigurationProvider instance will eventually be placed into the collection, as shown below


DotNET_Core_3.0_source-Configuration_extension_component_2.png

 

In this project, I used a packaged Consul (V0.7.2.6) class library, and based on the .NET Core design style, I made the following frame design.


DotNET_Core_3.0_source-Configuration_extension_component_3.png

 

Considering that I will create a ConsulClient instance inside the component, I abstracted some of the parameters of the ConsulClient constructor and added it to IConsulConfigurationSource to enhance the flexibility of the component.

As mentioned before, the configuration information in Consul is stored in JSON format, so Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser is used here to convert the information in JSON format into the general format Key/Value of Configuration.

Core code

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 constructor instance, customizable incoming
  13:      /// </summary>
  14:      Action<ConsulClientConfiguration> ConsulClientConfiguration { get; set; }
  15:   
  16:      /// <summary>
  17:       /// Consul constructor instance, customizable incoming
  18:      /// </summary>
  19:      Action<HttpClient> ConsulHttpClient { get; set; }
  20:   
  21:      /// <summary>
  22:       /// Consul constructor instance, customizable incoming
  23:      /// </summary>
  24:      Action<HttpClientHandler> ConsulHttpClientHandler { get; set; }
  25:   
  26:      /// <summary>
  27:       /// Service Name
  28:      /// </summary>
  29:      string ServiceKey { get; }
  30:   
  31:      /// <summary>
  32:       /// Optional
  33:      /// </summary>
  34:      bool Optional { get; set; }
  35:   
  36:      /// <summary>
  37:       /// Consul query options
  38:      /// </summary>
  39:      QueryOptions QueryOptions { get; set; }
  40:   
  41:      /// <summary>
  42:       /// Reload delay time in milliseconds
  43:      /// </summary>
  44:      int ReloadDelay { get; set; }
  45:   
  46:      /// <summary>
  47:       /// Whether to reload when the configuration changes
  48:      /// </summary>
  49:      bool ReloadOnChange { get; set; }
  50:   }

ConsulConfigurationSource

This class provides a constructor for receiving ServiceKey and CancellationToken instances.

   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:   }

Its build() method is also relatively simple, mainly to initialize the ConsulConfigurationParser instance.

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

ConsulConfigurationParser

This class is more complicated, mainly to achieve the acquisition, monitoring and fault-tolerant processing of the Consul configuration. The public method source code is as follows

   1:  /// <summary>
   2:   /// Get and convert Consul configuration information
   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:           }
  twenty one:   
  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 configuration information monitoring
  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:   }

In addition, the monitoring of Consul mainly uses the QueryResult.LastIndex property, which caches the value of the property and compares it with the value obtained to determine whether the cache configuration in memory needs to be reloaded.

ConsulConfigurationProvider

In addition to implementing the Load method, the class also registers the OnChange event in the constructor according to the ReloadOnChange property, which is used to reload the configuration information. The source code is as follows:

   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:   }

Call and run result

Here the call is implemented in 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:                   });
  twenty one:   
  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:   }

The result of the operation, as shown in the following figure, we have loaded two instances of ConsulProvider, which is consistent with the two Consul configurations we added in the Program, and the values ​​loaded are also consistent with the Key/Value style of .NET Core Configuration. The value loaded is also consistent with the one stored in Consul


DotNET_Core_3.0_source-Configuration_extension_component_4.png

 


DotNET_Core_3.0_source-Configuration_extension_component_5.png

 


DotNET_Core_3.0_source-Configuration_extension_component_6.png

 

to sum up

Extending a configuration component based on source code is still relatively simple. In addition, it should be noted that the JSON processing of this component is mainly based on the .NET Core native class library, which is located in System.Text.Json in the namespace, so the component cannot be Running in versions prior to .NET Core 3.0 requires the introduction of additional JSON component assisted processing.

Orignal link:https://www.cnblogs.com/edison0621/p/10922281.html