迁移appseting.json创建自定义配置中心
阅读原文时间:2021年10月13日阅读:1

创建一个自定义的配置中心,将框架中各类配置,迁移至数据库,支持切换数据库,热重载。

说在前面的话

自使用.net Core框架以来,配置大多存在json文件中:

  • 【框架默认加载配置】文件为appseting.json 以及ppsettings.Environment.json,
  • 【环境变量】存在aunchSettings.json 中,
  • 【用户机密】则存在%APPDATA%\Microsoft\UserSecrets\secrets.json
  • 他们都可以用.netcore 框架自带的方式读取编辑,例如IConfiguration。

文本讨论的是创建一个自定配置中心主要是想通过不改变去读取方式去将appseting.json这些配置迁移至数据库中。按照之前的做法,我们可以通过在program.cs中使用WebHost.ConfigureAppConfiguration去读取数据库的数据,然后填充至配置中去实现,如下图:

这样做会有两个问题

  • 配置是在程序入口的创建主机配置CreateHostBuilder()方法中去加入的,所以他无法二次构建,除非web重启,所以在修改了数据库内的配置无法实现热重载,
  • 此处使用的是SqLite去实现的,假设现在框架内换了数据库去实现,去修改Program.cs中代码并不现实且实在是不优雅的实现方式。

所以笔者创建一个自定义的以EFCore作为配置源的配置中心去解决以上两个问题,并且把他封装成一个类库,可适用于多场景。

依照惯例,源代码在文末,需要自取~

源码配合【使用方式】章节可直接食用

想要解决数据库切换的问题,首先就是把配置构建从Program类中抽离出来,重新构建一个类去创建配置所用到的IConfiguration,故我将配置的初始写在静态方法中,通过传递连接字符串以及数据库类型的方式去构建不同的上下文,并且在错误的时候抛出异常。

    public class EFConfigurationBuilder
    {
        /// <summary>
        /// 配置的IConfiguration
        /// </summary>
        public static IConfiguration EFConfiguration { get; set; }
        /// <summary>
        /// 连接字符串
        /// </summary>
        public static string ConnectionStr { get; set; }

        /// <summary>
        /// 初始化
        /// </summary>
    public static IConfiguration Init(string connetcion, DbType dbType, out string erroMesg, string version = "5.7.28-mysql")
        {
            try
            {
                erroMesg = string.Empty;
                ServerVersion serverVersion = ServerVersion.Parse(version);
                ConnectionStr = connetcion;
                if (string.IsNullOrEmpty(connetcion) && !Enum.IsDefined(typeof(DbType), dbType))
                {
                    erroMesg = "请检查连接字符串以及数据库类型";
                    return null;
                }
                var contextOptions = new DbContextOptions<DiyEFContext>();

                if (dbType.Equals(DbType.SqLite))
                {
                    contextOptions = new DbContextOptionsBuilder<DiyEFContext>()
                         .UseSqlite(connetcion)
                         .Options;
                }
                if (dbType.Equals(DbType.SqlServer))
                {
                    contextOptions = new DbContextOptionsBuilder<DiyEFContext>()
                         .UseSqlServer(connetcion)
                         .Options;
                }
                if (dbType.Equals(DbType.MySql))
                {
                    contextOptions = new DbContextOptionsBuilder<DiyEFContext>()
                         .UseMySql(connetcion, serverVersion)
                         .Options;
                }
                DbContext = new DiyEFContext(contextOptions);
                CreateEFConfiguration();
                return EFConfiguration;
            }
            catch (Exception ex)
            {
                erroMesg = ex.Message;
                return null;
            }
        }
    }


// 调用初始化方法
var configuration = EFConfigurationBuilder.Init(conn, DbType.MySql, out string erroMesg);

利用构建者&观察者模式可以实现热重载。

数据库切换其实也给了我们热重载的解决方案,可以将构建方法暴露出来,动态去刷新构造类的IConfiguration,如果是在控制台应用程序或者其他非Web项目中,可能没有appseting.json文件,所以稍微做了下判断

        /// <summary>
        /// 创建一个新的IConfiguration
        /// </summary>
        /// <returns>IConfiguration</returns>
        public static IConfiguration CreateEFConfiguration()
        {
            var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json");
            if (!File.Exists(filePath))
            {
                EFConfiguration = new ConfigurationBuilder()
                    .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
                    .Add(new EFConfigurationSource { ReloadDelay = 500, ReloadOnChange = true, DBContext = DbContext })
                    .Build();
            }
            else
            {
                EFConfiguration = new ConfigurationBuilder()
                   .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
                   .Add(new EFConfigurationSource { ReloadDelay = 500, ReloadOnChange = true, DBContext = DbContext })
                   .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true })
                   .Build();
            }
            return EFConfiguration;
        }

构建方法是准备好了,那么哪里去调用这个方法呢?

这里可以使用观察者模式,去监控配置实体的改变事件,如果有修改则调用一次构建方法去覆盖配置中心的IConfiguration。

实现最简便的方法则是在SaveChange之后加入实体监控

    internal class DiyEFContext : DbContext
    {
        public DiyEFContext(DbContextOptions<DiyEFContext> options) : base(options)
        {
        }

        public DbSet<DiyConfig> DiyConfigs { get; set; }

        public override int SaveChanges()
        {
            TrackEntityChanges();
            return base.SaveChanges();
        }
        public async Task<int> SaveChangesAsync()
        {
            TrackEntityChanges();
            return await base.SaveChangesAsync();
        }

        /// <summary>
        /// 实体监控
        /// </summary>
        private void TrackEntityChanges()
        {
            foreach (var entry in ChangeTracker.Entries().Where(e =>
                e.State == EntityState.Modified || e.State == EntityState.Added || e.State == EntityState.Deleted))
            {
                if (entry.Entity.GetType().Equals(typeof(DiyConfig)))
                {
                    EntityChangeObserver.Instance.OnChanged(new EntityChangeEventArgs(entry));
                }
                return;
            }
        }
    }

二话不说上代码

此思维导图是【艾心】大佬读取源码之后整理的,从代码层面来讲,我们的配置信息都会转换成一个IConfiguration对象供应用程序使用,IConfigurationBuilder是IConfiguration对象的构建者,IConfigurationSource则是各个配置数据的最原始来源,我们则只需要定制最底层的IConfigurationProvider提供键值对类型的数据给IConfigurationSource就可以实现自定义配置中心,说起来拗口,直接上UML图,该图源自【ASP.NET Core3框架揭秘(上册)】。

不喜欢看源码,可以直接跳到-【如何使用】

    public class EFConfigurationBuilder
    {

        /// <summary>
        /// 创建配置
        /// </summary>
        /// <param name="diyConfig"></param>
        /// <returns></returns>
        public static (bool, string) CreateConfig(DiyConfig diyConfig)
        {
            if (DbContext == null)
            {
                return (false, "未初始化上下文,请检查!");
            }
            if (diyConfig == null && DbContext.DiyConfigs.Any(x => x.Id.Equals(diyConfig.Id)))
            {
                return (false, "传入参数有误,请检查!");
            }
            if (DbContext.DiyConfigs.Any(x => x.Key.Equals(diyConfig.Key)))
            {
                return (false, "DB—已有对应的键值对");
            }
            DbContext.DiyConfigs.Add(diyConfig);
            if (DbContext.SaveChanges() > 0)
            {
                return (true, "成功");
            }
            else
            {
                return (false, "创建配置失败");
            }
        }

        /// <summary>
        /// 创建配置
        /// </summary>
        /// <param name="diyConfig"></param>
        /// <returns></returns>
        public static async Task<(bool, string)> CreateConfigAsync(DiyConfig diyConfig)
        {
            ...
        }

        /// <summary>
        /// 删除配置
        /// </summary>
        /// <param name="diyConfig"></param>
        /// <returns></returns>
        public static async Task<(bool, string)> DleteConfigAsync(DiyConfig diyConfig)
        {
            if (DbContext == null)
            {
                return (false, "未初始化上下文,请检查!");
            }
            if (diyConfig == null && !DbContext.DiyConfigs.Any(x => x.Id.Equals(diyConfig.Id)))
            {
                return (false, "传入参数有误,请检查!");
            }
            DbContext.DiyConfigs.Remove(diyConfig);
            if (await DbContext.SaveChangesAsync() > 0)
            {
                return (true, "成功");
            }
            else
            {
                return (false, "更新配置失败");
            }
        }

        /// <summary>
        /// 删除配置
        /// </summary>
        /// <param name="diyConfig"></param>
        /// <returns></returns>
        public static (bool, string) DleteConfig(DiyConfig diyConfig)
        {
           ...
        }

        /// <summary>
        /// 更新配置
        /// </summary>
        /// <param name="diyConfig"></param>
        /// <returns></returns>
        public (bool, string) UpdateConfig(DiyConfig diyConfig)
        {
         try
            {
                if (DbContext == null)
                {
                    return (false, "未初始化上下文,请检查!");
                }
                if (diyConfig == null && !DbContext.DiyConfigs.Any(x => x.Id.Equals(diyConfig.Id)))
                {
                    return (false, "传入参数有误,请检查!");
                }
                DbContext.DiyConfigs.Update(diyConfig);
                if (DbContext.SaveChanges() > 0)
                {
                    return (true, "成功");
                }
                else
                {
                    return (false, "更新配置失败");
                }
            }
            catch (Exception ex)
            {
                return (false, $"更新配置失败,error:{ex.Message}");
            }
        }

        /// <summary>
        /// 更新配置
        /// </summary>
        /// <param name="diyConfig"></param>
        /// <returns></returns>
        public async Task<(bool, string)> UpdateConfigAsync(DiyConfig diyConfig)
        {
          ...
        }

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="connetcion">连接字符串</param>
        /// <param name="dbType">数据库类型</param>
        /// <param name="erroMesg">错误消息</param>
        /// <param name="version">数据库版本</param>
        /// <returns>IConfiguration</returns>
        public static IConfiguration Init(string connetcion, DbType dbType, out string erroMesg, string version = "5.7.28-mysql")
        {
           ...
        }

        /// <summary>
        /// 创建一个新的IConfiguration
        /// </summary>
        /// <returns>IConfiguration</returns>
        public static IConfiguration CreateEFConfiguration()
        {
        ...
        }
    }


    internal class EFConfigurationSource : IConfigurationSource
    {
        public int ReloadDelay { get; set; } = 500;
        public bool ReloadOnChange { get; set; } = true;
        public DiyEFContext DBContext { get; set; }

        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new EFConfigurationProvider(this);
        }
    }


internal class EFConfigurationProvider : ConfigurationProvider
    {
        private readonly EFConfigurationSource _source;
        private IDictionary<string, string> _dictionary;

        internal EFConfigurationProvider(EFConfigurationSource eFConfigurationSource)
        {
            _source = eFConfigurationSource;
            if (_source.ReloadOnChange)
            {
                EntityChangeObserver.Instance.Changed += EntityChangeObserverChanged;
            }
        }

        public override void Load()
        {
            DiyEFContext dbContext = _source.DBContext;
            if (_source.DBContext != null)
            {
                dbContext = _source.DBContext;
            }
            _dictionary = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            // https://stackoverflow.com/questions/38238043/how-and-where-to-call-database-ensurecreated-and-database-migrate
            // context.Database.EnsureCreated()是新的 EF 核心方法,可确保上下文的数据库存在。如果存在,则不执行任何操作。如果它不存在,则创建数据库及其所有模式,并确保它与此上下文的模型兼容
            dbContext.Database.EnsureCreated();
            var keyValueData = dbContext.DiyConfigs.ToDictionary(c => c.Key, c => c.Value);
            foreach (var item in keyValueData)
            {
                if (JsonHelper.IsJson(item.Value))
                {
                    var jsonDict = JsonConvert.DeserializeObject<Dictionary<string, Object>>(item.Value);
                    _dictionary.Add(item.Key, item.Value);
                    InitData(jsonDict);
                }
                else
                {
                    _dictionary.Add(item.Key, item.Value);
                }
            }
            Data = _dictionary;
        }

        private void InitData(Dictionary<string, object> jsonDict)
        {
            foreach (var itemval in jsonDict)
            {
                if (itemval.Value.GetType().ToString().Equals("Newtonsoft.Json.Linq.JObject"))
                {
                    Dictionary<string, object> reDictionary = new Dictionary<string, object>();
                    JObject jsonObject = (JObject)itemval.Value;
                    foreach (var VARIABLE in jsonObject.Properties())
                    {
                        reDictionary.Add((itemval.Key + ":" + VARIABLE.Name), VARIABLE.Value);
                    }
                    string key = itemval.Key;
                    string value = itemval.Value.ToString();
                    if (!string.IsNullOrEmpty(value))
                    {
                        _dictionary.Add(key, value);
                        InitData(reDictionary);
                    }
                }
                if (itemval.Value.GetType().ToString().Equals("System.String"))
                {
                    string key = itemval.Key;
                    string value = itemval.Value.ToString();
                    if (!string.IsNullOrEmpty(value))
                    {
                        _dictionary.Add(key, value);
                    }
                }
                if (itemval.Value.GetType().ToString().Equals("Newtonsoft.Json.Linq.JValue"))
                {
                    string key = itemval.Key;
                    string value = itemval.Value.ToString();
                    if (!string.IsNullOrEmpty(value))
                    {
                        _dictionary.Add(key, value);
                    }
                    if (JsonHelper.IsJson(itemval.Value.ToString()))
                    {
                        var rejsonObjects = JsonConvert.DeserializeObject<Dictionary<string, Object>>(itemval.Value.ToString());
                        InitData(rejsonObjects);
                    }
                }
                if (itemval.Value.GetType().ToString().Equals("Newtonsoft.Json.Linq.JArray"))
                {
                    string key = itemval.Key;
                    string value = itemval.Value.ToString();
                    _dictionary.Add(key, value);
                }
            }
        }
        private void EntityChangeObserverChanged(object sender, EntityChangeEventArgs e)
        {
            if (e.EntityEntry.Entity.GetType() != typeof(DiyConfig))
            {
                return;
            }

            //在将更改保存到底层数据库之前,稍作延迟以避免触发重新加载
            Thread.Sleep(_source.ReloadDelay);
            EFConfigurationBuilder.CreateEFConfiguration();
        }
    }

使用方式

好的,代码也已经编辑好了,到底如何使用,效果是怎样的呢?

还记得我们最开始说的:不修改原始的IConfiguration读取方式的情况下创建自定义配置中心,故他的使用方式与原始的IConfiguration相差不大,只是加入了初始化步骤。

  1. 使用自定义的连接字符串,选择对应的数据库枚举。

  2. 调用初始化方法,返回IConfiguration

  3. 使用IConfiguration的GetSection(string key)方法,GetChildren()方法,GetReloadToken()方法去获取对应的值

            // 初始化之后返回 IConfiguration对象
            var configuration = EFConfigurationBuilder.Init(conn, DbType.MySql, out string erroMesg);
            // 使用GetSection方法获取对应的键值对
            var value = configuration.GetSection("Connection").Value;

我们测试使用一段复杂的json结构看能取到怎样的节点数据。

{
  "data": {
    "trainLines": [
      {
        "trainCode": "G6666",
        "fromStation": "衡山西",
        "toStation": "长沙南",
        "fromTime": "08:10",
        "toTime": "09:33",
        "fromDateTime": "2020-08-09 08:10",
        "toDateTime": "2020-08-09 09:33",
        "arrive_days": "0",
        "runTime": "01:23",
        "trainsType": 1,
        "trainsTypeName": "高铁",
        "beginStation": null,
        "beginTime": null,
        "endStation": null,
        "endTime": null,
        "Seats": [
          {
            "seatType": 4,
            "seatTypeName": "二等座",
            "ticketPrice": 75.0,
            "leftTicketNum": 20
          },
          {
            "seatType": 3,
            "seatTypeName": "一等座",
            "ticketPrice": 124.0,
            "leftTicketNum": 11
          },
          {
            "seatType": 1,
            "seatTypeName": "商务座",
            "ticketPrice": 231.0,
            "leftTicketNum": 3
          }
        ]
      }
    ]
  },
  "success": true,
  "msg": "请求成功"
}

我们将数据存到数据库中

通过调试查看数据

  • 可以看到我们首先通过传递连接字符串以及数据库类型初始化生成了IConfiguration,使用的是mysql数据库,切换数据库则只需要更换连接字符串和枚举即可,切换数据库实现。
  • 接着创建一个新的配置Key为diy,Value为testDiy的配置,短暂等待构造方法刷新IConfiguration之后,通过GetSection("diy")成功拿到了新的值,故热重载也成功实现!

参考资料

【源代码】https://gitee.com/yi_zihao/DiyEFConfiguration.git

【微软官网】ASP.NET Core 中的配置 https://mp.weixin.qq.com/s/lM808MxUu6tp8zU8SBu3sg

【艾心】.NET Core 3.0之深入源码理解Configuration https://www.cnblogs.com/edison0621/p/10854215.html

【ASP.NET Core3框架揭秘(上册)】

【开源项目】https://github.com/matjazbravc/Custom.ConfigurationProvider.Demo

【CYQ.DATA】json组件 https://www.cnblogs.com/cyq1162/p/5634414.html