• Asp.NetCore 从数据库加载配置(二)


            在第一节中,我们实现了基本的自定义数据库配置源,从而可以读取MySql数据库的配置,但是,我们没有实现动态加载数据库配置,也就是程序一但运行起来,数据库的配置更改后就不在被更新。所以本节重点来解决这个问题。


    1.基本操作

            我们知道在Option模式中,要想加载更新的配置,只需要两步:

    一是,添加配置的时候,将reloadChange属性设置为True;而是获取配置时,使用IOptionsSnapShot:

    1. WebHost.CreateDefaultBuilder(args)
    2. .ConfigureAppConfiguration((hostingContext, config) =>
    3. {
    4. config.SetBasePath(Directory.GetCurrentDirectory());
    5. config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
    6. config.AddJsonFile("appsettings.Development.json", optional: false, reloadOnChange: true);
    7. config.AddEnvironmentVariables();
    8. })

    IOptions是单例模式,所以第一次启动加载后,就不会再加载,而IOptionsSnapshot是Scope模式,每次加载时,都会重新读取一遍。

            但是我们怎么让IConfiguration对象重新读取数据库呢?我们查文档找到了一个方法:

    protected void OnReload ();

    官方解释是:Triggers the reload change token and creates a new one.

    也就是如果调用这个函数,整个配置树都会重新建立,这也就给了我们一种办法去动态加载。

    为了验证,我们用Controller做试验:

    承接(一)中的代码,我们在默认的WeatherForecastController下添加一个Action:

    1. [HttpGet,Route("ShowStudent")]
    2. public ActionResult<string> ShowStudent()
    3. {
    4. var configurationRoot = HttpContext.RequestServices.GetService() as IConfigurationRoot;
    5. if (null == configurationRoot)
    6. {
    7. return BadRequest();
    8. }
    9. configurationRoot.Reload();
    10. var stu = HttpContext.RequestServices.GetService>()?.Value;
    11. if(stu!=null)
    12. {
    13. return $"{stu.Name}---{stu.Age}";
    14. }else
    15. {
    16. return NotFound();
    17. }
    18. }

    运行,不关闭程序,然后改变数据库的Wang字段:

     再次执行,就会发现数据变成新修改的数据。

    上面的做法虽然可行,但是如果每次获取时都要手动刷新,无疑很繁琐,我们得找找更优雅的办法。


    二.思考

            基于前面的分析,当数据库的数据发生改变时,肯定要重新加载一般数据,这是无法避免的,简单点一般是全部加载,如果数据库有一些特定支持,也许可以实现加载变化的内容,这里我们还是简单一点,考虑到一般配置数据不大可能有上万条之多,也就是这点数据不造成性能问题。

            所以,第一步就是要能知道数据库中的数据发生变化,然后触发后续重载操作。

    在查看ConfigurationProvider类时,我们发现这两个函数成员,

    1. ///
    2. /// Returns a that can be used to listen when this provider is reloaded.
    3. ///
    4. /// The .
    5. public IChangeToken GetReloadToken()
    6. {
    7. return _reloadToken;
    8. }
    9. ///
    10. /// Triggers the reload change token and creates a new one.
    11. ///
    12. protected void OnReload()
    13. {
    14. ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
    15. previousToken.OnReload();
    16. }

    在OnReload接口中,会调用OnReload,其实就是触发cancel操作:

    1. ///
    2. /// Used to trigger the change token when a reload occurs.
    3. ///
    4. public void OnReload() => _cts.Cancel();

    也就是说,如果我们检测到数据变化,触发了Onload()函数,那么ConfigurationBuilder就会重载配置,也就达到我们的目的。

    三. 重构

            先给出EFConfigurationSource的代码,为了考虑通用性,我将配置源类改为泛型模式。

    1. public class EFConfigurationSource<TDbContext>: IConfigurationSource where TDbContext : DbContext
    2. {
    3. public readonly Action _optionsAction;
    4. public readonly bool _reloadOnChange;
    5. public readonly int _pollingInterval;
    6. public readonly Action>? OnLoadException;
    7. public EFConfigurationSource(Action optionsAction,
    8. bool reloadOnChange = false,
    9. int pollingInterval = 5000,
    10. Action>? onLoadException = null)
    11. {
    12. if (pollingInterval < 500)
    13. {
    14. throw new ArgumentException($"{nameof(pollingInterval)} can not less than 500.");
    15. }
    16. _optionsAction = optionsAction;
    17. _reloadOnChange = reloadOnChange;
    18. _pollingInterval = pollingInterval;
    19. OnLoadException = onLoadException;
    20. }
    21. public IConfigurationProvider Build(IConfigurationBuilder builder)
    22. {
    23. return new EFConfigurationProvider(this);
    24. }
    25. }

    新增了三个属性:

    1. _reloadChange: 是否开启热加载
    2. 数据库扫描时间间隔
    3. 异常处理

    因为我们要在循环中不停的加载数据库数据,因此可能会出现异常,我们自定了一个异常类,当然也是泛型的:

    1. public sealed class EFConfigurationLoadException<TDbContext> where TDbContext:DbContext
    2. {
    3. public Exception Exception { get; }
    4. public bool Ignorabel { get; set; }
    5. public EFConfigurationSource Source { get; }
    6. internal EFConfigurationLoadException(EFConfigurationSource source,Exception ex)
    7. {
    8. Source = source;
    9. Exception = ex;
    10. }
    11. }

    构造函数中,我们会对时间间隔进行判断,如果设置的间隔小于0.5s,则认为时间间隔过短。

    在Build函数中,我们将自身传递给了EFConfigurationProvider类。

    显然EFConfigurationSource没有太多要说的,核心实现还是在EFConfigurationProvider类:

    1. public class EFConfigurationProvider:ConfigurationProvider,IDisposable where TDbContext : DbContext
    2. {
    3. private readonly EFConfigurationSource _source;
    4. private readonly CancellationTokenSource _cancellationTokenSource;
    5. private byte[] _lastComputeHash;
    6. private Task? _watchDbTask;
    7. private bool _disposed;
    8. public EFConfigurationProvider(EFConfigurationSource configurationSource)
    9. {
    10. _source = configurationSource;
    11. _cancellationTokenSource = new CancellationTokenSource();
    12. _lastComputeHash = new byte[20];
    13. }
    14. public override void Load()
    15. {
    16. if(_watchDbTask != null)
    17. {
    18. return;
    19. }
    20. try
    21. {
    22. Data = GetData();
    23. _lastComputeHash = ComputeHash(Data);
    24. }
    25. catch(Exception ex)
    26. {
    27. var exception = new EFConfigurationLoadException(_source, ex);
    28. _source.OnLoadException?.Invoke(exception);
    29. if(!exception.Ignorabel)
    30. {
    31. throw;
    32. }
    33. }
    34. var cancellationToken= _cancellationTokenSource.Token;
    35. if(_source._reloadOnChange)
    36. {
    37. _watchDbTask = Task.Run(() => WatchDatabase(cancellationToken), cancellationToken);
    38. }
    39. }
    40. public void Dispose()
    41. {
    42. if(_disposed)
    43. {
    44. return;
    45. }
    46. _cancellationTokenSource.Cancel();
    47. _cancellationTokenSource.Dispose();
    48. _disposed = true;
    49. }
    50. }

    EFConfigurationProvider的主要实现如上,其中属性分别代表:

    1. _source:配置源,提供一些参数,包括数据库的配置
    2. _lastComputeHash:用来保存数据库字段的哈希值,以此判断两次读取是否一致
    3. _watchDbTask:监视任务
    4. _disposed:回收

    不用看构造函数,直接看Load函数:

    如果_watchDbTask不为空,则说明数据已经在监视中,直接返回;第一次调用,时就会调用WatchDataBase()函数,,启动监视。我们再看看这个函数:

    1. private async Task WatchDatabase(CancellationToken cancellationToken)
    2. {
    3. while(!cancellationToken.IsCancellationRequested)
    4. {
    5. try
    6. {
    7. await Task.Delay(_source._pollingInterval, cancellationToken);
    8. IDictionary actualData = await GetDataAsync();
    9. byte[] computedHash=ComputeHash(actualData);
    10. if(!computedHash.SequenceEqual(_lastComputeHash))
    11. {
    12. Data = actualData;
    13. OnReload();
    14. }
    15. _lastComputeHash = computedHash;
    16. }
    17. catch (Exception ex)
    18. {
    19. var exception = new EFConfigurationLoadException(_source, ex);
    20. _source.OnLoadException?.Invoke(exception);
    21. if(!exception.Ignorabel)
    22. {
    23. throw;
    24. }
    25. }
    26. }
    27. }

    我们会在循环中不停的读取数据库,时间间隔来自于_Source传递的参数,然后将读取的字典类型转化为字节,计算其hash值,进行对比,如果不同,则更新hash值和数据Data,并同时触发OnReload函数。如果出现异常,则根据传入的异常处理。

    1. public async Taskstring, string>> GetDataAsync()
    2. {
    3. using TDbContext dbContext=CreateDbContext();
    4. IQueryable entries=dbContext.Set();
    5. IDictionary<string, string> dict = entries.Any() ? await entries.ToDictionaryAsync(c => c.Key, c => c.Value) :
    6. new Dictionary<string, string>();
    7. return dict;
    8. }
    9. private TDbContext CreateDbContext()
    10. {
    11. DbContextOptionsBuilder builder = new DbContextOptionsBuilder();
    12. _source._optionsAction(builder);
    13. return (TDbContext)Activator.CreateInstance(typeof(TDbContext), new object[] { builder.Options })!;
    14. }
    15. private byte[] ComputeHash(IDictionary<string,string> dict)
    16. {
    17. List<byte> byteDict = new List<byte>();
    18. foreach(var kvp in dict)
    19. {
    20. byteDict.AddRange(Encoding.Unicode.GetBytes($"{kvp.Key}{kvp.Value}"));
    21. }
    22. return System.Security.Cryptography.SHA1.Create().ComputeHash(byteDict.ToArray());
    23. }

    最后我们编写一个扩展方法,方方便服务加载配置源:

    1. public static class ConfigurationBuilderExtension
    2. {
    3. ///
    4. ///
    5. ///
    6. /// DbContext type that contains setting values.
    7. /// The Microsoft.Extensions.Configuration.IConfigurationBuilder to add to.
    8. /// DbContextOptionsBuilder used to create related DbContext.
    9. ///
    10. ///
    11. ///
    12. ///
    13. public static IConfigurationBuilder AddEfConfiguration(this IConfigurationBuilder configurationBuilder,
    14. Action optionsAction,
    15. bool reloadOnChange=false,
    16. int pollingInterval=5000,
    17. Action>? onLoadException =null) where TDbContext:DbContext
    18. {
    19. return configurationBuilder.Add(new EFConfigurationSource(optionsAction,
    20. reloadOnChange, pollingInterval, onLoadException));
    21. }
    22. }

    然后在Main函数中调用:

    1. var builder = WebApplication.CreateBuilder(args);
    2. var ConnectionString = builder.Configuration.GetConnectionString("MySql");
    3. builder.Host.ConfigureAppConfiguration((_, configBuilder) =>
    4. {
    5. //var config = configBuilder.Build();
    6. //var configSource = new EFConfigurationSource(opts =>
    7. //opts.UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString)));
    8. //configBuilder.Add(configSource);
    9. configBuilder.Sources.Clear();
    10. configBuilder.AddEfConfiguration(
    11. opts => opts.UseMySql(ConnectionString, ServerVersion.AutoDetect(ConnectionString)), reloadOnChange: true);
    12. foreach(var (k,v) in configBuilder.Build().AsEnumerable().Where(t=>t.Value is not null))
    13. {
    14. Console.WriteLine($"{k}={v}");
    15. }
    16. });

    同样你在后台更改数据后,就可以发现,不用调用之前的configurationRoot.Reload();就能同步更新。

    自此,我们算是较好的实现了同步加载数据库配置的需求,实际上还有一些工作可以做:

    • 支持数据库中不同格式的配置
    • 支持跨应用更新,通过添加新的字段可以实现
    • 监视函数改用Timer来简化

    本章在重点参考了:Implement a complete custom configuration provider in .NET

    完整代码在:FrameWorks/ConfigurationFromDb

  • 相关阅读:
    安全开发之碰撞检测与伤害计算逻辑
    CSS - 常用属性和布局方式
    蓝桥杯练习题十 - 煤球数目(c++)
    系列十二、强引用、软引用、弱引用、虚引用分别是什么?
    亚马逊六页纸沟通法,拒绝PPT
    C++类与对象(3)—拷贝构造函数&运算符重载
    Vue.js 框架源码与进阶 - 封装 Vue.js 组件库
    MATLAB:电机控制(Motor Control)
    NeuN抗体丨SYSY NeuN抗体说明书及相关研究工具
    docker 构建并运行 python项目
  • 原文地址:https://blog.csdn.net/q__y__L/article/details/127653213