Asp.NetCore 从数据库加载配置(二)-程序员宅基地

技术标签: C#  微服务  EentityFramwork  数据库  

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


1.基本操作

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

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

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();
})

IOptions<T>是单例模式,所以第一次启动加载后,就不会再加载,而IOptionsSnapshot是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<IConfiguration>() as IConfigurationRoot;
            if (null == configurationRoot)
            {
                return BadRequest();
            }
            configurationRoot.Reload();
            var stu = HttpContext.RequestServices.GetService<IOptionsSnapshot<Student>>()?.Value;
            if(stu!=null)
            {
                return $"{stu.Name}---{stu.Age}";
            }else
            {
                return NotFound();
            }
        }

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

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

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


二.思考

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

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

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

  /// <summary>
        /// Returns a <see cref="IChangeToken"/> that can be used to listen when this provider is reloaded.
        /// </summary>
        /// <returns>The <see cref="IChangeToken"/>.</returns>
        public IChangeToken GetReloadToken()
        {
            return _reloadToken;
        }

        /// <summary>
        /// Triggers the reload change token and creates a new one.
        /// </summary>
        protected void OnReload()
        {
            ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
            previousToken.OnReload();
        }

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

        /// <summary>
        /// Used to trigger the change token when a reload occurs.
        /// </summary>
        public void OnReload() => _cts.Cancel();

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

三. 重构

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

public class EFConfigurationSource<TDbContext>: IConfigurationSource where TDbContext : DbContext
    {
        public readonly Action<DbContextOptionsBuilder> _optionsAction;
        public readonly bool _reloadOnChange;
        public readonly int _pollingInterval;
        public readonly Action<EFConfigurationLoadException<TDbContext>>? OnLoadException;
        public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction,
            bool reloadOnChange = false,
            int pollingInterval = 5000, 
            Action<EFConfigurationLoadException<TDbContext>>? 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<TDbContext>(this);
        }
    }

新增了三个属性:

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

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

    public sealed class EFConfigurationLoadException<TDbContext> where TDbContext:DbContext
    {
        public Exception Exception { get;  }
        public bool Ignorabel { get; set; }
        public EFConfigurationSource<TDbContext> Source { get; }
        internal EFConfigurationLoadException(EFConfigurationSource<TDbContext> source,Exception ex)
        {
            Source = source;
            Exception = ex;
        }
    }

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

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

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

public class EFConfigurationProvider<TDbContext>:ConfigurationProvider,IDisposable where TDbContext : DbContext
    {
        private readonly EFConfigurationSource<TDbContext> _source;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private byte[] _lastComputeHash;
        private Task? _watchDbTask;
        private bool _disposed;

        public EFConfigurationProvider(EFConfigurationSource<TDbContext> 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<TDbContext>(_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的主要实现如上,其中属性分别代表:

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

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

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

private async Task WatchDatabase(CancellationToken cancellationToken)
        {
            while(!cancellationToken.IsCancellationRequested)
            {
                try
                {
                    await Task.Delay(_source._pollingInterval, cancellationToken);
                    IDictionary<string, string> actualData = await GetDataAsync();
                    byte[] computedHash=ComputeHash(actualData);
                    if(!computedHash.SequenceEqual(_lastComputeHash))
                    {
                        Data = actualData;
                        OnReload();
                    }
                    _lastComputeHash = computedHash;
                }
                catch (Exception ex)
                {
                    var exception = new EFConfigurationLoadException<TDbContext>(_source, ex);
                    _source.OnLoadException?.Invoke(exception);
                    if(!exception.Ignorabel)
                    {
                        throw;
                    }
                }
            }
        }

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

        public async Task<IDictionary<string, string>> GetDataAsync()
        {
            using TDbContext dbContext=CreateDbContext();
            IQueryable<ConfigurationEntity> entries=dbContext.Set<ConfigurationEntity>();
            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<TDbContext> builder = new DbContextOptionsBuilder<TDbContext>();
            _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
    {
        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TDbContext">DbContext type that contains setting values.</typeparam>
        /// <param name="configurationBuilder">The Microsoft.Extensions.Configuration.IConfigurationBuilder to add to.</param>
        /// <param name="optionsAction">DbContextOptionsBuilder used to create related DbContext.</param>
        /// <param name="reloadOnChange"></param>
        /// <param name="pollingInterval"></param>
        /// <param name="onLoadException"></param>
        /// <returns></returns>
        public static IConfigurationBuilder AddEfConfiguration<TDbContext>(this IConfigurationBuilder configurationBuilder,
            Action<DbContextOptionsBuilder> optionsAction,
            bool reloadOnChange=false,
            int pollingInterval=5000,
            Action<EFConfigurationLoadException<TDbContext>>? onLoadException =null) where TDbContext:DbContext
        {
            return configurationBuilder.Add(new EFConfigurationSource<TDbContext>(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<ConfigurationDbContext>(
        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();就能同步更新。

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

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

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

完整代码在:FrameWorks/ConfigurationFromDb

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/q__y__L/article/details/127653213

智能推荐

JavaScript学习笔记_curry函数未定义-程序员宅基地

文章浏览阅读343次。五种原始的变量类型1.Undefined--未定义类型 例:var v;2.String -- ' '或" "3.Boolean4.Number5.Null--空类型 例: var v=null;Number中:NaN -- not a number非数本身是一个数字,但是它和任何数字都不相等,代表非数,它和自己都不相等判断是不是NaN不能用=_curry函数未定义

兑换码编码方案实践_优惠券编码规则-程序员宅基地

文章浏览阅读1.2w次,点赞2次,收藏17次。兑换码编码设计当前各个业务系统,只要涉及到产品销售,就离不开大大小小的运营活动需求,其中最普遍的就是兑换码需求,无论是线下活动或者是线上活动,都能起到良好的宣传效果。兑换码:由一系列字符组成,每一个兑换码对应系统中的一组信息,可以是优惠信息(优惠券),也可以是相关奖品信息。在实际的运营活动中,要求兑换码是唯一的,每一个兑换码对应一个优惠信息,而且需求量往往比较大(实际上的需求只有预期_优惠券编码规则

c语言周林答案,C语言程序设计实训教程教学课件作者周林ch04结构化程序设计课件.ppt...-程序员宅基地

文章浏览阅读45次。C语言程序设计实训教程教学课件作者周林ch04结构化程序设计课件.ppt* * 4.1 选择结构程序设计 4.2 循环结构程序设计 4.3 辅助控制语句 第四章 结构化程序设计 4.1 选择结构程序设计 在现实生活中,需要进行判断和选择的情况是很多的: 如果你在家,我去拜访你 如果考试不及格,要补考 如果遇到红灯,要停车等待 第四章 结构化程序设计 在现实生活中,需要进行判断和选择的情况..._在现实生活中遇到过条件判断的问

幻数使用说明_ioctl-number.txt幻数说明-程序员宅基地

文章浏览阅读999次。幻数使用说明 在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作。怎么实现这些操作,这是每一个程序员自己的事情。 因为设备都是特定的,这里也没法说。关键在于怎样组织命令码,因为在ioctl中命令码是唯一联系用户程序命令和驱动程序支持的途径 。 命令码的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,利_ioctl-number.txt幻数说明

ORB-SLAM3 + VScode:检测到 #include 错误。请更新 includePath。已为此翻译单元禁用波浪曲线_orb-slam3 include <system.h> 报错-程序员宅基地

文章浏览阅读399次。键盘按下“Shift+Ctrl+p” 输入: C++Configurations,选择JSON界面做如下改动:1.首先把 “/usr/include”,放在最前2.查看C++路径,终端输入gcc -v -E -x c++ - /usr/include/c++/5 /usr/include/x86_64-linux-gnu/c++/5 /usr/include/c++/5/backward /usr/lib/gcc/x86_64-linux-gnu/5/include /usr/local/_orb-slam3 include 报错

「Sqlserver」数据分析师有理由爱Sqlserver之十-Sqlserver自动化篇-程序员宅基地

文章浏览阅读129次。本系列的最后一篇,因未有精力写更多的入门教程,上篇已经抛出书单,有兴趣的朋友可阅读好书来成长,此系列主讲有理由爱Sqlserver的论证性文章,希望读者们看完后,可自行做出判断,Sqlserver是否真的合适自己,目的已达成。渴望自动化及使用场景笔者所最能接触到的群体为Excel、PowerBI用户群体,在Excel中,我们知道可以使用VBA、VSTO来给Excel带来自动化操作..._sqlsever 数据分析

随便推点

智慧校园智慧教育大数据平台(教育大脑)项目建设方案PPT_高校智慧大脑-程序员宅基地

文章浏览阅读294次,点赞6次,收藏4次。教育智脑)建立学校的全连接中台,对学校运营过程中的数据进行处理和标准化管理,挖掘数据的价值。能:一、原先孤立的系统聚合到一个统一的平台,实现单点登录,统一身份认证,方便管理;三、数据共享,盘活了教育大数据资源,通过对外提供数。的方式构建教育的通用服务能力平台,支撑教育核心服务能力的沉淀和共享。物联网将学校的各要素(人、机、料、法、环、测)全面互联,数据实时。智慧校园解决方案,赋能教学、管理和服务升级,智慧教育体系,该数据平台具有以下几大功。教育大数据平台底座:教育智脑。教育大数据平台,以中国联通。_高校智慧大脑

编程5大算法总结--概念加实例_算法概念实例-程序员宅基地

文章浏览阅读9.5k次,点赞2次,收藏27次。分治法,动态规划法,贪心算法这三者之间有类似之处,比如都需要将问题划分为一个个子问题,然后通过解决这些子问题来解决最终问题。但其实这三者之间的区别还是蛮大的。贪心是则可看成是链式结构回溯和分支界限为穷举式的搜索,其思想的差异是深度优先和广度优先一:分治算法一、基本概念在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两_算法概念实例

随笔—醒悟篇之考研调剂_考研调剂抑郁-程序员宅基地

文章浏览阅读5.6k次。考研篇emmmmm,这是我随笔篇章的第二更,原本计划是在中秋放假期间写好的,但是放假的时候被安排写一下单例模式,做了俩机试题目,还刷了下PAT的东西,emmmmm,最主要的还是因为我浪的很开心,没空出时间来写写东西。  距离我考研结束已经快两年了,距离今年的考研还有90天左右。  趁着这个机会回忆一下青春,这一篇会写的比较有趣,好玩,纯粹是为了记录一下当年考研中发生的有趣的事。  首先介绍..._考研调剂抑郁

SpringMVC_class org.springframework.web.filter.characterenco-程序员宅基地

文章浏览阅读438次。SpringMVC文章目录SpringMVC1、SpringMVC简介1.1 什么是MVC1.2 什么是SpringMVC1.3 SpringMVC的特点2、HelloWorld2.1 开发环境2.2 创建maven工程a>添加web模块b>打包方式:warc>引入依赖2.3 配置web.xml2.4 创建请求控制器2.5 创建SpringMVC的配置文件2.6 测试Helloworld2.7 总结3、@RequestMapping注解3.1 @RequestMapping注解的功能3._class org.springframework.web.filter.characterencodingfilter is not a jakart

gdb: Don‘t know how to run. Try “help target“._don't know how to run. try "help target".-程序员宅基地

文章浏览阅读4.9k次。gdb 远程调试的一个问题:Don't know how to run. Try "help target".它在抱怨不知道怎么跑,目标是什么. 你需要为它指定target remote 或target extended-remote例如:target extended-remote 192.168.1.136:1234指明target 是某IP的某端口完整示例如下:targ..._don't know how to run. try "help target".

c语言程序设计教程 郭浩志,C语言程序设计教程答案杨路明郭浩志-程序员宅基地

文章浏览阅读85次。习题 11、算法描述主要是用两种基本方法:第一是自然语言描述,第二是使用专用工具进行算法描述2、c 语言程序的结构如下:1、c 语言程序由函数组成,每个程序必须具有一个 main 函数作为程序的主控函数。2、“/*“与“*/“之间的内容构成 c 语言程序的注释部分。3、用预处理命令#include 可以包含有关文件的信息。4、大小写字母在 c 语言中是有区别的。5、除 main 函数和标准库函数以..._c语言语法0x1e