在第一节中,我们实现了基本的自定义数据库配置源,从而可以读取MySql数据库的配置,但是,我们没有实现动态加载数据库配置,也就是程序一但运行起来,数据库的配置更改后就不在被更新。所以本节重点来解决这个问题。
我们知道在Option模式中,要想加载更新的配置,只需要两步:
一是,添加配置的时候,将reloadChange属性设置为True;而是获取配置时,使用IOptionsSnapShot
- WebHost.CreateDefaultBuilder(args)
- .ConfigureAppConfiguration((hostingContext, config) =>
- {
- config.SetBasePath(Directory.GetCurrentDirectory());
- config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
- config.AddJsonFile("appsettings.Development.json", optional: false, reloadOnChange: true);
- config.AddEnvironmentVariables();
- })
IOptionsIOptionsSnapshot是Scope模式,每次加载时,都会重新读取一遍。
但是我们怎么让IConfiguration对象重新读取数据库呢?我们查文档找到了一个方法:
protected void OnReload ();
官方解释是:Triggers the reload change token and creates a new one.
也就是如果调用这个函数,整个配置树都会重新建立,这也就给了我们一种办法去动态加载。
为了验证,我们用Controller做试验:
承接(一)中的代码,我们在默认的WeatherForecastController下添加一个Action:
- [HttpGet,Route("ShowStudent")]
- public ActionResult<string> ShowStudent()
- {
- var configurationRoot = HttpContext.RequestServices.GetService
() as IConfigurationRoot; - if (null == configurationRoot)
- {
- return BadRequest();
- }
- configurationRoot.Reload();
- var stu = HttpContext.RequestServices.GetService
>()?.Value; - if(stu!=null)
- {
- return $"{stu.Name}---{stu.Age}";
- }else
- {
- return NotFound();
- }
- }
运行,不关闭程序,然后改变数据库的Wang字段:

再次执行,就会发现数据变成新修改的数据。
上面的做法虽然可行,但是如果每次获取时都要手动刷新,无疑很繁琐,我们得找找更优雅的办法。
基于前面的分析,当数据库的数据发生改变时,肯定要重新加载一般数据,这是无法避免的,简单点一般是全部加载,如果数据库有一些特定支持,也许可以实现加载变化的内容,这里我们还是简单一点,考虑到一般配置数据不大可能有上万条之多,也就是这点数据不造成性能问题。
所以,第一步就是要能知道数据库中的数据发生变化,然后触发后续重载操作。
在查看ConfigurationProvider类时,我们发现这两个函数成员,
- ///
- /// Returns a
that can be used to listen when this provider is reloaded. - ///
- ///
The . - public IChangeToken GetReloadToken()
- {
- return _reloadToken;
- }
-
- ///
- /// Triggers the reload change token and creates a new one.
- ///
- protected void OnReload()
- {
- ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
- previousToken.OnReload();
- }
在OnReload接口中,会调用OnReload,其实就是触发cancel操作:
- ///
- /// Used to trigger the change token when a reload occurs.
- ///
- public void OnReload() => _cts.Cancel();
也就是说,如果我们检测到数据变化,触发了Onload()函数,那么ConfigurationBuilder就会重载配置,也就达到我们的目的。
先给出EFConfigurationSource
- public class EFConfigurationSource<TDbContext>: IConfigurationSource where TDbContext : DbContext
- {
- public readonly Action
_optionsAction; - public readonly bool _reloadOnChange;
- public readonly int _pollingInterval;
- public readonly Action
>? OnLoadException; - public EFConfigurationSource(Action
optionsAction, - bool reloadOnChange = false,
- int pollingInterval = 5000,
- Action
>? onLoadException = null ) - {
- if (pollingInterval < 500)
- {
- throw new ArgumentException($"{nameof(pollingInterval)} can not less than 500.");
- }
- _optionsAction = optionsAction;
- _reloadOnChange = reloadOnChange;
- _pollingInterval = pollingInterval;
- OnLoadException = onLoadException;
- }
- public IConfigurationProvider Build(IConfigurationBuilder builder)
- {
- return new EFConfigurationProvider
(this); - }
- }
新增了三个属性:
因为我们要在循环中不停的加载数据库数据,因此可能会出现异常,我们自定了一个异常类,当然也是泛型的:
- public sealed class EFConfigurationLoadException<TDbContext> where TDbContext:DbContext
- {
- public Exception Exception { get; }
- public bool Ignorabel { get; set; }
- public EFConfigurationSource
Source { get; } - internal EFConfigurationLoadException(EFConfigurationSource
source,Exception ex ) - {
- Source = source;
- Exception = ex;
- }
- }
构造函数中,我们会对时间间隔进行判断,如果设置的间隔小于0.5s,则认为时间间隔过短。
在Build函数中,我们将自身传递给了EFConfigurationProvider类。
显然EFConfigurationSource没有太多要说的,核心实现还是在EFConfigurationProvider类:
- public class EFConfigurationProvider
:ConfigurationProvider,IDisposable where TDbContext : DbContext - {
- private readonly EFConfigurationSource
_source; - private readonly CancellationTokenSource _cancellationTokenSource;
- private byte[] _lastComputeHash;
- private Task? _watchDbTask;
- private bool _disposed;
-
- public EFConfigurationProvider(EFConfigurationSource
configurationSource) - {
- _source = configurationSource;
- _cancellationTokenSource = new CancellationTokenSource();
- _lastComputeHash = new byte[20];
- }
- public override void Load()
- {
- if(_watchDbTask != null)
- {
- return;
- }
- try
- {
- Data = GetData();
- _lastComputeHash = ComputeHash(Data);
- }
- catch(Exception ex)
- {
- var exception = new EFConfigurationLoadException
(_source, ex); - _source.OnLoadException?.Invoke(exception);
- if(!exception.Ignorabel)
- {
- throw;
- }
- }
- var cancellationToken= _cancellationTokenSource.Token;
- if(_source._reloadOnChange)
- {
- _watchDbTask = Task.Run(() => WatchDatabase(cancellationToken), cancellationToken);
- }
- }
- public void Dispose()
- {
- if(_disposed)
- {
- return;
- }
- _cancellationTokenSource.Cancel();
- _cancellationTokenSource.Dispose();
- _disposed = true;
- }
- }
EFConfigurationProvider的主要实现如上,其中属性分别代表:
不用看构造函数,直接看Load函数:
如果_watchDbTask不为空,则说明数据已经在监视中,直接返回;第一次调用,时就会调用WatchDataBase()函数,,启动监视。我们再看看这个函数:
- private async Task WatchDatabase(CancellationToken cancellationToken)
- {
- while(!cancellationToken.IsCancellationRequested)
- {
- try
- {
- await Task.Delay(_source._pollingInterval, cancellationToken);
- IDictionary
actualData = await GetDataAsync(); - byte[] computedHash=ComputeHash(actualData);
- if(!computedHash.SequenceEqual(_lastComputeHash))
- {
- Data = actualData;
- OnReload();
- }
- _lastComputeHash = computedHash;
- }
- catch (Exception ex)
- {
- var exception = new EFConfigurationLoadException
(_source, ex); - _source.OnLoadException?.Invoke(exception);
- if(!exception.Ignorabel)
- {
- throw;
- }
- }
- }
- }
我们会在循环中不停的读取数据库,时间间隔来自于_Source传递的参数,然后将读取的字典类型转化为字节,计算其hash值,进行对比,如果不同,则更新hash值和数据Data,并同时触发OnReload函数。如果出现异常,则根据传入的异常处理。
- public async Task
string, string>> GetDataAsync() - {
- using TDbContext dbContext=CreateDbContext();
- IQueryable
entries=dbContext.Set(); - IDictionary<string, string> dict = entries.Any() ? await entries.ToDictionaryAsync(c => c.Key, c => c.Value) :
- new Dictionary<string, string>();
- return dict;
- }
- private TDbContext CreateDbContext()
- {
- DbContextOptionsBuilder
builder = new DbContextOptionsBuilder(); - _source._optionsAction(builder);
- return (TDbContext)Activator.CreateInstance(typeof(TDbContext), new object[] { builder.Options })!;
- }
- private byte[] ComputeHash(IDictionary<string,string> dict)
- {
- List<byte> byteDict = new List<byte>();
- foreach(var kvp in dict)
- {
- byteDict.AddRange(Encoding.Unicode.GetBytes($"{kvp.Key}{kvp.Value}"));
- }
- return System.Security.Cryptography.SHA1.Create().ComputeHash(byteDict.ToArray());
- }
最后我们编写一个扩展方法,方方便服务加载配置源:
- public static class ConfigurationBuilderExtension
- {
- ///
- ///
- ///
- ///
DbContext type that contains setting values. - /// The Microsoft.Extensions.Configuration.IConfigurationBuilder to add to.
- /// DbContextOptionsBuilder used to create related DbContext.
- ///
- ///
- ///
- ///
- public static IConfigurationBuilder AddEfConfiguration
(this IConfigurationBuilder configurationBuilder, - Action
optionsAction, - bool reloadOnChange=false,
- int pollingInterval=5000,
- Action
>? onLoadException =null) where TDbContext:DbContext - {
- return configurationBuilder.Add(new EFConfigurationSource
(optionsAction, - reloadOnChange, pollingInterval, onLoadException));
- }
- }
然后在Main函数中调用:
- var builder = WebApplication.CreateBuilder(args);
- var ConnectionString = builder.Configuration.GetConnectionString("MySql");
- builder.Host.ConfigureAppConfiguration((_, configBuilder) =>
- {
- //var config = configBuilder.Build();
- //var configSource = new EFConfigurationSource(opts =>
- //opts.UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString)));
- //configBuilder.Add(configSource);
-
- configBuilder.Sources.Clear();
- configBuilder.AddEfConfiguration
( - opts => opts.UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString)), reloadOnChange: true);
- foreach(var (k,v) in configBuilder.Build().AsEnumerable().Where(t=>t.Value is not null))
- {
- Console.WriteLine($"{k}={v}");
- }
- });
同样你在后台更改数据后,就可以发现,不用调用之前的configurationRoot.Reload();就能同步更新。
自此,我们算是较好的实现了同步加载数据库配置的需求,实际上还有一些工作可以做:
本章在重点参考了:Implement a complete custom configuration provider in .NET