在第一节中,我们实现了基本的自定义数据库配置源,从而可以读取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 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: 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 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> GetDataAsync()
{
using TDbContext dbContext=CreateDbContext();
IQueryable entries=dbContext.Set();
IDictionary dict = entries.Any() ? await entries.ToDictionaryAsync(c => c.Key, c => c.Value) :
new Dictionary();
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 dict)
{
List byteDict = new List();
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
完整代码在:FrameWorks/ConfigurationFromDb