1. 程式人生 > 其它 >遷移appseting.json建立自定義配置中心

遷移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相差不大,只是加入了初始化步驟。

  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