
由于两个月的奋战导致很久没更新了。就是上回老周说的那个产线和机械手搬货的项目好不容易等到工厂放假了我就偷偷乐了。当然也过年了老周先给大伙伴们拜年了P话不多讲就祝大家身体健康、生活愉快。其实生活和健康是密不可分的想活得好就得健康。包括身体健康、思想健康、心理健康、精神健康。不能以为我无病无痛就很健康你起码要全方位健康。不管你的工作是什么忙或者不忙报酬高或低但是人总得活总得过日子。咱们最好多给自己点福利多整点可以自娱自乐的东西这就是生活。下棋、打游戏、绘画、书法、钓鱼、飙车、唢呐……不管玩点啥只要积极正向的就好可以大大降低得抑郁症、高血压的机率可以减少70%无意义的烦恼可以降低跳楼风险在这个礼崩乐坏的社会环境中可以抵御精神污染……总之益处是大大的有。然后老周再说一件事一月份的时候常去工厂调试也认识了机械臂厂商派的技术支持——吴大工程师。由于工厂所处地段非常繁华因此每次出差午饭只能在附近一家四川小吃店解决。毕竟这方圆百十里也仅此一家。不去那里吃饭除非自带面包蹲马路边啃工厂不供食也不供午休场所。刚开始几次出差还真的像个傻子似的蹲马路边午休。后来去多了直接钻进工厂的会议室睡午觉。有一天吃午饭时吴老师说你说什么样的人编程水平最高我直接从潜意识深处回答他我做一个排序仅供参考。编程水平从高到低排行1、黑客。虽然大家都说黑客一代不如一代但目前来说这群人还是最强的2、纯粹技术爱好者3、著名开源项目贡献者。毕竟拿不出手的代码也不好意思与人分享4、做过许多项目的一线开发者。我强调的项目数量多而不是长年只维护一个项目的。只有数量多你学到的才多5、社区贡献较多者这个和3差不多。不过老周认为的社区贡献就是不仅提供代码还提供文档、思路、技巧等6、刚入坑但基础较好的开发者7、培训机构的吹牛专业户8、大学老师/教授9、短视频平台上的砖家、成宫人士10、刚学会写 main 函数的小朋友。下面进入主题咱们今天聊聊 IChangeToken。它的主要功能是提供更改通知。比如你的配置源发生改变了要通知配置的使用者重新加载。你可能会疑惑这货跟使用事件有啥区别这个老周也不好下结论应该是为异步代码准备的吧。下面是 IChangeToken 接口的成员bool HasChanged { get; } bool ActiveChangeCallbacks { get; } IDisposable RegisterChangeCallback(Actionobject? callback, object? state);这个 Change Token 思路很清奇实际功能类似事件就是更改通知。咱们可以了解一下其原理但如果你觉得太绕不想了解也没关系的。在自定义配置源时咱们是不需要自己写 Change Token 的框架已有现成的。我们只要知道要触发更改通知时调用相关成员就行。如果你想看源码的话老周可以告你哪些文件github 项目是 dotnet\runtime1、runtime-main\src\libraries\Common\src\Extensions\ChangeCallbackRegistrar.cs这个主要是 UnsafeRegisterChangeCallback 方法用于注册回调委托2、runtime-main\src\libraries\Microsoft.Extensions.Primitives\src\ChangeToken.cs这个类主要是提供静态的辅助方法用于注册回调委托。它的好处是可以循环——注册回调后触发后委托被调用调用完又自动重新注册使得 Change Token 可以多次触发3、runtime-main\src\libraries\Microsoft.Extensions.Primitives\src\CancellationChangeToken.cs这个类是真正实现 IChangeToken 接口的4、runtime-main\src\libraries\Microsoft.Extensions.Configuration\src\ConfigurationReloadToken.cs这个也是实现 IChangeToken 接口而且它才是咱们今天的主角该类就是为重新加载配置数据而提供的。调用它的 OnReload 方法可以触发更改通知。看了上面这些你可能更疑惑了。啥原理为啥 Token 只能触发一次为何要重新注册回调咱们用一个简单例子演练一下。static void Main(string[] args) { CancellationTokenSource cs new(); // 这里获取token CancellationToken token cs.Token; // token 可以注册回调 token.Register(() { Console.WriteLine(你按下了【K】键); }); // 启动一个新task Task myTask Task.Run(() { // 等待输入如果按下【K】键就让CancellationTokenSource取消 ConsoleKeyInfo keyInfo; while(true) { keyInfo Console.ReadKey(true); if(keyInfo.Key ConsoleKey.K) { // 取消 cs.Cancel(); break; } } }); // 主线程等待任务完成 Task.WaitAll(myTask); }CancellationTokenSource 类表示一个取消任务的标记访问它的 Token 属性可以获得一个 CancellationToken 结构体实例可以检索它的 IsCancellationRequested 属性以明确是否有取消请求有则true无则false。还有更重要的CancellationToken 结构体的 Register 方法可以注册一个委托作为回调当收到取消请求后会触发这个委托。对的这个就是 Change Token 灵魂所在了。一旦回调被触发后CancellationTokenSource 就处于取消状态了你无法再次触发除非重置或重新实例化。这就是回调只能触发一次的原因。下面咱们完成一个简单的演示——用数据库做配置源。在 SQL Server 里面随便建个数据库然后添加一个表名为 tb_configdata。它有四个字段CREATE TABLE [dbo].[tb_configdata]( [ID] [int] NOT NULL, [config_key] [nvarchar](15) NOT NULL, [config_value] [nvarchar](30) NOT NULL, [remark] [nvarchar](50) NULL, CONSTRAINT [PK_tb_configdata] PRIMARY KEY CLUSTERED ( [ID] ASC, [config_key] ASC )WITH (PAD_INDEX OFF, STATISTICS_NORECOMPUTE OFF, IGNORE_DUP_KEY OFF, ALLOW_ROW_LOCKS ON, ALLOW_PAGE_LOCKS ON, OPTIMIZE_FOR_SEQUENTIAL_KEY OFF) ON [PRIMARY] ) ON [PRIMARY] GOID和config_key设为主键config_value 是配置的值remark 是备注。备注字段其实可以不用但实际应用的时候可以用来给配置项写点注释。然后在程序里面咱们用到 EF Core故要先生成与表对应的实体类。这里老周就不用工具了直接手写更有效率。// 实体类 public class MyConfigData { public int ID { get; set; } public string ConfigKey { get; set; } string.Empty; public string ConfigValue { get; set; } string.Empty; public string? Remark { get; set; } } // 数据库上下文对象 public class DemoConfigDBContext : DbContext { public DbSetMyConfigData ConfigData SetMyConfigData(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(Data SourceDEV-PC\\SQLTEST;Initial CatalogDemo;Integrated SecurityTrue;Connect Timeout30;EncryptTrue;Trust Server CertificateTrue;Application IntentReadWrite;Multi Subnet FailoverFalse); } protected override void OnModelCreating(ModelBuilder modelbd) { modelbd.EntityMyConfigData() .ToTable(tb_configdata) .HasKey(c new { c.ID, c.ConfigKey }); modelbd.EntityMyConfigData() .Property(c c.ConfigKey) .HasColumnName(config_key); modelbd.EntityMyConfigData() .Property(c c.ConfigValue) .HasColumnName(config_value); modelbd.EntityMyConfigData() .Property(c c.Remark) .HasColumnName(remark); } }上述代码的情况特殊实体类的名称和成员名称与数据表并不一致所以在重写 OnModelCreating 方法时需要进行映射。1、ToTable(tb_configdata) 告诉 EF 实体类对应的数据表是 tb_configdata2、HasKey(c new { c.ID, c.ConfigKey })表明该实体有两个主键——ID和ConfigKey。这里指定的是实体类的属性而不是数据表的字段名因为后面咱们会进行列映射3、HasColumnName(config_key)告诉 EF实体的 ConfigKey 属性对应的是数据表中 config_key。后面的几个属性的道理一样都是列映射。做映射就类似于填坑如果你不想挖坑那就直接让实体类名与表名一样属性名与表字段列一样这样就省事多了。不过在实际使用中真没有那么美好。很多时候数据库是小李负责的人家早就建好了存储过程都写了几万个了。后面前台程序是老张来开发对老张来说要么把实体的命名与数据库的一致要么就做一下映射。多数情况下是要映射的毕竟很多时候数据库对象的命名都比较奇葩。尤其有上千个表的时候为了看得顺眼很多人喜欢这样给数据表命名ta_XXX、ta_YYY、tb_ZZZ、tc_FFF、tx_PPP、ty_EEE、tz_WWW。还有这样命名的m1_Report、m2_ReportDetails…… m105_TMD、m106_WNM、m107_DOUBI。这种命名用在实体类上面确实很不优雅所以映射就很必要了。此处咱们不用直接实现 IConfigurationProvider 接口而是从 ConfigurationProvider 类派生就行了。自定义配置源的东东老周以前写过只是当时没有实现更改通知。public class MyConfigurationProvider : ConfigurationProvider, IDisposable { private System.Threading.Timer theTimer; public MyConfigurationProvider() { theTimer new Timer(OnTimer, null, 100, 10000); } private void OnTimer(object? state) { // 先调用Load方法然后用OnReload触发更新通知 Load(); OnReload(); } public void Dispose() { theTimer?.Change(0, 0); theTimer?.Dispose(); } public override void Load() { // 先读取一下 using DemoConfigDBContext dbctx new(); // 如果无数据先初始化 if(dbctx.ConfigData.Count() 0) { InitData(dbctx.ConfigData); } // 加载数据 Data dbctx.ConfigData.ToDictionary(k k.ConfigKey, k (string?)k.ConfigValue); // 本地函数 void InitData(DbSetMyConfigData set) { int _id 1; set.Add(new() { ID _id, ConfigKey page_size, ConfigValue 25 }); _id 1; set.Add(new() { ID _id, ConfigKey format, ConfigValue xml }); _id 1; set.Add(new() { ID _id, ConfigKey limited_height, ConfigValue 1450 }); _id 1; set.Add(new() { ID _id, ConfigKey msg_lead, ConfigValue TDXA_ }); // 保存数据 dbctx.SaveChanges(); } } }由于老周不知道怎么监控数据库更新最简单的办法就是用定时器循环检查。重点是重写 Load 方法完成加载配置的逻辑。Load 方法覆写后不需要调用 base 的 Load 方法因为基类的方法是空的调用了也没毛用。在 Timer 对象调用的方法OnTimer中先调用 Load 方法再调用 OnReload 方法。这样就可以在加载数据后触发更改通知。然后实现 IConfigurationSource 接口提供 MyConfigurationProvider 实例。public class MyConfigurationSource : IConfigurationSource { public IConfigurationProvider Build(IConfigurationBuilder builder) { return new MyConfigurationProvider(); } }默认的配置源有JSON文件、命令行、环境变量等为了排除干扰便于查看效果在 Main 方法中咱们先把配置源列表清空再添加咱们自定义的配置源。var builder WebApplication.CreateBuilder(args); // 清空配置源 builder.Configuration.Sources.Clear(); // 添加配置源到Sources builder.Configuration.Sources.Add(new MyConfigurationSource()); var app builder.Build();最后可以做个简单测试直接注入 Mini-API 中读取配置。app.MapGet(/, (IConfiguration config) { StringBuilder bd new(); foreach(var kp in config.AsEnumerable()) { bd.AppendLine(${kp.Key} {kp.Value}); } return bd.ToString(); });运行效果如下这时候咱们到数据库里把配置值改一下。update tb_configdata set config_value N55 where config_key Npage_size update tb_configdata set config_value N1900 where config_key Nlimited_height接着回应用程序的页面刷新一下配置值已更新。这里你可能会有个疑问连接字符串硬编码了不太好要不写在配置文件中可是写在JSON文件中咱们怎么获取呢毕竟 ConfigurationProvider 不使用依赖注入。IConfigurationSource 不是有个 Build 方法吗Build 方法不是有个参数是 IConfigurationBuilder 吗用它用它狠狠地用它。public class MyConfigurationSource : IConfigurationSource { public IConfigurationProvider Build(IConfigurationBuilder builder) { // 此处可以临时build一个配置树就能获取到JSON配置文件里面的连接字符串了 var config builder.Build(); string connStr config[ConnectionStrings:test]!; return new MyConfigurationProvider(connStr); } }