Link: cnblogs.com/OrcCoCo/p/15399228.html
preface
Create a user-defined configuration center, migrate various configurations in the framework to the database, and support database switching and hot overloading.
Since using the. NET Core framework, most of the configurations have been stored in json files:
- [framework default loading configuration] files are appsetting.json and ppsettings.Environment.json,
- [environment variable] exists in aunchSettings.json,
- [user secret] exists% appdata% \ Microsoft \ usersecrets < user_ secrets_ id>\secrets.json
- They can all read and edit in the way provided by the. NET Core framework, such as IConfiguration.
The text discusses the creation of a custom configuration center, mainly to migrate appsetting.json to the database without changing the reading method. According to the previous practice, we can use WebHost.ConfigureAppConfiguration in program.cs to read the data of the database, and then fill it into the configuration, as shown in the following figure:

There are two problems with this
- The configuration is added in the create host configuration CreateHostBuilder() method of the program entry, so it cannot be built again. Unless the web restarts, the configuration in the database cannot be hot overloaded after being modified,
- SqLite is used to implement it here. Assuming that the database is changed to implement it in the framework, it is unrealistic and really not elegant to modify the code in Program.cs.
Therefore, the author creates a custom configuration center with EFCore as the configuration source to solve the above two problems, and encapsulates it into a class library, which can be applied to multiple scenarios.
Database switching
To solve the problem of database switching, the first thing is to extract the configuration construction from the Program class and rebuild a class to create the IConfiguration used in the configuration. Therefore, I write the initial configuration in the static method, build different contexts by passing the connection string and database class type, and throw an exception in case of error.
public class EFConfigurationBuilder { /// <summary> ///Configured IConfiguration /// </summary> public static IConfiguration EFConfiguration { get; set; } /// <summary> ///Connection string /// </summary> public static string ConnectionStr { get; set; } /// <summary> ///Initialization /// </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 = "Please check the connection string and database type"; 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; } } } // Call initialization method var configuration = EFConfigurationBuilder.Init(conn, DbType.MySql, out string erroMesg);
Realize thermal overload
Thermal overloading can be achieved by using the builder observer pattern.
In fact, database switching also gives us a hot overload solution. We can expose the construction method and dynamically refresh the IConfiguration of the construction class. If it is in a console application or other non Web projects, there may be no appsetting.json file, so we make a slight judgment
/// <summary> ///Create a new 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; }
The build method is ready, so where to call this method? Here, you can use the observer mode to monitor the change events of the configuration entity. If there are modifications, you can call a build method to override the IConfiguration of the configuration center. The easiest way to implement this is to add entity monitoring after 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> ///Entity monitoring /// </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; } } }
Not to mention the code
Before coding, you need to supplement some knowledge,

This mind map is compiled by the boss of Ai Xin after reading the source code. From the code level, our configuration information will be converted into an IConfiguration object for use by the program. IConfigurationBuilder is the builder of IConfiguration object, and IConfigurationSource is the most original source of various configuration data, We only need to customize the lowest IConfigurationProvider and provide the data of key value pair type to IConfigurationSource to realize the custom configuration center. It's hard to say. We can directly upload the UML diagram, which is derived from the [disclosure of ASP.NET Core3 framework (Volume I)].

If you don't like the source code, you can directly jump to - [how to use]
ConfigurationBuilder
public class EFConfigurationBuilder { /// <summary> ///Create configuration /// </summary> /// <param name="diyConfig"></param> /// <returns></returns> public static (bool, string) CreateConfig(DiyConfig diyConfig) { if (DbContext == null) { return (false, "Context not initialized, please check!"); } if (diyConfig == null && DbContext.DiyConfigs.Any(x => x.Id.Equals(diyConfig.Id))) { return (false, "Error in incoming parameters, please check!"); } if (DbContext.DiyConfigs.Any(x => x.Key.Equals(diyConfig.Key))) { return (false, "DB—There is already a corresponding key value pair"); } DbContext.DiyConfigs.Add(diyConfig); if (DbContext.SaveChanges() > 0) { return (true, "success"); } else { return (false, "Failed to create configuration"); } } /// <summary> ///Create configuration /// </summary> /// <param name="diyConfig"></param> /// <returns></returns> public static async Task<(bool, string)> CreateConfigAsync(DiyConfig diyConfig) { ... } /// <summary> ///Delete configuration /// </summary> /// <param name="diyConfig"></param> /// <returns></returns> public static async Task<(bool, string)> DleteConfigAsync(DiyConfig diyConfig) { if (DbContext == null) { return (false, "Context not initialized, please check!"); } if (diyConfig == null && !DbContext.DiyConfigs.Any(x => x.Id.Equals(diyConfig.Id))) { return (false, "Error in incoming parameters, please check!"); } DbContext.DiyConfigs.Remove(diyConfig); if (await DbContext.SaveChangesAsync() > 0) { return (true, "success"); } else { return (false, "Failed to update configuration"); } } /// <summary> ///Delete configuration /// </summary> /// <param name="diyConfig"></param> /// <returns></returns> public static (bool, string) DleteConfig(DiyConfig diyConfig) { ... } /// <summary> ///Update configuration /// </summary> /// <param name="diyConfig"></param> /// <returns></returns> public (bool, string) UpdateConfig(DiyConfig diyConfig) { try { if (DbContext == null) { return (false, "Context not initialized, please check!"); } if (diyConfig == null && !DbContext.DiyConfigs.Any(x => x.Id.Equals(diyConfig.Id))) { return (false, "Error in incoming parameters, please check!"); } DbContext.DiyConfigs.Update(diyConfig); if (DbContext.SaveChanges() > 0) { return (true, "success"); } else { return (false, "Failed to update configuration"); } } catch (Exception ex) { return (false, $"Failed to update configuration,error:{ex.Message}"); } } /// <summary> ///Update configuration /// </summary> /// <param name="diyConfig"></param> /// <returns></returns> public async Task<(bool, string)> UpdateConfigAsync(DiyConfig diyConfig) { ... } /// <summary> ///Initialization /// </summary> ///< param name = "connection" > connection string < / param > ///< param name = "dbtype" > database type < / param > ///< param name = "erromesg" > error message < / param > ///< param name = "version" > database version < / param > /// <returns>IConfiguration</returns> public static IConfiguration Init(string connetcion, DbType dbType, out string erroMesg, string version = "5.7.28-mysql") { ... } /// <summary> ///Create a new 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() is a new EF core method to ensure the existence of the context database. If so, no action is taken. If it does not exist, create the database and all its schemas and ensure that it is compatible with the model in this context 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; } //Before saving changes to the underlying database, delay slightly to avoid triggering a reload Thread.Sleep(_source.ReloadDelay); EFConfigurationBuilder.CreateEFConfiguration(); } }
Mode of use
OK, the code has been edited. How to use it and what is the effect? Remember what we said at the beginning: the user-defined configuration center is created without modifying the original IConfiguration reading method, so its use method is not different from the original IConfiguration, but the initialization step is added.
- Use a custom connection string to select the corresponding database enumeration.
- Call the initialization method and return IConfiguration
- Use the GetSection(string key) method, GetChildren() method and GetReloadToken() method of IConfiguration to obtain the corresponding value
// Return IConfiguration object after initialization var configuration = EFConfigurationBuilder.Init(conn, DbType.MySql, out string erroMesg); // Use the GetSection method to obtain the corresponding key value pair var value = configuration.GetSection("Connection").Value;
We test using a complex json structure to see what kind of node data we can get.
{ "data": { "trainLines": [ { "trainCode": "G6666", "fromStation": "Hengshan West", "toStation": "Changsha South", "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": "high-speed rail", "beginStation": null, "beginTime": null, "endStation": null, "endTime": null, "Seats": [ { "seatType": 4, "seatTypeName": "second-class seat", "ticketPrice": 75.0, "leftTicketNum": 20 }, { "seatType": 3, "seatTypeName": "First class seat", "ticketPrice": 124.0, "leftTicketNum": 11 }, { "seatType": 1, "seatTypeName": "Business seat", "ticketPrice": 231.0, "leftTicketNum": 3 } ] } ] }, "success": true, "msg": "Request succeeded" }
We store the data in the database

View data through debugging

Implementation of hot overload of configuration center and database switching
- You can see that we first generate IConfiguration by passing the connection string and database type initialization. We use mysql database. To switch the database, we only need to change the connection string and enumeration to switch the database.
- Then create a new configuration with Key diy and Value testDiy. After briefly waiting for the constructor to refresh IConfiguration, the new Value is successfully obtained through GetSection ("diy"), so the hot overload is also successfully implemented!