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

说在前面的话

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

  • 【框架默认加载配置】文件为appseting.json 以及ppsettings.Environment.json,
  • 【环境变量】存在aunchSettings.json 中,
  • 【用户机密】则存在%APPDATA%\Microsoft\UserSecrets<user_secrets_id>\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框架揭秘(上册)】。

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

ConfigurationBuilder

    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()
{
...
}
}

EFConfigurationSource

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

EFConfigurationProvider

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

迁移appseting.json创建自定义配置中心的更多相关文章

  1. 使用 ConfigurationSection 创建自定义配置节

    我们可以通过用自己的 XML 配置元素来扩展标准的 ASP.NET 配置设置集,要完成这一功能,我们必须实现继承System.Configuration.ConfigurationSection 类来 ...

  2. C#创建自定义配置节

    在.Net应用程序中,我们经常看到VS为我们生成的项目工程中都会含有nfig或者nfig这样的文件.这个文件就是我们所说的应用程序配置文件.在这个文件里面记述着一些与我们的应用程序相关的信息,如:数据 ...

  3. C#创建自定义配置节点

    转载:http://www.educity.cn/develop/495003.html 在.Net应用程序中我们经常看到VS为我们生成的项目工程中都会含有app.config或者web.connfi ...

  4. 携程Apollo(阿波罗)配置中心Spring Boot迁移日志组件,使用配置中心进行管理的思路

    说明: 1.Spring Boot项目默认使用logback进行日志管理 2.logback在启动时默认会自动检查是否有logback.xml文件,如果有时会有限加载这个文件. 3.那么如果是用配置中 ...

  5. C# 创建自定义配置节点1

    转载:http://www.educity.cn/develop/495003.html 在.Net应用程序中我们经常看到VS为我们生成的项目工程中都会含有app.config或者web.connfi ...

  6. Angular4项目,默认的package.json创建及配置

        1.使用如下命令,可以创建一个默认的 package.json npm  init     创建后如下图所示: 添加 angular4 的 dependencies: npm install@ ...

  7. ASP.NET使用ConfigurationSection在Web.Config创建自定义配置节集合

    核心代码 using System; using System.Data; using System.Configuration; using System.Web; using System.Web ...

  8. ASP.NET使用ConfigurationSection在Web.Config创建自定义配置节

    主要代码,一定要继续System.Configuration.ConfigurationSection,具体的节点名称可以自行修改 using System; using System.Data; u ...

  9. 创建客户端项目并读取服务化的配置中心(Consul + Spring Cloud Config)

    创建客户端项目并读取服务化的配置中心 将配置中心注册到服务中心(Consul) POM文件添加依赖: <dependency> <groupId>org.springframe ...

随机推荐

  1. spring-data-redis 连接泄漏,我 TM 人傻了

    本系列是 我TM人傻了 系列第四期[捂脸],往期精彩回顾: 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了 这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了 获取异 ...

  2. rabbitMq可靠消息投递之交换机备份

    //备份队列 @Bean("alternate_queue") public Queue alternate_queue() { return new Queue("al ...

  3. promise链式调用的应用

    then在链式调用时,会等前一个then或者函数执行完毕,返回状态,才会执行回调函数. (1)代码顺序执行,第一步调用了函数cook ,cook执行返回了一个promise,promise返回的是成功 ...

  4. Linux基础——安装以及常用命令

    Linux基础--常用命令 1.安装Vmware 进入VMware官网:  https://www.vmware.com/cn.html下载安装 镜像推荐网址下载:https://www.linux. ...

  5. eh-admin一套前后端一体的轻量级后台管理系统

    https://gitee.com/DawnYang/eh-admin 主要技术 后端技术:Spring Boot,Apache Shiro,MyBatis-Plus等: 前端技术:Jquery,La ...

  6. 初步测试VS2017+Win10IoT,基于World Map of Makers

    测试World Map of Makers Demo,创建一个简单的internet连接应用程序来查询web API的状态(目测是一个延时时间),并使用该信息切换LED. 官方仓库的Solution是 ...

  7. 两种github action 打包.Net Core 项目docker镜像推送到阿里云镜像仓库

    两种github action 打包.Net Core 项目docker镜像推送到阿里云镜像仓库 1.GitHub Actions 是什么? 大家知道,持续集成由很多操作组成,比如抓取代码.运行测试. ...

  8. zigzag走线原理及应用

    电路板上弯弯扭扭的走线有什么用 往期文章: 一文读懂高速互联的阻抗及反射(上) 一文读懂高速互联的阻抗及反射(中) 前面几篇文章有部分读者反馈太深奥,不好懂,要求来一点轻松易懂的.这不,它来了!本期文 ...

  9. PyQT5基础布局管理

    绝对定位布局 使用move(x, y)可以对窗口进行布局,以窗口左上角为原点,向右为 x 轴正方向,向下为 y 轴正方向,移动(x,y); import sys from PyQt5.QtGui im ...

  10. Selenium系列(22) - 通过selenium控制浏览器滚动条的几种方式

    如果你还想从头学起Selenium,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1680176.html 其次,如果你不懂前端基础知识, ...