原文: USING CONSUL FOR STORING THE CONFIGURATION IN ASP.NET CORE
作者: Nathanael

[译者注:因急于分享给大家,所以本文翻译的很仓促,有些不准确的地方还望谅解]

来自 Hashicorp 公司的 Consul 是一个用于分布式架构的工具,可以用来做服务发现、运行健康检查和 kv 存储。本文详细介绍了如何使用 Consul 通过实现 ConfigurationProvider 在 ASP.Net Core 中存储配置。

为什么使用工具来存储配置?

通常,.Net 应用程序中的配置存储在配置文件中,例如 App.config、Web.config 或 appsettings.json。从 ASP.Net Core 开始,出现了一个新的可扩展配置框架,它允许将配置存储在配置文件之外,并从命令行、环境变量等等中检索它们。
配置文件的问题是它们很难管理。实际上,我们通常最终做法是使用配置文件和对应的转换文件,来覆盖每个环境。它们需要与 dll 一起部署,因此,更改配置意味着重新部署配置文件和 dll 。不太方便。
使用单独的工具集中化可以让我们做两件事:

  • 在所有机器上具有相同的配置

  • 能够在不重新部署任何内容的情况下更改值(对于功能启用关闭很有用)

Consul 介绍

本文的目的不是讨论 Consul,而是专注于如何将其与 ASP.Net Core 集成。
但是,简单介绍一下还是有必要的。Consul 有一个 Key/Value 存储功能,它是按层次组织的,可以创建文件夹来映射不同的应用程序、环境等等。这是一个将在本文中使用的层次结构的示例。每个节点都可以包含 JSON 值。

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

它提供了 REST API 以方便查询,key 包含在查询路径中。例如,获取 App1 在 Dev 环境中的配置的查询如下所示:GET http://:8500/v1/kv/App1/Dev/Settings
响应如下:

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

也可以以递归方式查询任何节点,GET http://:8500/v1/kv/App1/Dev?recurse 返回 :

HTTP/1.1 200 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": 75},{        "LockIndex": 0,        "Key": "App1/Dev/ConnectionStrings",        "Flags": 0,        "Value": "ewoiRGF0YWJhc2UiOiAiU2VydmVyPXRjcDpkYmRldi5kYXRhYmFzZS53aW5kb3dzLm5ldDtEYXRhYmFzZT1teURhdGFCYXNlO1VzZXIgSUQ9W0xvZ2luRm9yRGJdQFtzZXJ2ZXJOYW1lXTtQYXNzd29yZD1teVBhc3N3b3JkO1RydXN0ZWRfQ29ubmVjdGlvbj1GYWxzZTtFbmNyeXB0PVRydWU7IiwKIlN0b3JhZ2UiOiJEZWZhdWx0RW5kcG9pbnRzUHJvdG9jb2w9aHR0cHM7QWNjb3VudE5hbWU9ZGV2YWNjb3VudDtBY2NvdW50S2V5PW15S2V5OyIKfQ==",        "CreateIndex": 155,        "ModifyIndex": 155},{        "LockIndex": 0,        "Key": "App1/Dev/Settings",        "Flags": 0,        "Value": "ewogIkludCI6NDIsCiAiT2JqZWN0IjogewogICJTdHJpbmciOiAidG90byIsCiAgIkJsYSI6IG51bGwsCiAgIk9iamVjdCI6IHsKICAgIkRhdGUiOiAiMjAxOC0wMi0yM1QxNjoyMTowMFoiCiAgfQogfQp9Cgo=",        "CreateIndex": 501,        "ModifyIndex": 1071}
]

我们可以看到许多内容通过这个响应,首先我们可以看到每个 key 的 value 值都使用了 Base64 编码,以避免 value 值和 JSON 本身混淆,然后我们注意到属性“Index”在 JSON 和 HTTP 头中都有。 这些属性是一种时间戳,它们可以我们知道是否或何时创建或更新的 value。它们可以帮助我们知道是否需要重新加载这些配置了。

ASP.Net Core 配置系统

这个配置的基础结构依赖于 Microsoft.Extensions.Configuration.Abstractions NuGet包中的一些内容。首先,IConfigurationProvider 是用于提供配置值的接口,然后IConfigurationSource 用于提供已实现上述接口的 provider 的实例。
您可以在 ASP.Net GitHub 上查看一些实现。
与直接实现 IConfigurationProvider 相比,可以在 Microsoft.Extensions.Configuration 包中继承一个名为 ConfigurationProvider 的类,该类提供了一些样版代码(例如重载令牌的实现)。
这个类包含两个重要的东西:

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

Data 是包含所有键和值的字典,Load 是应用程序开始时使用的方法,正如其名称所示,它从某处(配置文件或我们的 consul 实例)加载配置并填充字典。

在 ASP.Net Core 中加载 consul 配置

我们第一个想到的方法就是利用 HttpClient 去获取 consul 中的配置。然后,由于配置在层级式的,像一棵树,我们需要把它展开,以便放入字典中,是不是很简单?

首先,实现 Load 方法,但是我们需要一个异步的方法,原始方法会阻塞,所以加入一个异步的 LoadAsync 方法

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

然后,我们将以递归的方式查询 consul 以获取配置值。它使用类中定义的一些对象,例如_consulUrls,这是一个数组用来保存 consul 实例们的 url(用于故障转移),_path 是键的前缀(例如App1/Dev)。一旦我们得到 json ,我们迭代每个键值对,解码 Base64 字符串,然后展平所有键和JSON对象。

private async Task<IDictionary<string, string>> ExecuteQueryAsync()
{    int consulUrlIndex = 0;    while (true){        try{            using (var httpClient = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }, true))            using (var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_consulUrls[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;}}
}

使键值变平的方法是对树进行简单的深度优先搜索。

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

包含构造方法和私有字段的完整的类代码如下:

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(_consulUrls[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, 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;}}}
}

动态重新加载配置

我们可以进一步使用 consul 的变更通知。它只是通过添加一个参数(最后一个索引配置的值)来工作的,HTTP 请求会一直阻塞,直到下一次配置变更(或 HttpClient 超时)。

与前面的类相比,我们只需添加一个方法 ListenToConfigurationChanges,以便在后台监听 consul 的阻塞 HTTP 。

public class ConsulConfigurationProvider : ConfigurationProvider{    private const string ConsulIndexHeader = "X-Consul-Index";    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<IDictionary<string, string>> ExecuteQueryAsync(bool isBlocking = false){        var requestUri = isBlocking ? $"?recurse=true&index={_consulConfigurationIndex}" : "?recurse=true";        using (var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_consulUrls[_consulUrlIndex], requestUri)))        using (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))            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;}}}
}

组合在一起

我们现在有了一个 ConfigurationProvider, 让我们再写一个 ConfigurationSource 来创建 我们的 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);}
}

以及一些扩展方法 :

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

现在可以在 Program.cs 中添加 Consul,使用其他的来源(例如环境变量或命令行参数)来向 consul 提供 url

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();

现在,可以使用 ASP.Net Core 的标准配置模式了,例如 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"));
}

要在我们的代码中使用它们,请注意如何使用 options ,对于可以动态重新加载的 options,使用 IOptions 将获得初始值。反之,ASP.Net Core 需要使用 IOptionsSnapshot。
这种情况对于功能切换非常棒,因为您可以通过更改 Consul 中的值来启用或禁用新功能,并且在不重新发布的情况下,用户就可以使用这些新功能。同样的,如果某个功能出现 bug,你可以禁用它,而无需回滚或热修复。

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

尾声

这几行代码允许我们在 ASP.Net Core 应用程序中添加对 consul 配置的支持。事实上,任何应用程序(甚至使用 Microsoft.Extensions.Configuration 包的经典 .Net 应用程序)都可以从中受益。在 DevOps 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。

原文链接:https://www.cnblogs.com/Rwing/p/consul-configuration-aspnet-core.html

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

如何在 ASP.Net Core 中使用 Consul 来存储配置相关推荐

  1. 如何在 ASP.NET Core 中使用 HttpClientFactory ?

    ASP.Net Core 是一个开源的,跨平台的,轻量级模块化框架,可用它来构建高性能的Web程序,这篇文章我们将会讨论如何在 ASP.Net Core 中使用 HttpClientFactory. ...

  2. 如何在 ASP.Net Core 中使用 Autofac

    依赖注入可以有效的实现对象之间的 松耦合 并能够实现代码的可测试和可维护性,ASP.Net Core 提供了一个极简版的容器实现对 依赖注入 的原生支持,然而内置的依赖注入容器相比成熟的 依赖注入容器 ...

  3. 如何在 ASP.Net Core 中使用 Lamar

    ASP.Net Core 自带了一个极简的 开箱即用 的依赖注入容器,实际上,你还可以使用第三方的 依赖注入容器 来替代它,依赖注入是一种设计模式,它能够有效的实现对象之间的解耦并有利于提高单元测试和 ...

  4. 如何在 ASP.Net Core 中使用 MediatR

    MediatR 是一个 中介者模式 的.NET开源实现, 中介者模式 管控了一组对象之间的相互通讯并有效的减少了对象之间错综复杂的相互依赖,在 中介者模式 中,一个对象不需要直接和另一个对象进行通讯, ...

  5. 如何在 ASP.Net Core 中对接 WCF

    在 REST API 出现之前,SOAP (Simple Object Access Protocol) 一直都是基于 web 的标准协议,虽然现在 REST 大行其道,但在平时开发中总会遇到对接第三 ...

  6. 如何在 ASP.Net Core 中使用 NCache

    虽然 ASP.Net Core 中缺少 Cache 对象,但它引入了三种不同的cache方式. 内存缓存 分布式缓存 Response缓存 Alachisoft 公司提供了一个开源项目 NCache, ...

  7. 如何在 ASP.Net Core 中使用 Configuration Provider

    ASP.NET Core 是一个开源的,跨平台的,精简的模块化框架,可用于构建高性能,可扩展的web应用程序, ASP.NET Core 中的数据配置常用 k-v 的形式存储,值得注意的是,新的数据配 ...

  8. 如何在 ASP.Net Core 中使用 Serilog

    记录日志的一个作用就是方便对应用程序进行跟踪和排错调查,在实际应用上都是引入 日志框架,但如果你的 日志文件 包含非结构化的数据,那么查询起来将是一个噩梦,所以需要在记录日志的时候采用结构化方式. 将 ...

  9. 如何在 ASP.NET Core 中使用 URL Rewriting 中间件

    URL rewriting 是根据预先配置好的一组规则去修改 request url,值得注意的是:URL Rewriting 的重写功能和 url 重定向 是两个概念,本篇我们就来讨论下如何在 AS ...

最新文章

  1. python用一行代码编写一个回声程序_一行python代码实现树结构
  2. arcgis坐标系学习总结
  3. linux userdel删除用户命令
  4. Kubernetes里的secret最基本的用法
  5. Golang学习笔记3——常量与运算符
  6. java 集合 总结 表_java 列表与集合总结
  7. wxpython 基本的控件 (文本)
  8. C#给图片加文字水印
  9. java 数据结构与算法_数据结构与算法—常用数据结构及其Java实现
  10. hdu 二分图最大匹配问题 (hdu 1083)
  11. 深入浅出Hadoop: 高效处理大数据
  12. Metasplotable3 简易安装教程
  13. 二、信号分解 —>经验模态分解(EMD)学习笔记
  14. 开源流媒体SRS结合硬件视频实时转码服务器的部署
  15. java验证码问题(不区分大小写)升级版,输入不正确则一直输入
  16. Unity3D正交-透视混合相机的实现
  17. python裁剪图像
  18. 向量的数量积,向量积,混合积及应用
  19. CVPR2020/2021行人检测重识别等论文,共33篇
  20. Java入门基本数据类型(羊驼)

热门文章

  1. 【我们一起写框架】C#的AOP框架
  2. 私有云搭建 OpenStack(centos7.3, centos-release-openstack-ocata)
  3. 书生云王东临:真正的超融合产品要像“机器猫” 开箱即用
  4. SDUT OJ -2892 A
  5. java获取ResultSet长度
  6. 承接数字油画图稿/线条图定制(出图)业务
  7. Asp-Net-Core开发笔记:在docker部署时遇到一个小坑
  8. MAUI安卓子系统调试方法(附安装教程)
  9. IoTSharp 2.0 发布
  10. Dapr牵手.NET学习笔记:绑定