創建一個自定義的配置中心,將框架中各類配置,遷移至數據庫,支持切換數據庫,熱重載。
說在前面的話
自使用.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
