迁移appseting.json创建自定义配置中心
创建一个自定义的配置中心,将框架中各类配置,迁移至数据库,支持切换数据库,热重载。
说在前面的话
自使用.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相差不大,只是加入了初始化步骤。
- 使用自定义的连接字符串,选择对应的数据库枚举。
- 调用初始化方法,返回IConfiguration
- 使用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创建自定义配置中心的更多相关文章
- 使用 ConfigurationSection 创建自定义配置节
我们可以通过用自己的 XML 配置元素来扩展标准的 ASP.NET 配置设置集,要完成这一功能,我们必须实现继承System.Configuration.ConfigurationSection 类来 ...
- C#创建自定义配置节
在.Net应用程序中,我们经常看到VS为我们生成的项目工程中都会含有nfig或者nfig这样的文件.这个文件就是我们所说的应用程序配置文件.在这个文件里面记述着一些与我们的应用程序相关的信息,如:数据 ...
- C#创建自定义配置节点
转载:http://www.educity.cn/develop/495003.html 在.Net应用程序中我们经常看到VS为我们生成的项目工程中都会含有app.config或者web.connfi ...
- 携程Apollo(阿波罗)配置中心Spring Boot迁移日志组件,使用配置中心进行管理的思路
说明: 1.Spring Boot项目默认使用logback进行日志管理 2.logback在启动时默认会自动检查是否有logback.xml文件,如果有时会有限加载这个文件. 3.那么如果是用配置中 ...
- C# 创建自定义配置节点1
转载:http://www.educity.cn/develop/495003.html 在.Net应用程序中我们经常看到VS为我们生成的项目工程中都会含有app.config或者web.connfi ...
- Angular4项目,默认的package.json创建及配置
1.使用如下命令,可以创建一个默认的 package.json npm init 创建后如下图所示: 添加 angular4 的 dependencies: npm install@ ...
- ASP.NET使用ConfigurationSection在Web.Config创建自定义配置节集合
核心代码 using System; using System.Data; using System.Configuration; using System.Web; using System.Web ...
- ASP.NET使用ConfigurationSection在Web.Config创建自定义配置节
主要代码,一定要继续System.Configuration.ConfigurationSection,具体的节点名称可以自行修改 using System; using System.Data; u ...
- 创建客户端项目并读取服务化的配置中心(Consul + Spring Cloud Config)
创建客户端项目并读取服务化的配置中心 将配置中心注册到服务中心(Consul) POM文件添加依赖: <dependency> <groupId>org.springframe ...
随机推荐
- Linkerd 2.10(Step by Step)—配置超时
Linkerd 2.10 系列 快速上手 Linkerd v2 Service Mesh(服务网格) 腾讯云 K8S 集群实战 Service Mesh-Linkerd2 & Traefik2 ...
- 前端性能优化之http缓存
前不久,公司前端开会,领导抽问了4个问题,前3个简单大家都答起来了,第4个问题关于缓存的这方面我只是了解,结果刚好问到我了(会的不问,专门挑我不熟悉的问,我这运气真是没话说),20多个前端看着我,答得 ...
- Java线程池工作原理
前言 当项目中有频繁创建线程的场景时,往往会用到线程池来提高效率.所以,线程池在项目开发过程中的出场率是很高的. 那线程池是怎么工作的呢?它什么时候创建线程对象,如何保证线程安全... 什么时候创建线 ...
- 重启网络服务 network 出现问题
2021-08-24 地址冲突了,因为想要设置成静态 ip 一直都不对,情急之下就将本地 ip 设置成了虚拟机的 ip,故出现此错误 后将地址改掉,重启网络服务就没有错误了 一开始我设置的虚拟网卡 n ...
- K8S日常运维中关于“ImagePullBackOff”报错的处理思路分析
故障案例: 发现故障:kubectl get pod -n kube-system -owide|grep -v "Running"NAME READY STATUS RESTAR ...
- Redis++:Redis 内存爆满 之 淘汰策略
前言: 我们的redis使用的是内存空间来存储数据的,但是内存空间毕竟有限,随着我们存储数据的不断增长,当超过了我们的内存大小时,即在redis中设置的缓存大小(maxmeory 4GB),redis ...
- NumPy的基本操作
1 简介 NumPy 是用于处理数组的 python 库,部分用 Python 编写,但是大多数需要快速计算的部分都是用 C 或 C ++ 编写的.它还拥有在线性代数.傅立叶变换和矩阵领域中工作的函数 ...
- sql常用查询命令
目录 SQL Server常用查询命令: 查看当前时间 查询所有数据库名 查询当前使用的数据库名 查询前几条数据 去重查询 字段换名 查询不等于 查询在两个值之间数据 查询条件或 模糊匹配查询 查询为 ...
- Docker(36)- docker run 的流程和原理
背景 目前项目组上, Docker 用的非常重,所有微服务都是通过 docker 来部署的 所以不能仅仅会命令,还得会一些原理的东西,特此补一篇基础点的,后面再更加深入一些 docker 原理 本篇学 ...
- Vue设置全局js/css样式
''' 配置全局js mian.js: import settings from '@/assets/js/settings' Vue.prototype.$settings = settings; ...