[Translate] How to use Consul to store configurations in ASP.

Source: Internet
Author: User
[Translate] How to use Consul to store configurations in ASP.

Original: USING CONSUL for storing the CONFIGURATION in ASP.
Author: Nathanael

[Translator Note: Because of anxious to share to everyone, so this article translation is very hasty, some inaccurate place also hope understanding]

The Consul from Hashicorp is a tool for distributed architectures that can be used for service discovery, running health checks, and KV storage. This article details how to use Consul to store configurations in ASP. NET Core by implementing Configurationprovider.

Why use tools to store configurations?

Typically, the configuration in a. Net application is stored in a configuration file, such as app. config, Web. config, or Appsettings.json. Starting with ASP. NET Core, a new extensible configuration framework is available that allows the configuration to be stored outside the configuration file and retrieved from the command line, environment variables, and so on.
The problem with configuration files is that they are difficult to manage. In fact, we usually end up with a configuration file and a corresponding transform file to cover each environment. They need to be deployed with DLLs, so changing the configuration means redeploying the configuration files and DLLs. Not very convenient.
Using separate tools for centralization allows us to do two things:

    • Have the same configuration on all machines
    • Ability to change values without redeploying any content (useful for feature-enabled shutdown)
Consul Introduction

The purpose of this article is not to discuss Consul, but to focus on how to integrate it with ASP.
However, a brief introduction is still necessary. Consul has a Key/value storage feature that is organized hierarchically and can create folders to map different applications, environments, and so on. This is an example of the hierarchy that will be used in this article. Each node can contain JSON values.

/|-- App1| |-- Dev| | |-- ConnectionStrings| | \-- Settings| |-- Staging| | |-- ConnectionStrings| | \-- Settings| \-- Prod|   |-- ConnectionStrings|   \-- Settings\-- App2  |-- Dev  | |-- ConnectionStrings  | \-- Settings  |-- Staging  | |-- ConnectionStrings  | \-- Settings  \-- Prod    |-- ConnectionStrings    \-- Settings

It provides a REST API for easy querying, and key is included in the query path. For example, the query that gets the configuration of APP1 in the Dev environment is as follows: Get HTTP ://8500/v1/kv/app1/dev/settings
The response is as follows:

HTTP/1.1 200 OKContent-Type: application/jsonX-Consul-Index: 1071X-Consul-Knownleader: trueX-Consul-Lastcontact: 0[    {        "LockIndex": 0,        "Key": "App1/Dev/Settings",        "Flags": 0,        "Value": "ewogIkludCI6NDIsCiAiT2JqZWN0IjogewogICJTdHJpbmciOiAidG90byIsCiAgIkJsYSI6IG51bGwsCiAgIk9iamVjdCI6IHsKICAgIkRhdGUiOiAiMjAxOC0wMi0yM1QxNjoyMTowMFoiCiAgfQogfQp9Cgo=",        "CreateIndex": 501,        "ModifyIndex": 1071    }]

The

can also recursively query any node, GET http:// , 8500/v1/kv/app1/dev?recurse return:

http/1.1 okcontent-type:application/jsonx-consul-index:1071x-consul-knownleader:truex-consul-lastcontact:0[{        "Lockindex": 0, "Key": "app1/dev/", "Flags": 0, "Value": null, "CreateIndex": 75,        "Modifyindex": "Lockindex": 0, "Key": "App1/dev/connectionstrings", "Flags": 0, "Value": " Ewoirgf0ywjhc2uioiaiu2vydmvypxrjcdpkymrldi5kyxrhymfzzs53aw5kb3dzlm5lddteyxrhymfzzt1teurhdgfcyxnlo1vzzxigsuq9w0xvz2lurm9yr Gjdqftzzxj2zxjoyw1lxttqyxnzd29yzd1tevbhc3n3b3jko1rydxn0zwrfq29ubmvjdglvbj1gywxzzttfbmnyexb0pvrydwu7iiwkiln0b3jhz2uioijezw zhdwx0rw5kcg9pbnrzuhjvdg9jb2w9ahr0chm7qwnjb3vude5hbwu9zgv2ywnjb3vuddtby2nvdw50s2v5pw15s2v5oyikfq== "," CreateIndex ": 155," Modifyindex ": 155}, {" Lockindex ": 0," Key ":" App1/dev/settings "," Flags ": 0," Value ":" Ewogikludci6ndisciait2jqzwn0ijogewogicjtdhjpbmcioiaidg90byisciagikjsysi6ig51bgwsciagik9iamvjdci6ihskicagikrhdguioiaimjaxoc0wmi0ym1qxnjoymtowmfoiciagfqogfqp9cgo= "," CreateIndex ": 501," Modifyindex ": 1071}] 

We can see a lot of content through this response, first we can see the value of each key is using the BASE64 encoding to avoid the value values and JSON itself confusion, and then we notice that the property "Index" in both JSON and HTTP headers. These properties are a timestamp, and they can let us know whether or when the value was created or updated. They can help us know if we need to reload these configurations.

ASP. NET Core configuration system

The infrastructure of this configuration relies on some of the content in the Microsoft.Extensions.Configuration.Abstractions nuget package. First, Iconfigurationprovider is an interface for providing configuration values, and then Iconfigurationsource is used to provide instances of provider that have implemented the above interfaces.
You can view some implementations on ASP.
A class named Configurationprovider can be inherited in the Microsoft.Extensions.Configuration package compared to the direct implementation of Iconfigurationprovider. This class provides some sample code (for example, an implementation of an overloaded token).
This class contains two important things:

/* Excerpt from the implementation */public abstract class ConfigurationProvider : IConfigurationProvider{    protected IDictionary<string, string> Data { get; set; }    public virtual void Load()    {    }}

Data is a dictionary that contains all the keys and values, and load is the method used at the beginning of the application, as its name implies, and it loads the configuration from somewhere (the configuration file or our consul instance) and populates the dictionary.

Load the consul configuration in ASP.

The first way we think of it is to use HttpClient to get the configuration in consul. Then, because the configuration is hierarchical, like a tree, we need to expand it so that it can be put into the dictionary, isn't it simple?

First, implement the Load method, but we need an asynchronous method, the original method will block, so join an asynchronous LoadAsync method

public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();

We will then query consul recursively to get the configuration value. It uses some of the objects defined in the class, such as _consulurls, which is an array to hold the URLs of consul instances (for failover), _path is the key prefix (for example, App1/dev). Once we get the JSON, we iterate over each key-value pair, decode the Base64 string, and flatten all the keys and JSON objects.

Private Async task<idictionary<string, string>> executequeryasync () {int consulurlindex = 0; while (true) {try {using (var httpClient = new HttpClient (new Httpclienthandler {AUTOMATICD Ecompression = Decompressionmethods.deflate | Decompressionmethods.gzip}, True)) using (var request = new Httprequestmessage (Httpmethod.get, New Uri (_consul            Urls[consulurlindex], "? Recurse=true"))) using (var response = await Httpclient.sendasync (request)) {Response.                Ensuresuccessstatuscode (); var tokens = Jtoken.parse (await response.                Content.readasstringasync ()); return tokens. Select (k = keyvaluepair.create (k.value<string> ("Key"). Substring (_path. Length + 1), k.value<string> ("Value")! = null? Jtoken.parse (Encoding.UTF8.GetString (convert.frombase64string (k.value<string> ("ValUE "))) (null)). Where (v =!string. Isnullorwhitespace (V.key)). SelectMany (Flatten). ToDictionary (v = configurationpath.combine (v.key.split ('/')), V = v.value, stringcomparer.ordinalignorecase)            ;            }} catch {consulurlindex++;        if (Consulurlindex >= _consulurls.count) throw; }    }}

The way to flatten the key values is to make a simple deep-first search of the tree.

private static IEnumerable<KeyValuePair<string, string>> Flatten(KeyValuePair<string, JToken> tuple){    if (!(tuple.Value is JObject value))        yield break;    foreach (var property in value)    {        var propertyKey = $"{tuple.Key}/{property.Key}";        switch (property.Value.Type)        {            case JTokenType.Object:                foreach (var item in Flatten(KeyValuePair.Create(propertyKey, property.Value)))                    yield return item;                break;            case JTokenType.Array:                break;            default:                yield return KeyValuePair.Create(propertyKey, property.Value.Value<string>());                break;        }    }}

The complete class code that contains the construction method and the private field is as follows:

public class simpleconsulconfigurationprovider:configurationprovider{private readonly string _path;    Private ReadOnly ireadonlylist<uri> _consulurls;        Public Simpleconsulconfigurationprovider (ienumerable<uri> consulurls, string path) {_path = path; _consulurls = consulurls.select (U = new Uri (U, $ "V1/kv/{path}")).        ToList ();        if (_consulurls.count <= 0) {throw new ArgumentOutOfRangeException (Nameof (consulurls)); }} public override void Load () = LoadAsync (). Configureawait (False). Getawaiter ().    GetResult ();    Private async Task LoadAsync () {Data = await executequeryasync ();        } private Async task<idictionary<string, string>> executequeryasync () {int consulurlindex = 0;                while (true) {try {var requesturi = '? Recurse=true '; using (var httpClient = new HttpClient (new Httpclienthandler {AUTOMATICDECOMPREssion = Decompressionmethods.deflate | Decompressionmethods.gzip}, True)) using (var request = new Httprequestmessage (Httpmethod.get, New Uri (_co                Nsulurls[consulurlindex], RequestUri))) using (var response = await Httpclient.sendasync (request)) {Response.                    Ensuresuccessstatuscode (); var tokens = Jtoken.parse (await response.                    Content.readasstringasync ()); return tokens. Select (k = keyvaluepair.create (k.value<string> ("Key"). Substring (_path. Length + 1), k.value<string> ("Value")! = null?                        Jtoken.parse (Encoding.UTF8.GetString (convert.frombase64string (k.value<string> ("Value"))): null ))                        . Where (v =!string. Isnullorwhitespace (V.key)). SelectMany (Flatten). ToDictionary (v = COnfigurationpath.combine (V.key.split ('/')), V = v.value, stringcomparer.ordinalignorecase);                }} catch {Consulurlindex = Consulurlindex + 1;            if (Consulurlindex >= _consulurls.count) throw; }}} private static ienumerable<keyvaluepair<string, string>> Flatten (keyvaluepair<string, JT Oken> tuple) {if (!) ( Tuple.        Value is jobject value) the yield break; foreach (var property in value) {var PropertyKey = $ "{tuple. Key}/{property.            Key} "; Switch (property. Value.Type) {case JTokenType.Object:foreach (var item in Flatten (Keyvaluepa ir. Create (PropertyKey, property.                    Value)) yield return item;                Break                Case JTokenType.Array:break; Default:yield returN Keyvaluepair.create (PropertyKey, property.                    Value.value<string> ());            Break }        }    }}
Dynamic Reload Configuration

We can further use the consul notice of change. It only works by adding a parameter (the value of the last index configuration), and the HTTP request is blocked until the next configuration change (or HttpClient timeout).

Compared to the previous class, we simply add a method listentoconfigurationchanges to listen for consul blocking HTTP in the background.

public class consulconfigurationprovider:configurationprovider{Private Const string Consulindexheader = "X-consul-in    Dex ";    Private readonly string _path;    Private ReadOnly HttpClient _httpclient;    Private ReadOnly ireadonlylist<uri> _consulurls;    Private ReadOnly Task _configurationlisteningtask;    private int _consulurlindex;    private int _failurecount;    private int _consulconfigurationindex;        Public Consulconfigurationprovider (ienumerable<uri> consulurls, string path) {_path = path; _consulurls = consulurls.select (U = new Uri (U, $ "V1/kv/{path}")).        ToList ();        if (_consulurls.count <= 0) {throw new ArgumentOutOfRangeException (Nameof (consulurls)); } _httpclient = new HttpClient (new Httpclienthandler {automaticdecompression = Decompressionmethods.deflate |        Decompressionmethods.gzip}, True);    _configurationlisteningtask = new Task (listentoconfigurationchanges); } public override VOID Load () = LoadAsync (). Configureawait (False). Getawaiter ().    GetResult ();        Private async Task LoadAsync () {Data = await executequeryasync ();    if (_configurationlisteningtask.status = = taskstatus.created) _configurationlisteningtask.start ();                Private async void Listentoconfigurationchanges () {while (true) {try {                    if (_failurecount > _consulurls.count) {_failurecount = 0;                Await Task.delay (Timespan.fromminutes (1));                } Data = await executequeryasync (true);                Onreload ();            _failurecount = 0;            } catch (taskcanceledexception) {_failurecount = 0;                } catch {_consulurlindex = (_consulurlindex + 1)% _consulurls.count;            _failurecount++; }}} Private Async task&Lt;idictionary<string, string>> executequeryasync (bool isblocking = false) {var RequestUri = IsBlockin G?        $ "? Recurse=true&index={_consulconfigurationindex}": "? Recurse=true"; using (var request = new Httprequestmessage (Httpmethod.get, New Uri (_consulurls[_consulurlindex], RequestUri))) usi Ng (var response = await _httpclient.sendasync (request)) {Response.            Ensuresuccessstatuscode (); if (response. Headers.contains (Consulindexheader)) {var indexvalue = response. Headers.getvalues (Consulindexheader).                FirstOrDefault (); Int.            TryParse (Indexvalue, out _consulconfigurationindex); } var tokens = Jtoken.parse (await response.            Content.readasstringasync ()); return tokens. Select (k = keyvaluepair.create (k.value<string> ("Key"). Substring (_path. Length + 1), K.value<string>("Value")! = null?                    Jtoken.parse (Encoding.UTF8.GetString (convert.frombase64string (k.value<string> ("Value"))): null ))                . Where (v =!string. Isnullorwhitespace (V.key)). SelectMany (Flatten). ToDictionary (v = configurationpath.combine (v.key.split ('/')), V = v.value, stringcomparer.ordinalignorecase)        ; }} private static ienumerable<keyvaluepair<string, string>> Flatten (keyvaluepair<string, jtoken> Tuple) {if (!) ( Tuple.        Value is jobject value) the yield break; foreach (var property in value) {var PropertyKey = $ "{tuple. Key}/{property.            Key} "; Switch (property. Value.Type) {case JTokenType.Object:foreach (var item in Flatten (Keyvaluepa ir. Create (PropertyKey, property.                    Value)) yield return item;                Break      Case Jtokentype.array:              Break Default:yield return Keyvaluepair.create (PropertyKey, property.                    Value.value<string> ());            Break }        }    }}
Grouped together

Now that we have a configurationprovider, let's write a configurationsource to create our provider.

public class ConsulConfigurationSource : IConfigurationSource{    public IEnumerable<Uri> ConsulUrls { get; }    public string Path { get; }    public ConsulConfigurationSource(IEnumerable<Uri> consulUrls, string path)    {        ConsulUrls = consulUrls;        Path = path;    }    public IConfigurationProvider Build(IConfigurationBuilder builder)    {        return new ConsulConfigurationProvider(ConsulUrls, Path);    }}

And some extension methods:

public static class ConsulConfigurationExtensions{    public static IConfigurationBuilder AddConsul(this IConfigurationBuilder configurationBuilder, IEnumerable<Uri> consulUrls, string consulPath)    {        return configurationBuilder.Add(new ConsulConfigurationSource(consulUrls, consulPath));    }    public static IConfigurationBuilder AddConsul(this IConfigurationBuilder configurationBuilder, IEnumerable<string> consulUrls, string consulPath)    {        return configurationBuilder.AddConsul(consulUrls.Select(u => new Uri(u)), consulPath);    }}

You can now add Consul to Program.cs and use other sources, such as environment variables or command-line arguments, to provide a URL to Consul

public static IWebHost BuildWebHost(string[] args) =>    WebHost.CreateDefaultBuilder(args)    .ConfigureAppConfiguration(cb =>        {            var configuration = cb.Build();            cb.AddConsul(new[] { configuration.GetValue<Uri>("CONSUL_URL") }, configuration.GetValue<string>("CONSUL_PATH"));        })        .UseStartup<Startup>()        .Build();

Now, you can use the standard configuration mode for ASP. NET Core, such as Options.

public void ConfigureServices(IServiceCollection services){    services.AddMvc();    services.AddOptions();    services.Configure<AppSettingsOptions>(Configuration.GetSection("Settings"));    services.Configure<AccountingFeaturesOptions>(Configuration.GetSection("FeatureFlags"));    services.Configure<CartFeaturesOptions>(Configuration.GetSection("FeatureFlags"));    services.Configure<CatalogFeaturesOptions>(Configuration.GetSection("FeatureFlags"));}

To use them in our code, note how to use the options, and use Ioptions to get the initial value for options that can be dynamically reloaded . Instead, ASP. NET Core needs to use Ioptionssnapshot .
This is great for feature switching because you can enable or disable new features by changing the values in Consul, and users can use these new features without republishing. Similarly, if a feature appears to be a bug, you can disable it without rolling back or hot fix.

public class CartController : Controller{    [HttpPost]    public IActionResult AddProduct([FromServices]IOptionsSnapshot<CartFeaturesOptions> options, [FromBody] Product product)    {        var cart = _cartService.GetCart(this.User);        cart.Add(product);        if (options.Value.UseCartAdvisorFeature)        {            ViewBag.CartAdvice = _cartAdvisor.GiveAdvice(cart);        }        return View(cart);    }}
End

These lines of code allow us to add support for the consul configuration in an ASP. NET Core application. In fact, any application (even a classic. Net application that uses the Microsoft.Extensions.Configuration package) can benefit from it. This will be cool in a DevOps environment where you can centralize all configurations in one place and use the hot reload feature for real-time switching.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

Tags Index: