最新版本号[免费下载]
  • [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置
    [翻译] 如何在 ASP.Net Core 中使用 Consul 来存储配置

    原文: 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": 0quot;{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, 

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;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 = 

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;{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, 

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;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 ? 

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;?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 = 

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;{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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    ,220)/}

    使用,配置,如何,翻译,存储
    2018-09-30

    119

  • ASP.NET Core 中的中间件
    ASP.NET Core 中的中间件

    前言  由于是第一次写博客quot;通过参数传递i值:{_i}");         logger.LogInformation("中间件开始");        await _next(context);         logger.LogInformation("中间件完成");     } }

    Startup.cs类的Configure方法里:

    //参数类型为: params object[] args
    app.UseMiddleware<RequestTestMiddleware>(1);

    具体实现方式同样在 Microsoft.AspNetCore.Http.Abstractions\Extensions\UseMiddlewareExtensions.cs 的 UseMiddleware 方法中

  • 高级用法 Map MapWhen

    1. Map

      app.Map("/map", _app =>
      {
          _app.Run(async context =>
          {        await context.Response.WriteAsync("Test Map!");
          });
      });

      当访问https://localhost:5001/map时将返回 Test Map!

      这里说一下,代码中并没有 MapController....

    2. MapWhen

      app.MapWhen(context => context.Request.Query.ContainsKey("branch"), _app =>
      {
          _app.Run(async context =>
          {        await context.Response.WriteAsync("Test Map!");
          });
      });

      看源代码会发现,MapWhen 的第二个参数(委托)并不是上面Use()next(),而是存在MapOptionsBranch属性中,也是RequestDelegate委托

    其他说明

    1. Middleware 的执行的有顺序的,在合适的 Middleware 返回请求可时管道更短,速度更快。
      比如 UseStaticFiles(),静态资源不必走验证、MVC 中间件,所以该方法在中间件的前面执行。

    2. 我们看到有很多内置的中间件的用法是*Use**,其实是加了个扩展:

      public static class RequestCultureMiddlewareExtensions{    public static IApplicationBuilder UseRequestCulture(        this IApplicationBuilder builder)
          {        return builder.UseMiddleware<RequestCultureMiddleware>();
          }
      }

    总结

      第一次写博客,最大的感触就是,然后就是思维逻辑有点混乱,总想用最简单的语言来表达,就是掌握不好。最后看起来还是太啰嗦了点。最后说明,以上很可能有错误的说法,希望大家以批判的角度来看,有任何问题可在留言区留言!Thanks!


    ,220)/}

    中间件,
2018-09-30

137

  • ASP.NET Core 入门教程 1、使用ASP.NET Core 构建第一个Web应用
    ASP.NET Core 入门教程 1、使用ASP.NET Core 构建第一个Web应用

    一、前言1、本文主要内容Visual Studio Code 开发环境配置使用 ASP.NET Core 构建Web应用ASP.NET Core Web 应用启动类说明ASP.NET Core Web 项目结构说明2、本教程环境信息软件/环境说明操作系统Windows 10SDK2.1.401ASP.NET Core2.1.3IDEVisual Studio Code 1.27浏览器Chrome 693、前置知识你可能需要的前置知识VS Code + .NET Core快速开始https://ken.io/serie/dotnet-core-quickstartC#语法学习http://www.runoob.com/csharp/csharp-tutorial.html二、环境安装与配置1、SDK 下载与安装下载下载地址:https://www.microsoft.com/net/download跨平台,根据自己的需求选择即可。这里我下载的是:SDK 2.1.401,你可以选择2.1.x的最新版本安装略,一直下一步即可,没什么需要特别注意的。如果你真想了解,可以参考:https://ken.io/note/dotnet-core-qucikstart-helloworld-windows2、VS Code下载&安装VS Code 下载下载地址:https://code.visualstudio.com/download反正VS Code跨平台,根据自己的需要选择就可以了,VS Code 安装略,一直下一步即可,没什么特别注意的。如果你用的macOS,直接拖动到应用程序目录即可,更简单快捷。3、VS Code配置基础扩展安装扩展说明C#包括语法高亮显示、智能感知、定义、查找所有引用等。调试支持。网络核心(CoreCLR)。Chinese (Simplified)简体中文补丁包快捷键(Ctrl+Shift+X)进入扩展管理页,直接搜索扩展名安装即可,或者点击左侧工具栏图标进入扩展管理页macOS版本快捷键是 Shift+Commnad+X三、VS Code 开发 ASP.NET Core Web项目1、项目创建通过命令行创建项目#创建项目目录 mkdir projects#进入项目目录cd projects#创建项目 dotnet new web -n helloweb2、VS Code打开项目菜单:文件->打开,选择项目目录打开项目项目打开后,VS Code会检测到缺少两个必须的Package:OmniSharp、.NET Core Debugger并且会自动帮你安装Downloading package 'OmniSharp for Windows (.NET 4.6 / x64)' (31017 KB).................... Done! Installing package 'OmniSharp for Windows (.NET 4.6 / x64)' Downloading package '.NET Core Debugger (Windows / x64)' (41984 KB).................... Done! Installing package '.NET Core Debugger (Windows / x64)' Finished安装完成后VS Code会提示:Required assets to build and debug are missing from ‘helloweb’. Add them?选择Yes即可。这时候,可以看一下左侧资源管理器,我们可以看到.vscode目录添加了两个配置文件:launch.json,tasks.json。项目的编译和调试配置文件就已经准备好了3、VS Code启动项目我们直接按下F5,或者菜单:调试->启动调试启动项目ASP.NET Core 默认绑定是5001端口,而且ASP.NET Core 2.1之后默认绑定了HTTPS,项目启动成功后,VS Code会帮我们打开默认浏览器并访问:https://localhost:5001因为我们并没有配置SSL证书,所以浏览器会发出警告⚠️,以Chrome为例:这时候,我们点击高级,救护出现继续访问的入口我们点击继续访问,就会出现Hello World!4、修改绑定协议HTTPS为HTTP接着我们可以修改配置去掉HTTPS协议绑定打开Properties/launchSettings.json文件{  "iisSettings": {    "windowsAuthentication": false

    使用,入门,应用,一个,构建
    2018-09-30

    270

  • ASP.NET Core 入门教程 2、使用ASP.NET Core MVC框架构建Web应用
    ASP.NET Core 入门教程 2、使用ASP.NET Core MVC框架构建Web应用

    一、前言1、本文主要内容使用dotnet cli创建基于解决方案(sln+csproj)的项目使用Visual Studio Code开发基于解决方案(sln+csproj)的项目Visual Studio Code Solution插件( vscode-solution-explorer)基础使用介绍基于 .NET Core web项目模板构建 ASP.NET Core MVC Web应用ASP.NET Core MVC框架上手2、本教程环境信息软件/环境说明操作系统Windows 10SDK2.1.401ASP.NET Core2.1.3IDEVisual Studio Code 1.27浏览器Chrome 693、前置知识你可能需要的前置知识MVC框架/模式介绍https://baike.baidu.com/item/mvc控制反转(IOC)原则与依赖注入(DI)ASP.NET Core 默认集成了DI。所有官方模块的引入都要使用DI的方式引入。https://baike.baidu.com/item/IOC二、项目准备1、项目创建.NET平台的项目构建有两个概念:解决方案(Solution)、项目(Project)。所有的项目开发,不论是Web项目,还是控制台应用程序,都必须基于Project来构建。而Solution的作用就是把Project组织起来如果项目简单,我们只需要基于Project来构建项目即可,但是当项目需要分层解耦时,我们如果在Project创建目录来隔离并不能起到硬性隔离的作用,毕竟只要在一个Project中就可以引用。而通过Project来分层就可以做到硬性隔离的效果。而且基于Project的代码复用更简洁合理(编译产出.dll可以在其他项目中引用等)解决方案(Solution)+ 项目(Project)就相当于用Maven构建的Java项目中,顶层Project和Project的关系。创建项目目录#创建项目目录 mkdir Ken.Tutorial#进入项目目录cd Ken.Tutorial创建解决方案文件dotnet new sln -n Ken.Tutorial创建Web项目dotnet new web -n Ken.Tutorial.Web将项目添加到解决方案中dotnet sln add Ken.Tutorial.Web2、VS Code 配置安装基于Solution开发 .NET Core 项目的扩展扩展名说明vscode-solution-explorer创建、删除、重命名或移动解决方案、解决方案文件夹和项目。管理项目引用。VS Code 扩展管理页直接搜索扩展名安装即可,本次安装的版本是:0.2.33三、VS Code开发基于解决方案的项目说明1、VS Code项目配置菜单:文件->打开文件夹,选择项目目录打开项目因为已经安装了VS Code的C#扩展和Solution扩展,所以也会提示缺失相关配置C#扩展提示:Required assets to build and debug are missing from ‘helloweb’. Add them?这是因为项目缺少编译、调试配置,选择Yes即可vscode-solution-explorer扩展提示:Would you like to create the vscode-solution-explorer templates folder?这是因为vscode-solution-explorer插件需要项目中的解决方案提供相应的模板。所有插件默认的配置文件,都会放在.vscode文件夹中资源管理器中除了默认的面板,我们安装的Solution插件还会提供友好的Solution Explorer。这个视图的风格,有VS(Visual Studio)的既视感。后续项目开发完全可以隐藏默认资源管理器,使用Solution Explorer就好。2、Solution Explorer菜单介绍Solution鼠标右键菜单介绍菜单快捷键说明Add existing project/添加已存在的项目(Project)Add new project/新建项目(Project)Create folderCtrl+Shift+F创建文件夹Open File/打开解决方案文件(.sln)RenameF2修改解决方案名称Build/编译解决方案(Solution)Clean/清理解决方案(Solution)的编译输出Pack/解决方案(Solution)打包Publish/发布解决方案(Solution)Restore/恢复解决方案(Solution)Test/执行解决方案(Solution)中的单元测试Project鼠标右键菜单介绍菜单快捷键说明Add package/添加packageAdd reference/引用解决方案中的其他项目Create fileCtrl+Shift+A创建文件Create folderCtrl+Shift+F创建文件夹Move/移动项目(Project)Remove project from solutionDel从解决方案中移除项目(Project)PasteCtrl+V粘贴Open File/打开项目文件(.csproj)RenameF2修改解决方案名称Build/编译项目(Project)Clean/清理项目(Project)的编译输出Pack/项目(Project)打包Publish/发布项目(Project)Restore/恢复项目(Project)Test/执行项目(Project)中的单元测试四、ASP.NET Core MVC 输出HelloWorld1、引入 ASP.NET Core MVC修改应用启动类(Startup.cs),引入MVC模块并配置默认路由public class Startup {    public void ConfigureServices(IServiceCollection services)     {        //引入MVC模块         services.AddMvc();     }    public void Configure(IApplicationBuilder app

    使用,入门,应用,构建,框架
    2018-09-30

    256

  • ASP.NET Core 入门教程 3、ASP.NET Core MVC路由入门
    ASP.NET Core 入门教程 3、ASP.NET Core MVC路由入门

    一、前言1、本文主要内容ASP.NET Core MVC路由工作原理概述ASP.NET Core MVC带路径参数的路由示例ASP.NET Core MVC固定前/后缀的路由示例ASP.NET Core MVC正则表达式匹配路由示例ASP.NET Core MVC路由约束与自定义路由约束ASP.NET Core MVC RouteAttribute绑定式路由使用介绍2、本教程环境信息软件/环境说明操作系统Windows 10SDK2.1.401ASP.NET Core2.1.3IDEVisual Studio Code 1.27浏览器Chrome 69本篇代码基于上一篇进行调整:https://github.com/ken-io/asp.net-core-tutorial/tree/master/chapter-023、前置知识你可能需要的前置知识MVC框架/模式介绍https://baike.baidu.com/item/mvc正则表达式http://www.runoob.com/regexp/regexp-tutorial.html二、ASP.NET Core MVC 路由简介1、ASP.NET Core MVC路由工作原理概述ASP.NET Core MVC路由的作用就是将应用接收到请求转发到对应的控制器去处理。应用启动的时候会将路由中间件(RouterMiddleware)加入到请求处理管道中,并将我们配置好的路由加载到路由集合(RouteCollection)中。当应用接收到请求时,会在路由管道(路由中间件)中执行路由匹配,并将请求交给对应的控制器去处理。另外,需要特别注意的是,路由的匹配顺序是按照我们定义的顺序从上之下匹配的,遵循是的先配置先生效的原则。2、路由配置参数说明参数名说明name路由名称,不可重复template路由模板,可在模板中以{name}格式定义路由参数defaults配置路由参数默认值constraints路由约束在路由配置中,MVC框架内置了两个参数,controllerquot;Welcome {name}(age:{age}) !"); }

    2、带路径参数的路由

    路由配置:

    routes.MapRoute(
            name: "TutorialPathValueRoute",
            template: "{controller}/{action}/{name}/{age}"
        );

    此路由适配URL:

    • /tutorial/welcome/ken/20

    不适配URL:

    • /tutorial/welcome/ken

    如果我们希望不在路径中设置age,也可以被路由到,那么可以将age指定为可选参数,将模板中的{age}修改为{age?}即可

    routes.MapRoute(
            name: "TutorialPathValueRoute",
            template: "{controller}/{action}/{name}/{age?}"
        );

    此路由适配URL:

    • /tutorial/welcome/ken/20

    • /tutorial/welcome/ken

    • /tutorial/welcome/ken?age=20

    3、固定前后缀的路由

    固定前缀路由配置:

    routes.MapRoute(
        name: "TutorialPrefixRoute",
        template: "jiaocheng/{action}",
        defaults: new { controller = "Tutorial" }
    );

    此路由适配URL:

    • /jiaocheng/index

    • /jiaocheng/welcome

    由于路径参数中不包含controller参数,所以需要在默认值中指定。

    固定后缀路由配置

    routes.MapRoute(
        name: "TutorialSuffixRoute",
        template: "{controller}/{action}.html"
    );

    此路由适配URL:

    • /tutorial/index.html

    • /tutorial/welcome.html

    • /home/index.html

    • /home/time.html

    固定后缀的路由适用于伪静态等诉求
    固定前后缀可以根据自己的需求结合起来使用。
    当然,你也可以在路由模板中间设定固定值。

    四、ASP.NET Core MVC 路由约束

    1、路由约束介绍

    路由约束主要是用于约束路由参数,在URL格式满足路有模板要求之后,进行参数检查。如果参数不满足路由约束,那么依然会返回未匹配该路由。最常用的可能就是参数类型校验、参数长度校验、以及通过正则满足的复杂校验。

    在开始之前需要在Startup.cs中引用相关命名空间

    using Microsoft.AspNetCore.Routing;using Microsoft.AspNetCore.Routing.Constraints;

    2、参数长度约束

    路由配置:约束name长度不能>5

    routes.MapRoute(
        name: "TutorialLengthRoute",
        template: "hello/{name}/{age?}",
        defaults: new { controller = "Tutorial", action = "Welcome", name = "ken" },
        constraints: new { name = new MaxLengthRouteConstraint(5) }
    );

    此路由适配

    • /hello

    • /hello/ken

    • /hello/ken/1000

    次路由不适配

    • /hello/kenaaaa

    我们也可以直接在模板中配置路由约束:

    routes.MapRoute(
        name: "TutorialLengthRoute2",
        template: "hello2/{name:maxlength(5)}/{age?}",
        defaults: new { controller = "Tutorial", action = "Welcome", name = "ken" }
    );

    3、参数范围约束

    路由配置:约束 1<=age<=150

    routes.MapRoute(
        name: "TutorialLengthRoute",    template: "hello/{name}/{age?}",
        defaults: new { controller = "Tutorial", action = "Welcome", name = "ken" },
        constraints: new {  age = new CompositeRouteConstraint(new IRouteConstraint[] { 
                                new IntRouteConstraint(), 
                                new MinRouteConstraint(1), 
                                new MaxRouteConstraint(150) }) }
    );

    此路由适配:

    • /hello/ken/1

    • /hello/ken/150

    此路由不适配

    • /hello/ken/1000

    我们也可以直接在模板中配置路由约束:

    routes.MapRoute(
        name: "TutorialLengthRoute2",
        template: "hello2/{name}/{age:range(1,150)?}",
        defaults: new { controller = "Tutorial", action = "Welcome", name = "ken" }
    );

    4、带有正则表达式约束的路由

    路由配置:

    routes.MapRoute(
        name: "TutorialRegexRoute",
        template: "welcome/{name}",
        defaults: new { controller = "Tutorial", Action = "Welcome" },
        constraints: new { name = @"k[a-z]*" }
    );

    此路由适配:

    • /welcome/k

    • /welcome/ken

    • /welcome/kevin

    此路由不适配

    • /welcome/k1

    • /welcome/keN

    • /welcome/tom

    这里我们用正则表达式约束了参数name,必须通过正则k[a-z]*匹配通过,即:以小写字母k开头,且后续可跟0到多个小写字母

    我们也可以直接在模板中配置路由约束:

    routes.MapRoute(
        name: "TutorialRegexRoute2",
        template: "welcome2/{name:regex(@"k[a-z]*")}",
        defaults: new { controller = "Tutorial", Action = "Welcome" }
    );

    5、自定义路由约束

    1、创建自定义约束

    在项目根目录创建目录Common,并在目录创建类:NameRouteConstraint.cs,然后实现接口:IRouteConstraint

    using System;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Routing;namespace Ken.Tutorial.Web.Common
    {    public class NameRouteConstraint : IRouteConstraint
        {        public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
            {            string name = values["name"]?.ToString();            if (name == null) return true;            if (name.Length > 5 && name.Contains(",")) return false;            return true;
            }
        }
    }

    这里我们约束当name长度>5时,name中不能包含,

    2、路由配置

    引入命名空间

    using Ken.Tutorial.Web.Common;

    在ConfigureServices引入路由约束

    public void ConfigureServices(IServiceCollection services)
        {        //引入MVC模块
            services.AddMvc();        //引入自定义路由约束
            services.Configure<RouteOptions>(options =>
            {
                options.ConstraintMap.Add("name", typeof(NameRouteConstraint));
            });
        }

    配置路由

    routes.MapRoute(
        name: "TutorialDiyConstraintRoute",
        template: "diy/{name}",
        defaults: new { controller = "Tutorial", action = "Welcome" },
        constraints: new { name = new NameRouteConstraint() }
    );

    此路由适配:

    • /diy/ken

    • /diy/ken,

    • /diy/kenny

    此路由不适配

    • /diy/kenny,

    当然,按照惯例,依然可以在模板中配置路由约束

    routes.MapRoute(
        name: "TutorialDiyConstraintRoute2",
        template: "diy2/{name:name}",
        defaults: new { controller = "Tutorial", action = "Welcome" }
    );

    五、ASP.NET Core MVC 绑定式路由配置

    1、路由配置风格

    • 集中式配置

    前面章节提到的路由配置都是在Startup类中进行的集中式路由配置,集中配置的路由,除了template中没有配置{controller}参数,默认都是对所有控制器(Controller)生效的。这种集中配置的方式一般我们只要配置一个默认路由,其他情况我们只需要不满足默认模板的情况下进行配置即可。尤其是对URL没有友好度要求的应用,例如:后台管理系统

    • 分散式配置/绑定式配置

    对于集中式路由配置的方式,如果某个Controller/Action配置了特殊路由,对于代码阅读就会不太友好。不过没关系,ASP.NET Core MVC也提供了RouteAttribute可以让我们在Controller或者Action上直接指定路由模板。

    不过要强调的是,一个控制器只能选择其中一种路由配置,如果控制器标记了RouteAttribute进行路由配置,那么集中式配置的路由将不对其生效。

    2、绑定式路由配置

    在项目Controllers目中新建TestController.cs继承与Controller
    并配置Action与路由

    using System;using Microsoft.AspNetCore.Mvc;namespace Ken.Tutorial.Web.Controllers
    {
        [Route("/test")]    public class TestController : Controller
        {
            [Route("")]
            [Route("/test/home")]        public IActionResult Index()
            {            return Content("ASP.NET Core RouteAttribute test by ken from ken.io");
            }
    
            [Route("servertime")]
            [Route("/t/t")]        public IActionResult Time(){            return Content(

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;ServerTime:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} - ken.io");         }     } }
    配置项说明
    [Route(“/test”)]表示该Controller访问路由前缀为/test,必须以/开头
    [Route(“”)]表示以Controller的路由配置为前缀访问该Action;可以通过/test路由到该Action
    [Route(“/test/home”)]表示忽略Controller的路由配置;可以通过/test/home路由到该Action
    [Route(“servertime”)]表示以Controller的路由配置为前缀访问该Action;可以通过/test/servertime路由到该Action
    [Route(“/t/t”)]表示忽略Controller的路由配置;可以通过/t/t路由到该Action

    RouteAttribute中配置的参数,就相当于我们集中式配置中的路由模板(template),最终框架还是帮我们初始化成路由规则,以[Route(“/test/home”)]为例,相当于生成了以下路由配置:

    routes.MapRoute(
        name: "Default",
        template: "test/home",
        defaults: new { controller = "Test", action = "Index" }
    );

    当然,我们也可以在[Route]配置中使用模板参数,而且依然可以在模板中使用约束,自定义约束也没问题。

    [Route("welcome/{name:name}")]public IActionResult Welcome(string name){    return Content(

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;Welcome {name} !"); }

    最大的区别就是不能定义默认值了,可能也不需要了,你说是吧。^_^

    六、备注

    1、附录

    • 本文代码示例

    https://github.com/ken-io/asp.net-core-tutorial/tree/master/chapter-03

    • 本文参考

    https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/routing?view=aspnetcore-2.1


    本文首发于我的独立博客:https://ken.io/note/asp.net-core-tutorial-mvc-route



    ,220)/}

    入门,路由,教程,
    2018-09-30

    152

  • .NET Core部署中你不了解的框架依赖与独立部署
    .NET Core部署中你不了解的框架依赖与独立部署

    作者:依乐祝 原文地址:https://www.cnblogs.com/yilezhu/p/9703460.html NET Core项目发布的时候你有没有注意到这两个选项呢?有没有纠结过框架依赖与独立部署到底有什么区别呢?如果有的话那么这篇文章可以参考下! 为什么要写这篇文章呢?因为今天同事问我框架依赖与独立部署到底应该选哪个呢?有什么区别。印象中只知道框架依赖发布后文件比独立部署要小很多,然后就是独立部署不占用net core的共享资源,而框架依赖需要与其他net core程序共享net core的一些资源。感觉很模糊,所以查了下资料整理如下,希望对大家有所帮助。依赖框架的部署 (FDD)定义框架依赖的部署:顾名思义,依赖框架的部署 (FDD) 依赖目标系统上存在共享系统级版本的 .NET Core。 由于已存在 .NET Core,因此应用在 .NET Core 安装程序间也是可移植的。 应用仅包含其自己的代码和任何位于 .NET Core 库外的第三方依赖项。 FDD 包含可通过在命令行中使用 dotnet 实用程序启动的 .dll 文件。 例如,dotnet app.dll 就可以运行一个名为 app 的应用程序。 对于 FDD,仅部署应用程序和第三方依赖项。 不需要部署 .NET Core,因为应用将使用目标系统上存在的 .NET Core 版本。 这是定目标到 .NET Core 的 .NET Core 和 ASP.NET Core 应用程序的默认部署模型。优点不需要提前定义 .NET Core 应用将在其上运行的目标操作系统。 因为无论什么操作系统,.NET Core 的可执行文件和库都是用通用的 PE 文件格式,因此,无论什么基础操作系统,.NET Core 都可执行应用。部署包很小。 只需部署应用及其依赖项,而无需部署 .NET Core 本身。许多应用都可使用相同的 .NET Core 安装,从而降低了主机系统上磁盘空间和内存使用量。缺点仅当主机系统上已安装你设为目标的 .NET Core 版本或更高版本时,应用才能运行。如果不了解将来版本,.NET Core 运行时和库可能发生更改。 在极少数情况下,这可能会更改应用的行为。独立部署 (SCD)定义独立部署:与 FDD 不同,独立部署 (SCD) 不依赖目标系统上存在的共享组件。 所有组件(包括 .NET Core 库和 .NET Core 运行时)都包含在应用程序中,并且独立于其他 .NET Core 应用程序。 SCD 包括一个可执行文件(如 Windows 平台上名为 app 的应用程序的 app.exe),它是特定于平台的 .NET Core 主机的重命名版本,还包括一个 .dll 文件(如 app.dll),而它是实际的应用程序。 对于独立部署,可以部署应用和所需的第三方依赖项以及生成应用所使用的 .NET Core 版本。 创建 SCD 不包括各种平台上的 .NET Core 本机依赖项,因此运行应用前这些依赖项必须已存在。 从 NET Core 2.1 SDK(版本 2.1.300)开始,.NET Core 支持修补程序版本前滚。 在创建独立部署时,.NET Core 工具会自动包含你的应用程序所指向的 .NET Core 版本的最新服务的运行时。 (最新服务的运行时包括安全修补程序和其他 bug 修复程序。)服务的运行时不需要存在于你的生成系统上;它会从 NuGet.org 自动下载。FDD 和 SCD 部署使用单独的主机可执行文件,使你可以使用发布者签名为 SCD 签署主机可执行文件。优点可以对与应用一起部署的 .NET Core 版本具有单独的控制权请放心,目标系统可以运行你的 .NET Core 应用,因为你提供的是应用将在其上运行的 .NET Core 版本缺点由于 .NET Core 包含在部署包中,因此必须提前选择为其生成部署包的目标平台部署包相对较大,因为需要将 .NET Core 和应用及其第三方依赖项包括在内。从.NET Core 2.0 开始,可以通过使用 .NET Core 全球化固定模式在 Linux 系统上减少大约 28 MB 的部署大小。 通常,Linux 上的 .NET Core 依赖于 ICU 库来实现全球化支持。 在固定模式下,库不包含在部署中,并且所有区域性的行为均类似于固定区域性。向系统部署大量独立的 .NET Core 应用可能会使用大量磁盘空间,因为每个应用都会复制 .NET Core 文件实例演示 .NET Core 应用的部署发布上面已经说了,可以将 .NET Core 应用程序部署为依赖框架的部署或独立部署,前者包含应用程序二进制文件,但依赖目标系统上存在的 .NET Core,而后者同时包含应用程序和 .NET Core 二进制文件。不包含第三方依赖的框架依赖的部署为项目创建一个目录,并将其设为当前目录在命令行中,键入 dotnet new console 以创建新的 C# 控制台项目在编辑器中打开 Program.cs 文件,然后使用下列代码替换自动生成的代码。 它会提示用户输入文本,并显示用户输入的个别词。 它使用正则表达式 \w+ 来将输入文本中的词分开。using System;using System.Text.RegularExpressions;namespace Applications.ConsoleApps{    public class ConsoleParser     {        public static void Main()        {             Console.WriteLine("Enter any textquot;\nThere are {matches.Count} words in your string:");                for (int ctr = 0; ctr < matches.Count; ctr++)                 {                     Console.WriteLine(

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;   #{ctr,2}: '{matches[ctr].Value}' at position {matches[ctr].Index}");                 }             }         }     } }
  • 运行 dotnet restore(请参阅注释)命令,还原项目中指定的依赖项。

  • 使用 dotnet build命令生成应用程序,或使用 dotnet run命令生成并运行应用程序。

  • 完成程序调试和测试后,使用下列命令创建部署

    dotnet publish -f netcoreapp2.1 -c Release

    这将创建一个应用的发行版(而不是调试版)。 生成的文件位于名为“publish”的目录中,该目录位于项目的 bin 目录的子目录中。

    与应用程序的文件一起,发布过程将发出包含应用调试信息的程序数据库 (.pdb) 文件。 该文件主要用于调试异常。 可以选择不将其与应用程序的文件一起分布。 但是,如果要调试应用的发布版本,则应保存该文件。

    可以采用任何喜欢的方式部署完整的应用程序文件集。 例如,可以使用简单的 copy 命令将其打包为 Zip 文件,或者使用选择的安装包进行部署。

  • 安装成功后,用户可通过使用 dotnet 命令或提供应用程序文件名(如 dotnet fdd.dll)来执行应用程序。
    除应用程序二进制文件外,安装程序还应捆绑共享框架安装程序,或在安装应用程序的过程中将其作为先决条件进行检查。 安装共享框架需要管理员/根访问权限。

  • 包含第三方依赖项的依赖框架的部署

    要使用一个或多个第三方依赖项来部署依赖框架的部署,需要这些依赖项都可供项目使用。 在运行 dotnet restore命令之前,还需执行额外两个步骤:

    1. 向 csproj 文件的部分添加对所需第三方库的引用。 以下部分包含 Json.NET 的依赖项(作为第三方库):

    <ItemGroup>
      <PackageReference Include="Newtonsoft.Json" Version="10.0.2" /></ItemGroup>
    1. 如果尚未安装,请下载包含第三方依赖项的 NuGet 包。 若要下载该包,请在添加依赖项后执行 dotnet restore命令。 因为依赖项在发布时已从本地 NuGet 缓存解析出来,因此它一定适用于你的系统。

      请注意,如果依赖框架的部署具有第三方依赖项,则其可移植性只与第三方依赖项相同。 例如,如果某个第三方库只支持 macOS,该应用将无法移植到 Windows 系统。 当第三方依赖项本身取决于本机代码时,也可能发生此情况。 Kestrel 服务器就是一个很好的示例,它需要 libuv 的本机依赖项。 当为具有此类第三方依赖项的应用程序创建 FDD 时,已发布的输出会针对每个本机依赖项支持(存在于 NuGet 包中)的运行时标识符 (RID) 包含一个文件夹。

    不包含第三方依赖项的独立部署

    部署没有第三方依赖项的独立部署包括创建项目、修改 csproj 文件、生成、测试以及发布应用。 一个用 C# 编写的简单示例可说明此过程。 该示例演示如何使用命令行中的 dotnet 实用工具创建独立部署。

    1. 为项目创建一个目录,并将其设为当前目录。

    2. 在命令栏行中,键入 dotnet new console,在该目录中创建新的 C# 控制台项目

    3. 在编辑器中打开 Program.cs 文件,然后使用下列代码替换自动生成的代码。 它会提示用户输入文本,并显示用户输入的个别词。 它使用正则表达式 \w+ 来将输入文本中的词分开。

    using System;using System.Text.RegularExpressions;namespace Applications.ConsoleApps{    public class ConsoleParser
        {        public static void Main()        {
                Console.WriteLine("Enter any text, followed by <Enter>:\n");
                String s = Console.ReadLine();
                ShowWords(s);
                Console.Write("\nPress any key to continue... ");
                Console.ReadKey();
            }        private static void ShowWords(String s)        {
                String pattern = @"\w+";            var matches = Regex.Matches(s, pattern);            if (matches.Count == 0)
                {
                    Console.WriteLine("\nNo words were identified in your input.");
                }            else
                {
                    Console.WriteLine(

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;\nThere are {matches.Count} words in your string:");                for (int ctr = 0; ctr < matches.Count; ctr++)                 {                     Console.WriteLine(

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;   #{ctr,2}: '{matches[ctr].Value}' at position {matches[ctr].Index}");                 }             }         }     } }
    1. 在 csproj 文件(该文件用于定义应用的目标平台)的部分中创建标记,然后指定每个目标平台的运行时标识符 (RID)。 请注意,还需要添加分号来分隔 RID。 请查看运行时标识符目录,获取运行时标识符列表。
      例如,以下部分表明应用在 64 位 Windows 10 操作系统和 64 位 OS X 10.11 版本的操作系统上运行。

    <PropertyGroup>
        <RuntimeIdentifiers>win10-x64;osx.10.11-x64</RuntimeIdentifiers></PropertyGroup>

    请注意,元素可能出现在 csproj 文件的任何中。 本节后面部分将显示完整的示例 csproj 文件。

    1. 运行 dotnet restore命令,还原项目中指定的依赖项。

    2. 运行 dotnet restore(请参阅注释)命令,还原项目中指定的依赖项。特别是如果应用面向 Linux,则可以通过利用全球化固定模式来减小部署的总规模。 全球化固定模式适用于不具有全局意识且可以使用固定区域性的格式约定、大小写约定以及字符串比较和排序顺序的应用程序。要启用固定模式,右键单击“解决方案资源管理器”中的项目(不是解决方案),然后选择“编辑 SCD.csproj”。 然后将以下突出显示的行添加到文件中:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp2.1</TargetFramework>
      </PropertyGroup>
      <ItemGroup>
        <RuntimeHostConfigurationOption Include="System.Globalization.Invariant" Value="true" />
      </ItemGroup> </Project>
    1. 在命令行中,使用 dotnet run 生成命令。

    2. 调试并测试程序后,为应用的每个目标平台创建要与应用一起部署的文件。
      同时对两个目标平台使用 dotnet publish 命令,如下所示:

    dotnet publish -c Release -r win10-x64dotnet publish -c Release -r osx.10.11-x64

    这将为每个目标平台创建一个应用的发行版(而不是调试版)。 生成的文件位于名为“发布”的子目录中,该子目录位于项目的 .\bin\Release\netcoreapp2.1子目录的子目录中。 请注意,每个子目录中都包含完整的启动应用所需的文件集(既有应用文件,也有所有 .NET Core 文件)。

    与应用程序的文件一样,发布过程将生成包含应用调试信息的程序数据库 (.pdb) 文件。 该文件主要用于调试异常。 可以选择不使用应用程序文件打包该文件。 但是,如果要调试应用的发布版本,则应保存该文件。
    可按照任何喜欢的方式部署已发布的文件。 例如,可以使用简单的 copy 命令将其打包为 Zip 文件,或者使用选择的安装包进行部署。
    下面是此项目完整的 csproj 文件。

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp2.1</TargetFramework>
        <RuntimeIdentifiers>win10-x64;osx.10.11-x64</RuntimeIdentifiers>
      </PropertyGroup></Project>

    包含第三方依赖项的独立部署

    部署包含一个或多个第三方依赖项的独立部署包括添加依赖项。 在运行 dotnet restore命令之前,还需执行额外两个步骤:

    1. 将对任何第三方库的引用添加到 csproj 文件的部分。 以下部分使用 Json.NET 作为第三方库。

    <ItemGroup>
        <PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
      </ItemGroup>
    1. 如果尚未安装,请将包含第三方依赖项的 NuGet 包下载到系统。 若要使依赖项对应用适用,请在添加依赖项后执行 dotnet restore命令。 因为依赖项在发布时已从本地 NuGet 缓存解析出来,因此它一定适用于你的系统。
      下面是此项目的完整 csproj 文件:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp2.1</TargetFramework>
        <RuntimeIdentifiers>win10-x64;osx.10.11-x64</RuntimeIdentifiers>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
      </ItemGroup></Project>

    部署应用程序时,应用中使用的任何第三方依赖项也包含在应用程序文件中。 运行应用的系统上不需要第三方库。
    请注意,可以只将具有一个第三方库的独立部署部署到该库支持的平台。 这与依赖框架的部署中具有本机依赖项和第三方依赖项相似,其中的本机依赖项必须与部署应用的平台兼容。

    备注:
    从 .NET Core 2.0 开始,无需运行 dotnet restore,因为它由所有需要还原的命令隐式运行,如 dotnet newdotnet builddotnet run

    总结

    本文首先介绍了框架依赖与独立部署的概念,然后分别介绍了框架依赖与独立部署的优缺点让大家加深理解!最后通过一个实例来讲述了如何进行框架依赖与独立部署。采用的实例使用的是控制台的方式进行的,当然你也可以使用vs进行发布。

    作者:依乐祝

    出处:https://www.cnblogs.com/yilezhu/p/9703460.html

    本站使用「署名 4.0 国际」创作共享协议,转载请在文章明显位置注明作者及出处。


    ,220)/}

    依赖,独立,部署,框架,了解
    2018-09-30

    163

  • ASP.NET Core 中的配置
    ASP.NET Core 中的配置

    前言配置在我们开发过程中必不可少,ASP.NET中的配置在 Web.config 中。也可配置在如:JSON、XML、数据库等(但ASP.NET并没提供相应的模块和方法)。在ASP.NET Core中Web.config已经不存在了(但如果托管到 IIS 的时候可以使用 web.config 配置 IIS),而是用appsettings.json和appsettings.(Development、Staging、Production).json配置文件(可以理解为ASP.NET中的Web.config和Web.Release.config的关系)。下面我们一起看下ASP.NET Core 中的配置基础用法HomeController.cs:[ApiController]public class HomeController : ControllerBase{    private readonly IConfiguration _configuration;    public HomeController(IConfiguration configuration)    {         _configuration = configuration;     }     [HttpGet("/")]    public dynamic Index()    {        return JsonConvert.SerializeObject(new         {             ConnectionString = _configuration["ConnectionString"]quot;配置信息已更改:{info}");          });         return JsonConvert.SerializeObject(new          {              OptionsMoitor = _optionsMonitor          });      }

    当我们修改配置文件后会看到日志信息:

    info: WebApiSample.Controllers.HomeController[0]
    配置信息已更改:{"Options":{"ConnectionString":"data source=.;initial catalog=TEST;user id=sa","Parent":{"Child1":"child","Child2":{"GrandChildren":"grandchildren"}}},"Name":"TestOptions"}
    info: WebApiSample.Controllers.HomeController[0]
    配置信息已更改:{"Options":{"ConnectionString":"data source=.;initial catalog=TEST;user id=sa","Parent":{"Child1":"child","Child2":{"GrandChildren":"grandchildren"}}},"Name":"TestOptions"}
    info: WebApiSample.Controllers.HomeController[0]
    配置信息已更改:{"Options":{"ConnectionString":"data source=.;initial catalog=TEST;user id=sa","Parent":{"Child1":"child","Child2":{"GrandChildren":"grandchildren"}}},"Name":"TestOptions"}
    info: WebApiSample.Controllers.HomeController[0]
    配置信息已更改:{"Options":{"ConnectionString":"data source=.;initial catalog=TEST;user id=sa","Parent":{"Child1":"child","Child2":{"GrandChildren":"grandchildren"}}},"Name":"TestOptions"}

    不知道为什么会有这么多日志...

    还有一点不管我们修改哪个日志文件,只要是执行了AddJsonFile的文件,都会触发该事件

  • 写在最后

    写的有点乱希望不要见怪,本以为一个配置模块应该不会复杂,但看了源代码后发现里面的东西好多...本来还想写下是如何实现的,但感觉太长了就算了。
    
    最后还是希望大家以批判的角度来看,有任何问题可在留言区留言!


    ,220)/}

    配置,
    2018-09-30

    136

  • ASP.NET Core WebListener 服务器
    ASP.NET Core WebListener 服务器

    原文地址:WebListener server for ASP.NET CoreBy Tom Dykstra

    服务器,
    2018-09-30

    139

  • ASP.NET Core服务器综述
    ASP.NET Core服务器综述

    原文地址:Servers overview for ASP.NET CoreBy Tom Dykstra

    服务器,综述,
    2018-09-30

    123

  • ASP.NET Core的Kestrel服务器
    ASP.NET Core的Kestrel服务器

    原文地址----Kestrel server for ASP.NET CoreBy Tom Dykstraquot;<p>Request URL: {context.Request.GetDisplayUrl()}<p>");     }); }

    SSL的URL前缀

    如果你调用UseSSL扩展方法,请确保在https:中包含URL前缀,如下所示:

    var host = new WebHostBuilder() 
        .UseKestrel(options => 
        { 
            options.UseHttps("testCert.pfx", "testPassword"); 
        }) 
       .UseUrls("http://localhost:5000", "https://localhost:5001") 
       .UseContentRoot(Directory.GetCurrentDirectory()) 
       .UseStartup<Startup>() 
       .Build();

    Note

    HTTPS和HTTP不能在同一端口上被托管。

    下一步

    更多的信息,请参考以下资源:

    本教程在本地仅使用Kestrel,在将该应用部署到Azure之后,它将在Windows上使用IIS作为反向代理服务器。


    ,220)/}

    服务器,
    2018-09-30

    209

  • ASP.NET Core模块概述
    ASP.NET Core模块概述

    原文地址:ASP.NET Core Module overviewBy Tom Dykstra

    模块,概述,
    2018-09-30

    158

  • ASP.NET Core File Providers
    ASP.NET Core File Providers

    原文地址:FileProviderBy Steve SmithASP.NET Core通过对File Providers的使用实现了对文件系统访问的抽象。查看或下载示例代码File Provider 抽象File Providers是文件系统之上的一层抽象。它的主要接口是IFileProvider。IFileProvider公开了相应方法用来获取文件信息(IFileInfo), 目录信息(IDirectoryContents),以及设置更改通知(通过使用一个IChangeToken)。IFileInfo接口提供了操作单个文件和目录的方法和属性。它有两个boolean属性,Exists和IsDirectory,以及两个描述文件的两个属性Name和Length(按字节),还包括一个LastModified日期属性。你还可以通过CreateReadStream方法读取文件内容。File Provider 实现有三种对于IFileProvider的实现可供选择:物理式,嵌入式和复合式。物理式用于访问实际系统中的文件。嵌入式用于访问嵌入在程序集中的文件。 复合式则是对前两种方式的组合使用。PhysicalFileProviderPhysicalFileProvider提供了对物理文件系统的访问。它封装了System.IO.File类型,范围限定到一个目录及其子目录的所有路径。这类作用域会限制访问某个目录及其子目录,防止作用域以外的其他操作访问文件系统。当实例化此类provider时,你必须为它提供一个目录路径,以供服务器拿来当做由这个provider发出的所有请求的基础路径(这个provider会限制路径以外的访问请求)。在一个ASP.NET Core应用,你可以直接实例化出一个PhysicalFileProvider provider,或者你也可以通过在控制器和服务中使用构造函数依赖注入的方式,请求一个IFileProvider接口。后者生成的解决方案通常更灵活以及更便于测试。要创建一个PhysicalFileProvider其实很简单,只需要对其实化,再传递给它一个物理路径。之后你就可以通过它的目录遍历内容或提供子路径获取特定文件的信息。IFileProvider provider = new PhysicalFileProvider(applicationRoot); IDirectoryContents contents = provider.GetDirectoryContents(""); // the applicationRoot contentsIFileInfo fileInfo = provider.GetFileInfo("wwwroot/js/site.js"); // a file under applicationRoot为了在控制器中请求一个provider,需要在控制器的构造函数中指定类型参数并赋值给本地属性。之后你就可以在你的动作器方法中使用本地实例了。public class HomeController : Controller{    private readonly IFileProvider _fileProvider;    public HomeController(IFileProvider fileProvider)    {         _fileProvider = fileProvider;     }    public IActionResult Index()    {        var contents = _fileProvider.GetDirectoryContents("");        return View(contents);     } }在应用的Startup类中创建provider的代码如下:using System.Linq;using System.Reflection;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.FileProviders;using Microsoft.Extensions.Logging;namespace FileProviderSample{    public class Startup     {        private IHostingEnvironment _hostingEnvironment;        public Startup(IHostingEnvironment env)        {            var builder = new ConfigurationBuilder()                 .SetBasePath(env.ContentRootPath)                 .AddJsonFile("appsettings.json"quot;appsettings.{env.EnvironmentName}.json", optional: true)                 .AddEnvironmentVariables();             Configuration = builder.Build();             _hostingEnvironment = env;         }        public IConfigurationRoot Configuration { get; }        // This method gets called by the runtime. Use this method to add services to the container.         public void ConfigureServices(IServiceCollection services)        {            // Add framework services.             services.AddMvc();            var physicalProvider = _hostingEnvironment.ContentRootFileProvider;            var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());            var compositeProvider = new CompositeFileProvider(physicalProvider, embeddedProvider);            // choose one provider to use for the app and register it             //services.AddSingleton<IFileProvider>(physicalProvider);             //services.AddSingleton<IFileProvider>(embeddedProvider);             services.AddSingleton<IFileProvider>(compositeProvider);         }     } }

    Index.chhtml 视图中,可以遍历操作IDirectoryContents模型参数

    @using Microsoft.Extensions.FileProviders
    @model  IDirectoryContents<h2>Folder Contents</h2><ul>
        @foreach (IFileInfo item in Model)
        {
            if (item.IsDirectory)
            {            <li><strong>@item.Name</strong></li>
            }
            else
            {            <li>@item.Name - @item.Length bytes</li>
            }
        }</ul>

    结果如下:

    EmbeddedFileProvider

    EmbeddedFileProvider用于访问嵌入到程序集中的文件。在.NET Core中,你可以通过修改 project.json 文件的buildOptions属性参数来把文件嵌入到程序集中。

    "buildOptions": {  "emitEntryPoint": true,  "preserveCompilationContext": true,  "embed": [    "Resource.txt",    "**/*.js"
      ]
    }

    当你把文件嵌入到程序集中时,你可以使用通配符模式。这些模式可以被用来匹配一个或多个文件。

    Note
    把项目中所有的.js文件都嵌入到项目程序集里的情况是不太可能发生的,以上示例仅作为demo给出。

    当创建一个EmbeddedFileProvider时,请在其构造函数中传入一个程序集实例供其读取。

    var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());

    以上的代码片段描述了如何创建一个能访问当前工作程序集的EmbeddedFileProvider类型变量。

    使用EmbeddedFileProvider更新示例项目代码后的输出结果如下:

    Note
    如上图所示,嵌入式资源不会公开目录。相反的,资源路径(经由资源的命名空间)会被嵌入到它的文件名中并以.作为分隔符。


    Tip
    EmbeddedFileProvider构造器接受一个可选的baseNamespace参数,指定此参数将限定GetDirectoryContents方法调用该命名空间下的资源。

    CompositeFileProvider

    CompositeFileProvider联合IFileProvider实例公开了一个单一的接口,用以和来自多种provider的文件工作。当创建一个CompositeFileProvider时,你可以为它的构造函数传入一个或多个IFileProvider实例。

    var physicalProvider = _hostingEnvironment.ContentRootFileProvider;var embeddedProvider = new EmbeddedFileProvider(Assembly.GetEntryAssembly());var compositeProvider = new CompositeFileProvider(physicalProvider, embeddedProvider);

    使用包含物理式provider(在前)和嵌入式provider的CompositeFileProvider更新示例项目代码后的输出结果如下:

    查看更改

    IFileProviderWatch方法能用来查看一个或多个文件/目录的更改信息。Watch方法接受一个路径字符串,它也可以使用通配符模式来指定多个文件,Watch方法最终返回一个IChangeToken。这个token公开了一个HasChanged属性用以检视状态,公开了一个RegisterChangeCallback方法,此方法会在指定的路径字符串检测到更改时被调用。请注意每个更改token只调用其关联回调以响应单次更改。为了使监控持续,你可以使用如下所示的TaskCompletionSource方法,或者重建IChangeToken以响应更改。

    在这个文章的示例中,无论何时当文本文件内容发生修改,按如下代码配置的console应用都会显示相应的信息。

    using System;using System.IO;using System.Threading.Tasks;using Microsoft.Extensions.FileProviders;using Microsoft.Extensions.Primitives;namespace WatchConsole{    public class Program
        {        private static PhysicalFileProvider _fileProvider = 
                new PhysicalFileProvider(Directory.GetCurrentDirectory());        public static void Main(string[] args)        {
                Console.WriteLine("Monitoring quotes.txt for changes (ctrl-c to quit)...");            while (true)
                {
                    MainAsync().GetAwaiter().GetResult();
                }
            }        private static async Task MainAsync()        {
                IChangeToken token = _fileProvider.Watch("quotes.txt");            var tcs = new TaskCompletionSource<object>();
                token.RegisterChangeCallback(state => 
                    ((TaskCompletionSource<object>)state).TrySetResult(null), tcs);            await tcs.Task.ConfigureAwait(false);
                Console.WriteLine("quotes.txt changed");
            }
        }
    }

    以下是执行过几次文本保存动作后的运行结果截图:

    Note
    有一些文件系统,例如Docker容器和网络共享,可能不能很可靠地发送更改通知。设置环境变量DOTNET_USE_POLLINGFILEWATCHER的值为1true,使得每四秒轮询一次文件系统的变更。

    通配符模式

    文件系统路径规则使用叫作globbing patterns的通配符模式,这类简单模式可以被用来指定文件组。这两个通配符分别是***

    *
    *表示在当前文件夹级别上匹配任何文件名称或文件扩展名。匹配以文件路径字符串中的/.符号结尾。

    **
    **表示在多个目录级别上匹配任何文件名称或文件扩展名。可用于在一个目录层次结构中递归地匹配多个文件。

    通配符模式示例

    directory/file.txt

    在指定的文件夹中匹配指定的文件。

    directory/*.txt

    在指定的文件夹中匹配所有以.txt扩展名结尾的文件。

    directory/*/project.json

    在指定的directory文件夹下的一级目录位置中匹配所有符合project.json名称的文件

    directory/**/*.txt

    在指定的directory文件夹下的所有位置中匹配所有以.txt扩展名结尾的文件。

    在ASP.NET Core中File Provider的用法

    ASP.NET Core有几个组件使用file provider功能。IHostingEnvironmentIFileProvider接口类型公开了应用的目录根和Web根。静态文件中间件使用file provider来定位静态文件。Razor更是大量使用IFileProvider来定位视图。Dotnet的发布功能使用file provider和通配符模式来指定需要跟随发布的文件。

    在应用程序中使用的建议

    如果你的ASP.NET Core应用需要访问文件系统,你可以通过依赖注入创建IFileProvider接口实例,然后再通过前文所示的相应方法执行访问。当应用启动的时候,这些方法允许你一次性配置provider并减少应用初始化时生成的实例类型数目。


    ,220)/}

    2018-09-30

    178

  • ASP.NET Core依赖注入解读&使用Autofac替代实现
    ASP.NET Core依赖注入解读&使用Autofac替代实现

    ASP.NET Core依赖注入解读&使用Autofac替代实现1. 前言2. ASP.NET Core 中的DI方式3. Autofac实现和自定义实现扩展方法3.1 安装Autofac3.2 创建容器并注册依赖4. 参考链接1. 前言关于IoC模式(控制反转)和DI技术(依赖注入),我们已经见过很多的探讨,这里就不再赘述了。比如说必看的Martin Fowler《IoC 容器和 Dependency Injection 模式》,相关资料链接都附于文章末尾。其中我非常赞同Artech的说法"控制更多地体现为一种流程的控制",而依赖注入技术让我们的应用程序实现了松散耦合。ASP.NET Core本身已经集成了一个轻量级的IOC容器,开发者只需要定义好接口后,在Startup.cs的ConfigureServices方法里使用对应生命周期的绑定方法即可,常见方法如下services.AddTransient<IApplicationService

    实现,使用,替代,依赖,注入
    2018-09-30

    47

  • ASP.NET Core 介绍和项目解读
    ASP.NET Core 介绍和项目解读

    1. 前言 2. ASP.NET Core 简介2.3.1 项目文件夹总览2.3.2 project.json和global.json2.3.1 Properties——launchSettings.json2.3.4 Startup.cs2.3.5 bundleconfig.json2.3.6 wwwroot和bower.json2.3.7 appsettings(1) 构造函数(2) ConfigureServices(3) Configure2.1 什么是ASP.NET Core2.2 ASP.NET Core的特点2.3 ASP.NET Core 项目文件夹解读3. 参考资料1. 前言作为一个.NET Web开发者,我最伤心的时候就是项目开发部署时面对Windows Server上贫瘠的解决方案,同样是神器Nginx,Win上的Nginx便始终不如Linux上的,你或许会说“干嘛不用windows自带的NLB呢”,那这就是我这个小鸟的从众心理了,君不见Stack Overflow 2016最新架构中,用的负载和缓存技术也都是采用在Linux上已经成熟的解决方案吗。没办法的时候找个适合的解决办法是好事,有办法的时候当然要选择最好的解决办法。所幸,.ASP.NET Core出现了,它顺应了开源大趋势,摆脱了一直为人诟病的Win Server,以ASP.NET的跨平台版本出现在了我们的眼前。暂且不论Benchmark中无聊的性能比较,也不探讨将来是否能和JAVA,PHP Web应用分庭抗礼,但是至少对我们.NET平台开发者来说,我们多了一种开发方向,也多了一个尝试前沿成熟技术的机会。所谓工欲善其事,必先利其器,我们先来看看ASP.NET Core是什么吧。2. ASP.NET Core 简介2.1 什么是ASP.NET CoreASP.NET Core 是一个新的开源和跨平台的框架,用于构建如 Web 应用、物联网(IoT)应用和移动后端应用等连接到互联网的基于云的现代应用程序。ASP.NET Core 应用可运行于 .NET Core 和完整的 .NET Framework 之上。它整合了原来ASP.NET中的MVC和WebApi框架,你可以在 Windows、Mac 和 Linux 上跨平台的开发和运行你的 ASP.NET Core 应用。2.2 ASP.NET Core的特点ASP.NET Core 在架构上做出了一些改变,这些改变会使它成为一个更为精简并且模块化的框架。在project.json文件中我们可以发现,ASP.NET Core 不再基于 System.Web.dll(我们在project.json中见到的大部分都是Microsoft打头) ,基于一系列颗粒化的,并且良好构建的 NuGet 包,结合智能提示,它能够让你通过仅仅包含需要的 NuGet 包的方法来优化你的应用。一个更小的应用程序接口通过“只为你需要的功能付出”(pay-for-what-you-use)的模型获得的好处包括更可靠的安全性、简化服务、改进性能和减少成本。Tips:通过 Ctrl+F5(非调试模式)启动这个应用程序允许你进行代码更改,保存文件,刷新浏览器,之后查看代码改变。许多开发者更倾向于使用非调试模式来快速启动应用程序和查看变化。以下列举其他几个改良特点开源和跨平台满足运行在.NET Core和.NET Framework上中间件支持性能优化无所不在的依赖注入标准日志记录整合MVC和Web Api到一个框架中MVC 标签帮助CLI工具2.3 ASP.NET Core 项目文件夹解读ASP.NET Core 1.0 发布以来,相较于传统项目编码发布的行为,新项目中的操作已经有了很大的变化,如解析依赖,选择运行平台和Runtime等等,就连项目结构也有了比较大的改变,越来越多的配置选项由编辑器转交给了开发者手动决定,这一点在新的各类配置文件中体现得尤为明显,这里就来简单解读一下。Tips:顺便吐槽一下都Upadte3了,最新的.NET Core项目中,Visual操作中还是有好多明显的bug呀。2.3.1 项目文件夹总览2.3.2 project.json和global.jsonproject.json是.NET Core项目中最重要的一个配置文件,它类似于.NET Framework上的 .csrpoj文件(在下一版本中.NET Core将弃用该文件,转而回归.csrpoj)。所以这里还是搬运下张大大的博客吧,包括对global.json的解读。project.json 这葫芦里卖的什么药2.3.1 Properties——launchSettings.json顾名思义——启动配置文件。launchSettings.json文件为一个ASP.NET Core应用保存特有的配置标准,用于应用的启动准备工作,包括环境变量,开发端口等。在launchSettings.json文件中进行配置修改,和开发者右键项目——属性中所提交的更改的效果是一样的(目前右键属性中的Property真是少得可怜),并且支持同步更新。{  "iisSettings": {                                 #选择以IIS Express启动       "windowsAuthentication": falsequot;appsettings.{env.EnvironmentName}.json", optional: true)                 .AddEnvironmentVariables();            if (env.IsDevelopment()) //读取环境变量是否为Development,在launchSettings.json中定义             {                // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.                 builder.AddApplicationInsightsSettings(developerMode: true);             }             Configuration = builder.Build();         }

    (2) ConfigureServices

    ConfigureServices 用来配置我们应用程序中的各种服务,它通过参数获取一个IServiceCollection 实例并可选地返回 IServiceProvider。ConfigureServices 方法需要在 Configure 之前被调用。我们的Entity Framework服务,或是开发者自定义的依赖注入(ASP.NET Core自带的依赖注入也是无所不在),更多内容请见官方文档

     public void ConfigureServices(IServiceCollection services)
            {            
                // Add framework services.
                services.AddApplicationInsightsTelemetry(Configuration);            services.AddMvc();
            }

    (3) Configure

    Configure 方法用于处理我们程序中的各种中间件,这些中间件决定了我们的应用程序将如何响应每一个 HTTP 请求。它必须接收一个IApplicationBuilder参数,我们可以手动补充IApplicationBuilder的Use扩展方法,将中间件加到Configure中,用于满足我们的需求。

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
            {            loggerFactory.AddConsole(Configuration.GetSection("Logging"));            loggerFactory.AddDebug();            app.UseApplicationInsightsRequestTelemetry();            if (env.IsDevelopment())
                {                app.UseDeveloperExceptionPage();                app.UseBrowserLink();
                }            else
                {                app.UseExceptionHandler("/Home/Error");
                }            app.UseApplicationInsightsExceptionTelemetry();            app.UseStaticFiles(); 
    
                app.UseMvc(routes =>  //MVC路由配置
                {                routes.MapRoute(                    name: "default",                    template: "{controller=Home}/{action=Index}/{id?}");
                });
            }

    2.3.5 bundleconfig.json


    bundleconfig.json是一个压缩包的集合文件(这个不是很明白),这里有一篇bundleconfig.json specs,大意是它可以自动压缩关联文件用于项目中,如生成 <script><link>符号.

    2.3.6 wwwroot和bower.json


    wwwroot是一个存放静态内容的文件夹,存放了诸如css,js,img等文件。刚才提到新的ASP.NET Core使开发灵活度大大提高,文件配置也都是手动为主,所以既然有存放文件的wwwroot,那也有存放文件引用的bower.json

    {  "name": "asp.net",  "private": true,  "dependencies": {    "bootstrap": "3.3.6",    "jquery": "2.2.0",    "jquery-validation": "1.14.0",    "jquery-validation-unobtrusive": "3.2.6"
      }
    }

    bower.json记录了项目需要的相关文件引用,我们可以在里面自由删除增加需要的文件,如jquery.form.js,Bower配置管理器也会自动帮我们在github上下载相关文件,下载后的文件也将放在wwwroot文件夹中。这些改变在项目的“依赖项”上都能直观查看。

    Tips:每个项目中只能有一个bower.json配置文件,对于bower.json的详细信息请参见Bower —— 管理你的客户端依赖关系

    2.3.7 appsettings


    同样是顾名思义——应用配置,类似于.NET Framework上的Web.Config文件,开发者可以将系统参数通过键值对的方式写在appsettings文件中(如程序的连接字符串),而Startup类中也在构造器中通过如下代码使得程序能够识别该文件

    var builder = new ConfigurationBuilder()
                    .SetBasePath(env.ContentRootPath)
                    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddJsonFile(

    原文: 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 环境中这将非常酷,你可以将所有配置集中在一个位置,并使用热重新加载功能进行实时切换。


    quot;appsettings.{env.EnvironmentName}.json", optional: true)                 .AddEnvironmentVariables();

    3. 参考资料


    ,220)/}

    项目,介绍,和解,
    2018-09-30

    112

  • .NET Core全新路线图(译)
    .NET Core全新路线图(译)

    承接张善友大大的.NET Core全新路线图,翻译了原文,水平有限,尽量一观。原文地址《.NET Core Roadmap》,原作者Scott Hunter.1. .NET Core 新路线自我们发布.NET Core/Asp.NET Core 1.0以来,已经过去了两个星期。开发小组已经用这两个星期做好了调整,所以是时候为接下来的开发计划开始做些准备了。我们已经看到了大量关于.NET Core的下载,同时也收到了很多显著和有效的反馈,我们欢迎所有开发者继续保持这样的反馈。 以下内容为我们未来的开发计划提供了一份粗略的时间表。要注意的是,这些计划虽然都有针对性的日期,开发小组目前正朝着这样的目标努力,但实际情况可能会发生变化。2. 1.0.1版本 (~August 2016)我们正在积极地监测.NET Core/Asp.NET Core 1.0发布版本的各个问题,包括.NET Core Sdk 1.0发行版的首个补丁(1.0.1)。这个补丁更新的日期没有排定,但在8月前是可能的。以下是一个我们正在调查的热门问题的列表:*dotnet build 程序的性能改进,它将改进Asp.NET Core的发布时间(F#相关,略)基于碰撞检测的工具的多项修复3. 早至Q4 2016,晚至Q1 2017这将是第一次较小的更新,主要集中在对使用.csproj/MSBuild替换.xproj/project.json等工具的更新。我们认为项目格式的更新应该是自动的。比如说当我们打开一个1.0版本的项目时,它会自动更新到新的项目格式。同时这次更新也包括了关于运行时和类库的相关功能上的更新和改进。4. .NET Core 工具对.csproj/MSBuild项目系统的支持dotnet restore程序的改进——不要还原本属于.NET Core的包用于管理在机器上的框架的新命令为了最佳的发布空间大小,dotnet publish程序将只发布所需要的依赖5. 语言 (适用于 .NET Framework 和 .NET Core).NET语言C#的下一个发布版本(C# 7)将实现面向所有的.NET平台应用。关于在这些版本中包括的功能已经有很多信息了,这里只列出一个简短的总结:为.NET 语言带来函数式编程概念Tuples(元组数据结构)Pattern matching(模式匹配)性能和代码质量Value Tasks(未找到相关资料,猜测是将Task类重写为值类型)Ref returns(引用返回)Throw expressionsBinary literals(二进制字面值)Digit separators(数字分隔符)开发人员生产效率Out vars(该特性允许当变量被out参数传递时可以同时声明变量)Local functions(局域函数)这些特性都将在C# 7中实现。而VB 15将实现全部影响语言互操作的特性(tuples,ref returns等),但是一些特性只会在下下语言版本更新时补充(如pattern matching),或者将不再出现在路线图上(如local functions) (省略F#特性说明)6. ASP.NET CoreWeb ScoketsURL Rewriting Middleware(URL重写中间组件)Azure(对于大多数国内开发者并不是很重要)App Service startup time improvements(应用程序服务启动时间改进)App Service Logging Provider(应用程序服务日志提供者)Azure Key Vault Provider(What is Azure Key Vault?)Azure AD B2C SupportContainers and Microservices(容器和微服务)Service Fabric support via WebListener based server(What is Service Fabric)MVC & 依赖注入启动时间改进Previews(前瞻)SignalR(常见的Web实时消息交互方式和SignalR)View Pages (没有MVC控制器的视图)7. .NET Core Runtime and LibrariesARM 32/64(ARM 32/64位架构)支持更多的Linux发行版(从源代码构建)8. Entity Framework CoreAzureTransient fault handling (resiliency)Mapping(匹配)Custom type conversions(自定义类型转换)Complex types (value objects)Entity entry APIs(实体 Entry接口)Update pipelineCUD stored procedures(增删改存储过程)Better batching (TVPs)更好的批处理(Table Valued Parameters)Ambient transactions(环境事务)QueryStability

    路线图,全新,
    2018-09-30

    186

  • EntityFramework Core 学习扫盲 (上)
    EntityFramework Core 学习扫盲 (上)

    1. 备用键2. 唯一索引1. Fluent API1. Data Annotations2. Fluent API1. Fluent API1. Fluent API1. Data Annotations [Key]2. Fluent API [HasKey]1. Data Annotations1. Data Annotations [NotMapped] 排除实体和属性2. Fluent API [Ignore] 排除实体和属性1. 准备工作2. Data Annotations3. Fluent Api0. 写在前面1. 建立运行环境2. 添加实体和映射数据库3. 包含和排除实体类型4. 列名称和类型映射5. 主键6. 备用键7. 计算列8. 生成值9. 默认值10. 索引11. 主体和唯一标识12. 继承13. 关系14. Console中的EntityframeworkCore15. 参考链接和优秀博客0. 写在前面本篇文章虽说是入门学习,但是也不会循规蹈矩地把EF1.0版本一直到现在即将到来的EF Core 2.0版本相关的所有历史和细节完完整整还原出来。在后文中,笔者会直接进入正题,所以这篇文章仍然还是需要一定的EF ORM基础。对于纯新手用户,不妨先去看看文末链接中一些优秀博客,笔者当初也是从这些博客起家,也从中得到了巨大的帮助。当然了,官方教程同样至关重要,笔者之前也贡献过部分EF CORE 官方文档资料(基本都是勘误,逃…),本篇文章中很多内容都是撷取自官方的英文文档和示例。下文示例中将使用Visual Studio自带的Local Sql Server作为演示数据库进行演示,不过可以放心的是,大部分示例都能流畅地在各种关系型数据库中实现运行,前提是更换不同的DATABASE PROVIDERS,如NPGSQL,MYSQL等。对于未涉及到的知识点(CLI工具,Shadow Property,Logging,从Exsiting Database反向工程生成Context等),只能说笔者最近一直在忙着毕业收尾的事情,有空的时候会把草稿整理下在博文中贴出的,一晃四年,终于也要毕业了。1. 建立运行环境新建一个APS.NET CORE WEB模板项目安装相关Nuget包//Sql Server Database ProviderInstall-Package Microsoft.EntityFrameworkCore.SqlServer      //提供熟悉的Add-Migration,Update-Database等Powershell命令,不区分关系型数据库类型Install-Package Microsoft.EntityFrameworkCore.Tools自定义Contextpublic class MyContext : DbContext{    public MyContext(DbContextOptions<MyContext> options):base(options)    {     }    protected override void OnModelCreating(ModelBuilder modelBuilder)    {     } }在Startup的ConfigurationServices方法中添加EF CORE服务public void ConfigureServices(IServiceCollection services) {    // Add framework services.     services.AddMvc();     services.AddDbContext<MyContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); }         // 需要在appsettings.json中新增一个ConnectionStrings节点,用于存放连接字符串。"ConnectionStrings": {    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-WebApplication4;Trusted_Connection=True;MultipleActiveResultSets=true"   }

    学习,
    2018-09-30

    128

  • EntityFramework Core 学习扫盲 (中)
    EntityFramework Core 学习扫盲 (中)

    6. 备用键Alternate Keys是EF CORE引入的新功能,EF 6.X版本中并没有此功能。备用键可以用作实体中除主键和索引外的唯一标识符,还可以用作外键目标。在Fluent Api中,有两种方法可以指定备用键,一种是当开发者将实体中的属性作为另一个实体的外键目标,另一种是手动指定。EF CORE的默认约束是前者。备用键和主键的作用十分相似,同样也存在复合备用键的功能,请大家注意区分。在要求单表列的一致性的场景中,使用唯一索引比使用备用键更佳。1. Fluent APIpublic class MyContext : DbContext{    public MyContext(DbContextOptions<MyContext> options) : base(options)    {     }    public DbSet<Car> Cars { get; set; }    public DbSet<Blog> Blogs { get; set; }    public DbSet<Post> Posts { get; set; }    protected override void OnModelCreating(ModelBuilder modelBuilder)    {        // 第一种方法         modelBuilder.Entity<Post>()             .HasOne(p => p.Blog)             .WithMany(b => b.Posts)             .HasForeignKey(p => p.BlogUrl)             .HasPrincipalKey(b => b.Url);                     // 第二种方法         modelBuilder.Entity<Car>()             .HasAlternateKey(c => c.LicensePlate)             .HasName("AlternateKey_LicensePlate");     } }public class Car{    public int CarId { get; set; }    public string LicensePlate { get; set; }    public string Make { get; set; }    public string Model { get; set; } }public class Blog{    public int BlogId { get; set; }    public string Url { get; set; }    public List<Post> Posts { get; set; } }public class Post{    public int PostId { get; set; }    public string Title { get; set; }    public string Content { get; set; }    public string BlogUrl { get; set; }    public Blog Blog { get; set; } }上述代码中的第一种方法指定Post实体中的BlogUrl属性作为Blog对应Post的外键,指定Blog实体中的Url属性作为备用键(HasPrincipalKey方法将在下文的唯一标识节中讲解),此时Url将被配置为唯一列,扮演BlogId的作用。7. 计算列计算列指的是列的数据由数据库计算生成,在EF CORE层面,我们只需要定义计算规则即可。目前EF CORE 1.1 版本中,暂不支持使用Data Annotations方式定义。1 Fluent APIclass MyContext : DbContext{    public DbSet<Person> People { get; set; }    protected override void OnModelCreating(ModelBuilder modelBuilder)    {         modelBuilder.Entity<Person>()             .Property(p => p.DisplayName)             .HasComputedColumnSql("[LastName] + '

    学习,
    2018-09-30

    119

  • EntityFramework Core 学习扫盲 (下)
    EntityFramework Core 学习扫盲 (下)

    11. 主体和唯一标识在这一节中,让我们来回顾一下HasPrincipalKey方法和唯一标识。在EF CORE中,主体(Principal Entity)指的是包含主键/备用键的实体。所以在一般情况下,所有的实体都是主体。而主体键(Principal Key)指的是主体中的主键/备用键。大家都知道,主键/备用键都是不可为空且唯一的,这就引出了唯一标识列的写法。唯一标识列一般有“主体键”,“唯一索引”两种写法,其中主体键中的主键没有什么讨论的价值。让我们来看看其他两种的写法。1. 备用键备用键在之前的小节中已经提过,使用以下代码配置的列将自动设置为唯一标识列。modelBuilder.Entity<Car>()                 .HasAlternateKey(c => new {c.LicensePlate

    学习,
    2018-09-30

    111

  • .NET CORE——Console中使用依赖注入
    .NET CORE——Console中使用依赖注入

    [ERR:(RemoveHtmlTag)标签缺少参数]quot;We have got the {i} Loop");     } }

    • Program 中对 DI 组件的初始化和服务的注册

    private static void Main(string[] args){    var serviceProvider = new ServiceCollection()
            .AddLogging()
            .AddSingleton<ICounterAppService, CounterAppService>()
            .BuildServiceProvider();
    
        serviceProvider
            .GetService<ILoggerFactory>()
            .AddConsole(LogLevel.Debug);    var logger = serviceProvider.GetService<ILoggerFactory>()
            .CreateLogger<Program>();
        logger.LogDebug("Starting application");    var counter = serviceProvider.GetService<ICounterAppService>();
        counter.Count(10);
    
        logger.LogDebug("All done!");
    }

    我们手动创建 serviceProvider 的过程其实就是 ASP.NET CORE 执行 ConfigureServices 方法的过程,同样的,上述代码也展示了手动解析 Logger 实例和通过构造函数注入解析 Logger 实例的两种方式。其中 AddLogging 方法的背后代码如下所示:

    public static IServiceCollection AddLogging(this IServiceCollection services){  if (services == null)    throw new ArgumentNullException("services");
      services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
      services.TryAdd(ServiceDescriptor.Singleton(typeof (ILogger<>), typeof (Logger<>)));  return services;
    }

    2. 在 Console 中使用第三方 Autofac DI 组件

    笔者曾经写过在 ASP.NET CORE 使用 Autofac 组件的例子,而在 Console 中,注册流程也没有什么变化。以下是 Program 中的代码

    private static void Main(string[] args){    var serviceCollection = new ServiceCollection();
        
        serviceCollection.AddLogging();    var containerBuilder = new ContainerBuilder();    
        // 将原本注册在内置 DI 组件中的依赖迁移入 Autofac 中
        containerBuilder.Populate(serviceCollection);    
        // 也可以把 ICounterAppService 预先注入到内置 DI 中再使用 Populate 方法迁移
        containerBuilder.RegisterType<CounterAppService>().As<ICounterAppService>();    
        var container = containerBuilder.Build();    var serviceProvider = new AutofacServiceProvider(container);
        
        serviceProvider
            .GetService<ILoggerFactory>()
            .AddConsole(LogLevel.Debug);    var logger = serviceProvider.GetService<ILoggerFactory>()
            .CreateLogger<Program>();
        logger.LogDebug("Starting!");    var counter = serviceProvider.GetService<ICounterAppService>();
        counter.Count(10);
    
        logger.LogDebug("Done!");
    }

    同时,Autofac中也提供了诸如 RegisterAssemblyTypes 的方法用于程序集中服务的批量注入,这也是第三方容器的优势所在。

    Using dependency injection in a .Net Core console application
    ASP.NET Core Dependency Injection Deep Dive


    ,220)/}

    使用,依赖,注入,
    2018-09-30

    137

  • 浅谈 EF CORE 迁移和实例化的几种方式
    浅谈 EF CORE 迁移和实例化的几种方式

    出于学习和测试的简单需要,使用 Console 来作为 EF CORE 的承载程序是最合适不过的。今天笔者就将平时的几种使用方式总结成文,以供参考,同时也是给本人一个温故知新的机会。因为没有一个完整的脉络,所以也只是想起什么写点什么,不通顺的地方还请多多谅解。本文对象数据库默认为 VS 自带的 LocalDB1. Normal & Simple先介绍一种最简单的构建方式,人人都会。新建 Console 应用程序,命名自定安装相关Nuget 包//Sql Server Database ProviderInstall-Package Microsoft.EntityFrameworkCore.SqlServer      //提供熟悉的Add-Migration,Update-Database等Powershell命令,不区分关系型数据库类型Install-Package Microsoft.EntityFrameworkCore.Tools自定义 DbContextpublic class MyContext:DbContext{    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)    {         optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=ConsoleApp;Trusted_Connection=True;MultipleActiveResultSets=true;");     } }执行迁移和更新命令Add-Migration InitializeUpdate-Database使用方式using (var context = new MyContext()) {    // TODO}刚以上,我们便见识到了了一种最平常也是最简单的使用方式,接下来,让我们用其他方式去慢慢地改造它,从而尽可能地接触更多的用法。2. Level Up2.1 准备工作将第一步生成的数据库,迁移文件和使用方式内容全部删除。2.2 更新 MyContext 内容删除 MyContext 中的 OnConfiguring 方法及其内容,增加含有 DbContextOptions 类型参数的构造器,我们的MyContext看起来应该是下面这个样子。public class MyContext : DbContext{    public MyContext(DbContextOptions options) : base(options)    {     } }假如我们此时仍然再执行迁移命令,VS将提示以下错误No parameterless constructor was found on 'MyContext'. Either add a parameterless constructor to 'MyContext' or add an implementation of 'IDbContextFactory' in the same assembly as 'MyContext'.添加无参构造器的方式之后再讲解,先来按照提示信息添加一个 IDbContextFactory的实现类。public class MyContextFactory : IDbContextFactory<MyContext> {    public MyContext Create(DbContextFactoryOptions options)     {         var optionsBuilder = new DbContextOptionsBuilder<MyContext>();         optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=ConsoleApp;Trusted_Connection=True;MultipleActiveResultSets=true;");                     return new MyContext(optionsBuilder.Options);     } }之后再次运行迁移和更新数据库的命令也是水到渠成。2.3 使用方式:构造器实例化既然 MyContext 含有 DbContextOptions 类型参数的构造器,那就手动创建一个参数实例注入即可。var contextOptionsBuilder = new DbContextOptionsBuilder<MyContext>(); contextOptionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=ConsoleApp;Trusted_Connection=True;MultipleActiveResultSets=true;");// 注入配置选项using (var context = new MyContext(contextOptionsBuilder.Options)) {    // TODO}经此,我们知道了迁移命令会检测 Context 的相关配置入口,只有在满足存在 OnConfiguring 方法或者存在自建 IDbContextFactory 实现类的情况下,命令才能成功运行。3. Day Day Up目前为止,我们已经知道如何手动迁移和实例化 Context 的步骤了所以让我们更进一步。写过 ASP.NET CORE 的人可能知道在 ASP.NET CORE 中,Context 常常以依赖注入的方式引入到我们的 Web 层,Service 层,或者 XXCore 层中(话说笔者最近最喜欢的解决方案开发架构就是伪 DDD 的四层架构,有空再介绍吧)。其实在 Console 应用中,这也可以很容易实现,具体的依赖注入引入可以参考笔者的上一篇博客,所以最终的代码效果如下:var serviceCollection = new ServiceCollection(); serviceCollection.AddDbContext<MyContext>(c =>{     c.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=ConsoleApp;Trusted_Connection=True;MultipleActiveResultSets=true;"); });var serviceProvider = serviceCollection.BuildServiceProvider(); using (var context = serviceProvider.GetService<MyContext>()) {    //context.Database.Migrate();}至此,我们便基本完成了本文的主题,唯一有些美中不足的是我们的数据库连接字符串好像到处都是,这不是什么大问题,笔者直接将 Configuration 的配置代码贴在下面,这也是 ABP 中的方式。public class AppConfigurations{    private static readonly ConcurrentDictionary<stringquot;appsettings.{environmentName}.json", true);         builder = builder.AddEnvironmentVariables();        return builder.Build();     } }

    这个工具类的使用方式就不再赘述了。

    4. 结尾

    最后,想必会有人问为什么要折腾这样一个小小的 Console 应用呢?其实通过这样一步步下来,我们可以发现一些项目功能上的亮点,比如既然可以自配置 DbContext 的 Option 选项,同时我们也知道了如何在类库和 Console 项目中添加依赖注入以及 Configuration 提取链接参数的功能,那针对三层架构或是 DDD 项目增加含真实数据库或是内存数据库(InMemory)的单元测试,或者是自动Migrate Context 和更新数据库也将是十分简单的一件事,至少看起来会比官方的示例更加真实和具有可操作性。而这部分内容笔者也将会在之后的博文中给出。

    我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan


    ,220)/}

    实例,迁移,方式,
    2018-09-30

    129