ASP.NET Core 构建配置中心

@adens 8/28/2020 8:15:57 AM

配置,几乎所有的应用程序都离不开它。.Net Framework时代我们使用App.config、Web.config,到了.Net Core的时代我们使用appsettings.json,这些我们再熟悉不过了。然而到了容器化、微服务的时代,这些本地文件配置有的时候就不太合适了。当你把本地部署的服务搬到docker上后,你会发现要修改一个配置文件变的非常麻烦。你不得不通过宿主机进入容器内部来修改文件,也许容器内还不带vi等编辑工具,你连看都不能看,改都不能。更别说当你启动多个容器实例来做分布式应用的时候,一个个去修改容器的配置,这简直要命了。

因为这些原因,所以“配置中心”就诞生了。配置中心是微服务的基础设施,它对配置进行集中的管理并对外暴露接口,当应用程序需要的时候通过接口读取。配置通常为Key/Value模式,然后通过http接口暴露。

1.Web Api 模拟配置中心

模拟提供一个方法获取配置 新建一个Web Api 项目,新建一个ConfigController,提供一个Get方法下载JSON格式1的配置文件

    [Route("[controller]")]
    [ApiController]
    public class ConfigController : ControllerBase
    {
    
        [HttpGet]
        public IActionResult Get()
        {
            string config = "{\"ConnectString\":\"Server=(localdb)\\\\MSSQLLocalDB;Database=aspnet-config-7db2893b-375e-48bd-86a3-bb9779b72ebe;Trusted_Connection=True;MultipleActiveResultSets=true\"}";
            return File( UTF8Encoding.UTF8.GetBytes(config), "application/json");
        }
    }

运行结果为: ConfigDemo01.png

2. Web Api模拟程序获取配置

2.1 新建一个测试的Web Api项目 2.2 新建一个RemoteConfigurationProvider类继承自ConfigurationProvider. 这个类用来连接配置中心的接口,获取JSON格式的配置文件

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

namespace Adens.Configuration.Test.Configuration
{
    public class RemoteConfigurationProvider: ConfigurationProvider
    {
        public HttpClient Backchannel { get; set; }
        public JsonConfigurationParser Parser { get; set; }
        public override void Load()
        {
            Backchannel = new HttpClient();
            Parser = new JsonConfigurationParser();
            var requestMessage = new HttpRequestMessage(HttpMethod.Get,"https://localhost:4040/Config");

            //Source.Events.SendingRequest(requestMessage);

            try
            {
                var response = Backchannel.SendAsync(requestMessage)
                    .ConfigureAwait(false)
                    .GetAwaiter()
                    .GetResult();

                if (response.IsSuccessStatusCode)
                {
                    using (var stream = response.Content.ReadAsStreamAsync()
                        .ConfigureAwait(false)
                        .GetAwaiter()
                        .GetResult())
                    {
                        var data = Parser.Parse(stream, "");
                        Data = data;
                    }
                }

            }
            catch (Exception)
            {
             
                    throw;
            }
        }
    }
}

2.3 从github上把JsonConfigurationParser方法Copy下来

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace Adens.Configuration.Test.Configuration
{
    public interface IConfigurationParser
    {
        /// <summary>
        /// Parse the input stream into a configuration dictionary 
        /// </summary>
        /// <param name="input">The stream to parse</param>
        /// <param name="initialContext">The initial context prefix to add to all keys</param>
        /// <returns></returns>
        IDictionary<string, string> Parse(Stream input, string initialContext);
    }
}

// JsonConfigurationParser.cs
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace Adens.Configuration.Test.Configuration
{
    /// <summary>
    /// Taken from Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser
    /// </summary>
    public class JsonConfigurationParser : IConfigurationParser
    {
        private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        private readonly Stack<string> _context = new Stack<string>();
        private string _currentPath;

        private JsonTextReader _reader;

        /// <summary>
        /// Parse the input stream into a configuration dictionary 
        /// </summary>
        /// <param name="input">The stream to parse</param>
        /// <param name="initialContext">The initial context prefix to add to all keys</param>
        /// <returns></returns>
        public IDictionary<string, string> Parse(Stream input, string initialContext)
        {
            try
            {
                _data.Clear();
                _reader = new JsonTextReader(new StreamReader(input));
                _reader.DateParseHandling = DateParseHandling.None;

                var jsonConfig = JObject.Load(_reader);

                if (!string.IsNullOrEmpty(initialContext)) { EnterContext(initialContext); }
                VisitJObject(jsonConfig);
                if (!string.IsNullOrEmpty(initialContext)) { ExitContext(); }

                return _data;
            }
            catch (JsonReaderException e)
            {
                string errorLine = string.Empty;
                if (input.CanSeek)
                {
                    input.Seek(0, SeekOrigin.Begin);

                    IEnumerable<string> fileContent;
                    using (var streamReader = new StreamReader(input))
                    {
                        fileContent = ReadLines(streamReader);
                        errorLine = RetrieveErrorContext(e, fileContent);
                    }
                }

                throw new FormatException(string.Format(
                        "Could not parse the JSON file. Error on line number '{0}': '{1}'.",
                        e.LineNumber,
                        errorLine),
                    e);
            }

        }

        private void VisitJObject(JObject jObject)
        {
            foreach (var property in jObject.Properties())
            {
                EnterContext(property.Name);
                VisitProperty(property);
                ExitContext();
            }
        }

        private void VisitProperty(JProperty property)
        {
            VisitToken(property.Value);
        }

        private void VisitToken(JToken token)
        {
            switch (token.Type)
            {
                case JTokenType.Object:
                    VisitJObject(token.Value<JObject>());
                    break;

                case JTokenType.Array:
                    VisitArray(token.Value<JArray>());
                    break;

                case JTokenType.Integer:
                case JTokenType.Float:
                case JTokenType.String:
                case JTokenType.Boolean:
                case JTokenType.Bytes:
                case JTokenType.Raw:
                case JTokenType.Null:
                    VisitPrimitive(token);
                    break;

                default:
                    throw new FormatException(string.Format(
                        "Unsupported JSON token '{0}' was found. Path '{1}', line {2} position {3}.",
                        _reader.TokenType,
                        _reader.Path,
                        _reader.LineNumber,
                        _reader.LinePosition));
            }
        }

        private void VisitArray(JArray array)
        {
            for (int index = 0; index < array.Count; index++)
            {
                EnterContext(index.ToString());
                VisitToken(array[index]);
                ExitContext();
            }
        }

        private void VisitPrimitive(JToken data)
        {
            var key = _currentPath;

            if (_data.ContainsKey(key))
            {
                throw new FormatException(string.Format(
                    "A duplicate key '{0}' was found.",
                    key));
            }
            _data[key] = data.ToString();
        }

        private void EnterContext(string context)
        {
            _context.Push(context);
            _currentPath = ConfigurationPath.Combine(_context.Reverse());
        }

        private void ExitContext()
        {
            _context.Pop();
            _currentPath = ConfigurationPath.Combine(_context.Reverse());
        }

        private static IEnumerable<string> ReadLines(StreamReader streamReader)
        {
            string line;
            do
            {
                line = streamReader.ReadLine();
                yield return line;
            } while (line != null);
        }

        private static string RetrieveErrorContext(JsonReaderException e, IEnumerable<string> fileContent)
        {
            string errorLine;
            if (e.LineNumber >= 2)
            {
                var errorContext = fileContent.Skip(e.LineNumber - 2).Take(2).ToList();
                errorLine = errorContext[0].Trim() + Environment.NewLine + errorContext[1].Trim();
            }
            else
            {
                var possibleLineContent = fileContent.Skip(e.LineNumber - 1).FirstOrDefault();
                errorLine = possibleLineContent ?? string.Empty;
            }

            return errorLine;
        }
    }
}


2.4 新建一个RemoteConfigurationSource类.继承IConfigurationSource Main方法里 CreateHostBuilder Build时执行该类的Build方法添加一个配置源

public class RemoteConfigurationSource : IConfigurationSource
    {
        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new RemoteConfigurationProvider();
        }
    }

2.5 修改 Program 里面的代码

 public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((context, builder) => {
					// 添加一个新的配置源
                    builder.Add(new RemoteConfigurationSource());
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

2.6 测试代码

  public class HomeController : Controller
    {
     
        IConfiguration _configuration;
        public HomeController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public string Index()
        {
            return _configuration.GetValue<string>("ConnectString");
        }

    }

2.7 测试结果 configresult01.png

3. Q&A

Q:1.为什么使用JSON格式的文件作为返回值,而不是KeyValue的键值对 A:为了适应以后在页面编辑配置,使用JSON格式更加易读.多层嵌套数据和数组之类的数据可以更好的维护.而且和ASP.NET Core 使用的Appsettings.json的配置方式相同.

缺点就是需要专门的解析JSON格式到KeyValue格式的方法.不过这个方法微软已经写在了Microsoft.Extensions.Configuration.Json.JsonConfigurationFileParser这个方法里面.可以直接拿过来使用.同样由于已存在这个方法,可以随意改造接口返回KeyValue的键值对式的配置

Last Modification : 8/28/2020 8:15:57 AM


In This Document