<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hans">
	<id>https://tst.fannq.com/index.php?action=history&amp;feed=atom&amp;title=%E8%81%8A%E8%81%8A_IChangeToken_%E6%8E%A5%E5%8F%A3</id>
	<title>聊聊 IChangeToken 接口 - 版本历史</title>
	<link rel="self" type="application/atom+xml" href="https://tst.fannq.com/index.php?action=history&amp;feed=atom&amp;title=%E8%81%8A%E8%81%8A_IChangeToken_%E6%8E%A5%E5%8F%A3"/>
	<link rel="alternate" type="text/html" href="https://tst.fannq.com/index.php?title=%E8%81%8A%E8%81%8A_IChangeToken_%E6%8E%A5%E5%8F%A3&amp;action=history"/>
	<updated>2026-06-13T14:52:19Z</updated>
	<subtitle>本wiki上该页面的版本历史</subtitle>
	<generator>MediaWiki 1.41.1</generator>
	<entry>
		<id>https://tst.fannq.com/index.php?title=%E8%81%8A%E8%81%8A_IChangeToken_%E6%8E%A5%E5%8F%A3&amp;diff=491&amp;oldid=prev</id>
		<title>Root：​创建页面，内容为“ &lt;div&gt;  由于两个月的奋战，导致很久没更新了。就是上回老周说的那个产线和机械手搬货的项目，好不容易等到工厂放假了，我就偷偷乐了。当然也过年了，老周先给大伙伴们拜年了，P话不多讲，就祝大家身体健康、生活愉快。其实生活和健康是密不可分的，想活得好，就得健康。包括身体健康、思想健康、心理健康、精神健康。不能以为我无病无痛…”</title>
		<link rel="alternate" type="text/html" href="https://tst.fannq.com/index.php?title=%E8%81%8A%E8%81%8A_IChangeToken_%E6%8E%A5%E5%8F%A3&amp;diff=491&amp;oldid=prev"/>
		<updated>2024-12-09T13:16:28Z</updated>

		<summary type="html">&lt;p&gt;创建页面，内容为“ &amp;lt;div&amp;gt;  由于两个月的奋战，导致很久没更新了。就是上回老周说的那个产线和机械手搬货的项目，好不容易等到工厂放假了，我就偷偷乐了。当然也过年了，老周先给大伙伴们拜年了，P话不多讲，就祝大家身体健康、生活愉快。其实生活和健康是密不可分的，想活得好，就得健康。包括身体健康、思想健康、心理健康、精神健康。不能以为我无病无痛…”&lt;/p&gt;
&lt;p&gt;&lt;b&gt;新页面&lt;/b&gt;&lt;/p&gt;&lt;div&gt;&lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
由于两个月的奋战，导致很久没更新了。就是上回老周说的那个产线和机械手搬货的项目，好不容易等到工厂放假了，我就偷偷乐了。当然也过年了，老周先给大伙伴们拜年了，P话不多讲，就祝大家身体健康、生活愉快。其实生活和健康是密不可分的，想活得好，就得健康。包括身体健康、思想健康、心理健康、精神健康。不能以为我无病无痛就很健康，你起码要全方位健康。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
不管你的工作是什么，忙或者不忙，报酬高或低，但是，人，总得活，总得过日子。咱们最好多给自己点福利，多整点可以自娱自乐的东西，这就是生活。下棋、打游戏、绘画、书法、钓鱼、飙车、唢呐……不管玩点啥，只要积极正向的就好，可以大大降低得抑郁症、高血压的机率；可以减少70%无意义的烦恼；可以降低跳楼风险；在这个礼崩乐坏的社会环境中，可以抵御精神污染……总之，益处是大大的有。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
然后老周再说一件事，一月份的时候常去工厂调试，也认识了机械臂厂商派的技术支持——吴大工程师。由于工厂所处地段非常繁华，因此每次出差，午饭只能在附近一家四川小吃店解决。毕竟这方圆百十里也仅此一家。不去那里吃饭除非自带面包蹲马路边啃，工厂不供食也不供午休场所。刚开始几次出差还真的像个傻子似的蹲马路边午休。后来去多了，直接钻进工厂的会议室睡午觉。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
有一天吃午饭时，吴老师说：你说什么样的人编程水平最高？&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
我直接从潜意识深处回答他：我做一个排序，仅供参考。编程水平从高到低排行：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
1、黑客。虽然大家都说黑客一代不如一代，但目前来说，这群人还是最强的；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
2、纯粹技术爱好者；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
3、著名开源项目贡献者。毕竟拿不出手的代码也不好意思与人分享；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
4、做过许多项目的一线开发者。我强调的项目数量多，而不是长年只维护一个项目的。只有数量多你学到的才多；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
5、社区贡献较多者，这个和3差不多。不过，老周认为的社区贡献就是不仅提供代码，还提供文档、思路、技巧等；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
6、刚入坑但基础较好的开发者；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
7、培训机构的吹牛专业户；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
8、大学老师/教授；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
9、短视频平台上的砖家、成宫人士；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
10、刚学会写 main 函数的小朋友。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
==========================================================================================================&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
下面进入主题，咱们今天聊聊 IChangeToken。它的主要功能是提供更改通知。比如你的配置源发生改变了，要通知配置的使用者重新加载。你可能会疑惑，这货跟使用事件有啥区别？这个老周也不好下结论，应该是为异步代码准备的吧。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
下面是 IChangeToken 接口的成员：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;bool HasChanged { get; }&lt;br /&gt;
bool ActiveChangeCallbacks { get; }&lt;br /&gt;
IDisposable RegisterChangeCallback(Action&amp;lt;object?&amp;gt; callback, object? state);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
这个 Change Token 思路很清奇，实际功能类似事件，就是更改通知。咱们可以了解一下其原理，但如果你觉得太绕，不想了解也没关系的。在自定义配置源时，咱们是不需要自己写 Change Token 的，框架已有现成的。我们只要知道要触发更改通知时调用相关成员就行。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
如果你想看源码的话，老周可以告你哪些文件（github 项目是 dotnet\runtime）：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
1、runtime-main\src\libraries\Common\src\Extensions\ChangeCallbackRegistrar.cs：这个主要是 UnsafeRegisterChangeCallback 方法，用于注册回调委托；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
2、runtime-main\src\libraries\Microsoft.Extensions.Primitives\src\ChangeToken.cs：这个类主要是提供静态的辅助方法，用于注册回调委托。它的好处是可以循环——注册回调后，触发后委托被调用；调用完又自动重新注册，使得 Change Token 可以多次触发；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
3、runtime-main\src\libraries\Microsoft.Extensions.Primitives\src\CancellationChangeToken.cs：这个类是真正实现 IChangeToken 接口的；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
4、runtime-main\src\libraries\Microsoft.Extensions.Configuration\src\ConfigurationReloadToken.cs：这个也是实现 IChangeToken 接口，而且它才是咱们今天的主角，该类就是为重新加载配置数据而提供的。调用它的 OnReload 方法可以触发更改通知。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
看了上面这些，你可能更疑惑了。啥原理？为啥 Token 只能触发一次？为何要重新注册回调？&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
咱们用一个简单例子演练一下。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;static void Main(string[] args)&lt;br /&gt;
{&lt;br /&gt;
    CancellationTokenSource cs = new();&lt;br /&gt;
    // 这里获取token&lt;br /&gt;
    CancellationToken token = cs.Token;&lt;br /&gt;
    // token 可以注册回调&lt;br /&gt;
    token.Register(() =&amp;gt;&lt;br /&gt;
    {&lt;br /&gt;
        Console.WriteLine(&amp;quot;你按下了【K】键&amp;quot;);&lt;br /&gt;
    });&lt;br /&gt;
    // 启动一个新task&lt;br /&gt;
    Task myTask = Task.Run(() =&amp;gt;&lt;br /&gt;
    {&lt;br /&gt;
        // 等待输入，如果按下【K】键，就让CancellationTokenSource取消         ConsoleKeyInfo keyInfo;&lt;br /&gt;
        while(true)&lt;br /&gt;
        {&lt;br /&gt;
            keyInfo = Console.ReadKey(true);&lt;br /&gt;
            if(keyInfo.Key == ConsoleKey.K)&lt;br /&gt;
            {&lt;br /&gt;
                // 取消                 cs.Cancel();&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    });&lt;br /&gt;
    // 主线程等待任务完成     Task.WaitAll(myTask);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
CancellationTokenSource 类表示一个取消任务的标记，访问它的 Token 属性可以获得一个 CancellationToken 结构体实例，可以检索它的 IsCancellationRequested 属性以明确是否有取消请求（有则true，无则false）。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
还有更重要的，CancellationToken 结构体的 Register 方法可以注册一个委托作为回调，当收到取消请求后会触发这个委托。对的，这个就是 Change Token 灵魂所在了。一旦回调被触发后，CancellationTokenSource 就处于取消状态了，你无法再次触发，除非重置或重新实例化。这就是回调只能触发一次的原因。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
下面，咱们完成一个简单的演示——用数据库做配置源。在 SQL Server 里面随便建个数据库，然后添加一个表，名为 tb_configdata。它有四个字段：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;CREATE TABLE [dbo].[tb_configdata](&lt;br /&gt;
    [ID] [int] NOT NULL,&lt;br /&gt;
    [config_key] [nvarchar](15) NOT NULL,&lt;br /&gt;
    [config_value] [nvarchar](30) NOT NULL,&lt;br /&gt;
    [remark] [nvarchar](50) NULL,&lt;br /&gt;
 CONSTRAINT [PK_tb_configdata] PRIMARY KEY CLUSTERED &lt;br /&gt;
(&lt;br /&gt;
    [ID] ASC,&lt;br /&gt;
    [config_key] ASC&lt;br /&gt;
)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]&lt;br /&gt;
) ON [PRIMARY] GO&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
ID和config_key设为主键，config_value 是配置的值，remark 是备注。备注字段其实可以不用，但实际应用的时候，可以用来给配置项写点注释。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
然后，在程序里面咱们用到 EF Core，故要先生成与表对应的实体类。这里老周就不用工具了，直接手写更有效率。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;// 实体类 public class MyConfigData&lt;br /&gt;
{&lt;br /&gt;
    public int ID { get; set; }&lt;br /&gt;
    public string ConfigKey { get; set; } = string.Empty;&lt;br /&gt;
    public string ConfigValue { get; set; } = string.Empty;&lt;br /&gt;
    public string? Remark { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// 数据库上下文对象 public class DemoConfigDBContext : DbContext&lt;br /&gt;
{&lt;br /&gt;
    public DbSet ConfigData =&amp;gt; Set();&lt;br /&gt;
&lt;br /&gt;
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)&lt;br /&gt;
    {&lt;br /&gt;
        optionsBuilder.UseSqlServer(&amp;quot;Data Source=DEV-PC\\SQLTEST;Initial Catalog=Demo;Integrated Security=True;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Intent=ReadWrite;Multi Subnet Failover=False&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    protected override void OnModelCreating(ModelBuilder modelbd)&lt;br /&gt;
    {&lt;br /&gt;
        modelbd.Entity()&lt;br /&gt;
            .ToTable(&amp;quot;tb_configdata&amp;quot;)&lt;br /&gt;
            .HasKey(c =&amp;gt; new { c.ID, c.ConfigKey });&lt;br /&gt;
        modelbd.Entity()&lt;br /&gt;
            .Property(c =&amp;gt; c.ConfigKey)&lt;br /&gt;
            .HasColumnName(&amp;quot;config_key&amp;quot;);&lt;br /&gt;
        modelbd.Entity()&lt;br /&gt;
            .Property(c =&amp;gt; c.ConfigValue)&lt;br /&gt;
            .HasColumnName(&amp;quot;config_value&amp;quot;);&lt;br /&gt;
        modelbd.Entity()&lt;br /&gt;
            .Property(c =&amp;gt; c.Remark)&lt;br /&gt;
            .HasColumnName(&amp;quot;remark&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
上述代码的情况特殊，实体类的名称和成员名称与数据表并不一致，所以在重写 OnModelCreating 方法时，需要进行映射。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
1、ToTable(&amp;quot;tb_configdata&amp;quot;) 告诉 EF 实体类对应的数据表是 tb_configdata；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
2、HasKey(c =&amp;gt; new { c.ID, c.ConfigKey })：表明该实体有两个主键——ID和ConfigKey。这里指定的是实体类的属性，而不是数据表的字段名，因为后面咱们会进行列映射；&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
3、HasColumnName(&amp;quot;config_key&amp;quot;)：告诉 EF，实体的 ConfigKey 属性对应的是数据表中 config_key。后面的几个属性的道理一样，都是列映射。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
做映射就类似于填坑，如果你不想挖坑，那就直接让实体类名与表名一样，属性名与表字段（列）一样，这样就省事多了。不过，在实际使用中真没有那么美好。很多时候数据库是小李负责的，人家早就建好了，存储过程都写了几万个了。后面前台程序是老张来开发，对老张来说，要么把实体的命名与数据库的一致，要么就做一下映射。多数情况下是要映射的，毕竟很多时候数据库对象的命名都比较奇葩。尤其有上千个表的时候，为了看得顺眼，很多人喜欢这样给数据表命名：ta_XXX、ta_YYY、tb_ZZZ、tc_FFF、tx_PPP、ty_EEE、tz_WWW。还有这样命名的：m1_Report、m2_ReportDetails…… m105_TMD、m106_WNM、m107_DOUBI。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
这种命名用在实体类上面确实很不优雅，所以映射就很必要了。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
此处咱们不用直接实现 IConfigurationProvider 接口，而是从 ConfigurationProvider 类派生就行了。自定义配置源的东东老周以前写过，只是当时没有实现更改通知。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;public class MyConfigurationProvider : ConfigurationProvider, IDisposable&lt;br /&gt;
{&lt;br /&gt;
    private System.Threading.Timer theTimer;&lt;br /&gt;
&lt;br /&gt;
    public MyConfigurationProvider()&lt;br /&gt;
    {&lt;br /&gt;
        theTimer = new Timer(OnTimer, null, 100, 10000);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private void OnTimer(object? state)&lt;br /&gt;
    {&lt;br /&gt;
        // 先调用Load方法，然后用OnReload触发更新通知 Load();&lt;br /&gt;
        OnReload();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    public void Dispose()&lt;br /&gt;
    {&lt;br /&gt;
        theTimer?.Change(0, 0);&lt;br /&gt;
        theTimer?.Dispose();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    public override void Load()&lt;br /&gt;
    {&lt;br /&gt;
        // 先读取一下 using DemoConfigDBContext dbctx = new();&lt;br /&gt;
        // 如果无数据，先初始化 if(dbctx.ConfigData.Count() == 0)&lt;br /&gt;
        {&lt;br /&gt;
            InitData(dbctx.ConfigData);&lt;br /&gt;
        }&lt;br /&gt;
        // 加载数据 Data = dbctx.ConfigData.ToDictionary(k =&amp;gt; k.ConfigKey, k =&amp;gt; (string?)k.ConfigValue);&lt;br /&gt;
&lt;br /&gt;
        // 本地函数 void InitData(DbSet set)&lt;br /&gt;
        {&lt;br /&gt;
            int _id = 1;&lt;br /&gt;
            set.Add(new()&lt;br /&gt;
            {&lt;br /&gt;
                ID = _id,&lt;br /&gt;
                ConfigKey = &amp;quot;page_size&amp;quot;,&lt;br /&gt;
                ConfigValue = &amp;quot;25&amp;quot;&lt;br /&gt;
            });&lt;br /&gt;
            _id += 1;&lt;br /&gt;
            set.Add(new()&lt;br /&gt;
            {&lt;br /&gt;
                ID = _id,&lt;br /&gt;
                ConfigKey = &amp;quot;format&amp;quot;,&lt;br /&gt;
                ConfigValue = &amp;quot;xml&amp;quot;&lt;br /&gt;
            });&lt;br /&gt;
            _id += 1;&lt;br /&gt;
            set.Add(new()&lt;br /&gt;
            {&lt;br /&gt;
                ID = _id,&lt;br /&gt;
                ConfigKey = &amp;quot;limited_height&amp;quot;,&lt;br /&gt;
                ConfigValue = &amp;quot;1450&amp;quot;&lt;br /&gt;
            });&lt;br /&gt;
            _id += 1;&lt;br /&gt;
            set.Add(new()&lt;br /&gt;
            {&lt;br /&gt;
                ID = _id,&lt;br /&gt;
                ConfigKey = &amp;quot;msg_lead&amp;quot;,&lt;br /&gt;
                ConfigValue = &amp;quot;TDXA_&amp;quot;&lt;br /&gt;
            });&lt;br /&gt;
            // 保存数据             dbctx.SaveChanges();&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
由于老周不知道怎么监控数据库更新，最简单的办法就是用定时器循环检查。重点是重写 Load 方法，完成加载配置的逻辑。Load 方法覆写后不需要调用 base 的 Load 方法，因为基类的方法是空的，调用了也没毛用。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
在 Timer 对象调用的方法（OnTimer）中，先调用 Load 方法，再调用 OnReload 方法。这样就可以在加载数据后触发更改通知。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
然后实现 IConfigurationSource 接口，提供 MyConfigurationProvider 实例。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;public class MyConfigurationSource : IConfigurationSource&lt;br /&gt;
{&lt;br /&gt;
    public IConfigurationProvider Build(IConfigurationBuilder builder)&lt;br /&gt;
    {&lt;br /&gt;
        return new MyConfigurationProvider();&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
默认的配置源有JSON文件、命令行、环境变量等，为了排除干扰，便于查看效果，在 Main 方法中咱们先把配置源列表清空，再添加咱们自定义的配置源。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;var builder = WebApplication.CreateBuilder(args);&lt;br /&gt;
// 清空配置源 builder.Configuration.Sources.Clear();&lt;br /&gt;
// 添加配置源到Sources builder.Configuration.Sources.Add(new MyConfigurationSource());&lt;br /&gt;
var app = builder.Build();&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
最后，可以做个简单测试，直接注入 Mini-API 中读取配置。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;app.MapGet(&amp;quot;/&amp;quot;, (IConfiguration config) =&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    StringBuilder bd = new();&lt;br /&gt;
    foreach(var kp in config.AsEnumerable())&lt;br /&gt;
    {&lt;br /&gt;
        bd.AppendLine($&amp;quot;{kp.Key} = {kp.Value}&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    return bd.ToString();&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
运行效果如下：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
[[Image:22aa57921dbd2352837fb6d504a2268a.png]]&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
这时候咱们到数据库里把配置值改一下。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;update tb_configdata&lt;br /&gt;
    set config_value = N&amp;#039;55&amp;#039; where config_key = N&amp;#039;page_size&amp;#039; update tb_configdata&lt;br /&gt;
    set config_value = N&amp;#039;1900&amp;#039; where config_key = N&amp;#039;limited_height&amp;#039;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
接着回应用程序的页面，刷新一下，配置值已更新。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
[[Image:c803c753f996dbdbd4b3c93d9638eb56.png]]&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
这里你可能会有个疑问：连接字符串硬编码了不太好，要不写在配置文件中，可是，写在JSON文件中咱们怎么获取呢？毕竟 ConfigurationProvider 不使用依赖注入。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
IConfigurationSource 不是有个 Build 方法吗？Build 方法不是有个参数是 IConfigurationBuilder 吗？用它，用它，狠狠地用它。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;public class MyConfigurationSource : IConfigurationSource&lt;br /&gt;
{&lt;br /&gt;
    public IConfigurationProvider Build(IConfigurationBuilder builder)&lt;br /&gt;
    {&lt;br /&gt;
        // 此处可以临时build一个配置树，就能获取到JSON配置文件里面的连接字符串了 var config = builder.Build();&lt;br /&gt;
        string connStr = config[&amp;quot;ConnectionStrings:test&amp;quot;]!;&lt;br /&gt;
        return new MyConfigurationProvider(connStr);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
前面定义的一些类也要改一下。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
先是 MyConfigurationProvider 的构造函数。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;public class MyConfigurationProvider : ConfigurationProvider, IDisposable&lt;br /&gt;
{&lt;br /&gt;
    private System.Threading.Timer theTimer;&lt;br /&gt;
    private string connectString;&lt;br /&gt;
&lt;br /&gt;
    public MyConfigurationProvider(string cnnstr)&lt;br /&gt;
    {&lt;br /&gt;
        connectString = cnnstr;&lt;br /&gt;
        ……&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    ……&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
DemoConfigDBContext 类是连接字符串的最终使用者，所以也要改一下。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;public class DemoConfigDBContext : DbContext&lt;br /&gt;
{&lt;br /&gt;
    private string connStr;&lt;br /&gt;
&lt;br /&gt;
    public DemoConfigDBContext(string connectionString)&lt;br /&gt;
    {&lt;br /&gt;
        connStr = connectionString;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    ……&lt;br /&gt;
&lt;br /&gt;
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)&lt;br /&gt;
    {&lt;br /&gt;
        optionsBuilder.UseSqlServer(connStr);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
在appsettings.json 文件中配置连接字符串。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;{&lt;br /&gt;
  &amp;quot;Logging&amp;quot;: {&lt;br /&gt;
    ……&lt;br /&gt;
  },&lt;br /&gt;
  &amp;quot;AllowedHosts&amp;quot;: &amp;quot;*&amp;quot;,&lt;br /&gt;
  &amp;quot;ConnectionStrings&amp;quot;: {&lt;br /&gt;
    &amp;quot;test&amp;quot;: &amp;quot;Data Source=DEV-PC\\SQLTEST;Initial Catalog=Demo;Integrated Security=True;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Intent=ReadWrite;Multi Subnet Failover=False&amp;quot;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
回到 Main 方法，咱们还得加上 JSON 配置源。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;var builder = WebApplication.CreateBuilder(args);&lt;br /&gt;
// 清空配置源 builder.Configuration.Sources.Clear();&lt;br /&gt;
// 添加配置源到Sources builder.Configuration.AddJsonFile(&amp;quot;appsettings.json&amp;quot;);&lt;br /&gt;
builder.Configuration.Sources.Add(new MyConfigurationSource());&lt;br /&gt;
var app = builder.Build();&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
其他的不变。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
-----------------------------------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
接下来，咱们弄个一对多的例子。逻辑是这样的：启动程序显示主窗口，接着创建五个子窗口。主窗口上有个大大的按钮，点击后，五个子窗口会收到通知。大概就这个样子：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
[[Image:d9fdc28380c3dadfa2008d2f4cf8244a.png]]&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
子窗口名为 TextForm，代码如下：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;internal class TestForm : Form&lt;br /&gt;
{&lt;br /&gt;
    private IDisposable _changeTokenReg;&lt;br /&gt;
    private TextBox _txtMsg;&lt;br /&gt;
    public TestForm(Func getToken)&lt;br /&gt;
    {&lt;br /&gt;
        // 初始化子级控件&lt;br /&gt;
        _txtMsg = new()&lt;br /&gt;
        {&lt;br /&gt;
            Dock = DockStyle.Fill,&lt;br /&gt;
            Margin = new Padding(5),&lt;br /&gt;
            Multiline = true,&lt;br /&gt;
            ScrollBars = ScrollBars.Vertical&lt;br /&gt;
        };&lt;br /&gt;
        Controls.Add(_txtMsg);&lt;br /&gt;
&lt;br /&gt;
        _changeTokenReg = ChangeToken.OnChange(getToken, OnCallback);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // 回调方法 void OnCallback()&lt;br /&gt;
    {&lt;br /&gt;
        DateTime curtime = DateTime.Now;&lt;br /&gt;
        string str = $&amp;quot;{curtime.ToLongTimeString()} 新年快乐\r\n&amp;quot;;&lt;br /&gt;
        _txtMsg.BeginInvoke(() =&amp;gt;&lt;br /&gt;
        {&lt;br /&gt;
            _txtMsg.AppendText(str);&lt;br /&gt;
        });&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    protected override void Dispose(bool disposing)&lt;br /&gt;
    {&lt;br /&gt;
        // 释放对象 if (disposing)&lt;br /&gt;
        {&lt;br /&gt;
            _changeTokenReg?.Dispose();&lt;br /&gt;
        }&lt;br /&gt;
        base.Dispose(disposing);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
窗口上只放了一个文本框。上面代码中，使用了 ChangeToken.OnChange 静态方法，为 Change Token 注册回调委托，本例中回调委托绑定的是 OnCallback 方法，也就是说：当 Change Token 触发后会在文本框中追加文本。OnChange 静态方法有两个重载：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;// 咱们示例中用的是这个版本 static IDisposable OnChange(Func changeTokenProducer, Action changeTokenConsumer);&lt;br /&gt;
// 这是另一个重载 static IDisposable OnChange(Func changeTokenProducer, Action changeTokenConsumer, TState state);&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
上述例子用的是第一个，其实里面调用的也是第二个重载，只是把咱们传递的 OnCallback 方法当作 TState 传进去了。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
请大伙伴暂时记住 changeTokenProducer 和 changeTokenConsumer 这两参数。changeTokenProducer 也是一个委托，返回 IChangeToken。用的时候一定要注意，每次触发之前，Change Token 要先创建新实例。注意是先创建新实例再触发，否则会导致无限。尽管内部会判断 HasChanged 属性，可问题是这个判断是在注册回调之后的。这个是跟 Change Token 的清奇逻辑有关，咱们看看 OnChage 的源代码就明白了。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt; public static IDisposable OnChange(Func changeTokenProducer, Action changeTokenConsumer, TState state)&lt;br /&gt;
 {&lt;br /&gt;
     if (changeTokenProducer is null)&lt;br /&gt;
     {&lt;br /&gt;
         ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenProducer);&lt;br /&gt;
     }&lt;br /&gt;
     if (changeTokenConsumer is null)&lt;br /&gt;
     {&lt;br /&gt;
         ThrowHelper.ThrowArgumentNullException(ExceptionArgument.changeTokenConsumer);&lt;br /&gt;
     }&lt;br /&gt;
&lt;br /&gt;
     return new ChangeTokenRegistration(changeTokenProducer, changeTokenConsumer, state);&lt;br /&gt;
 }&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
简单来说，就是返回一个 ChangeTokenRegistration 实例，这是个私有类，咱们是访问不到的，以 IDisposable 接口公开。其中，它有两个方法是递归调用的：&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;private void OnChangeTokenFired()&lt;br /&gt;
{&lt;br /&gt;
    // The order here is important. We need to take the token and then apply our changes BEFORE&lt;br /&gt;
 // registering. This prevents us from possible having two change updates to process concurrently.&lt;br /&gt;
    // // If the token changes after we take the token, then we&amp;#039;ll process the update immediately upon&lt;br /&gt;
    // registering the callback. IChangeToken? token = _changeTokenProducer();&lt;br /&gt;
&lt;br /&gt;
 try&lt;br /&gt;
    {&lt;br /&gt;
        _changeTokenConsumer(_state);&lt;br /&gt;
    }&lt;br /&gt;
    finally&lt;br /&gt;
    {&lt;br /&gt;
        // We always want to ensure the callback is registered RegisterChangeTokenCallback(token);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
private void RegisterChangeTokenCallback(IChangeToken? token)&lt;br /&gt;
{&lt;br /&gt;
    if (token is null)&lt;br /&gt;
    {&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
    IDisposable registraton = token.RegisterChangeCallback(s =&amp;gt; ((ChangeTokenRegistration?)s)!.OnChangeTokenFired(), this);&lt;br /&gt;
    if (token.HasChanged &amp;amp;&amp;amp; token.ActiveChangeCallbacks)&lt;br /&gt;
    {&lt;br /&gt;
        registraton?.Dispose();&lt;br /&gt;
        return;&lt;br /&gt;
    }&lt;br /&gt;
    SetDisposable(registraton);&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
在 ChangeTokenRegistration 类的构造函数中，先调用 RegisterChangeTokenCallback 方法，开始了整个递归套娃的过程。在 RegisterChangeTokenCallback 方法中，为 token 注册的回调就是调用 OnChangeTokenFired 方法。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
而 OnChangeTokenFired 方法中，是&amp;#039;&amp;#039;&amp;#039;先获取新的 Change Token，再触发旧 token&amp;#039;&amp;#039;&amp;#039;。最后，又调用 RegisterChangeTokenCallback 方法，实现了无限套娃的逻辑。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
因此，咱们在用的时候，必须先创建新的 Change Token 实例，然后再调用 RegisterChangeTokenCallback 实例的 Cancel 方法。不然这无限套娃会一直进行到栈溢出，除非你提前把 ChangeTokenRegistration 实例 Dispose 掉（由 OnChange 静态方法返回）。可是那样的话，你就不能多次接收更改了。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
下面就是主窗口部分，也是最危险的部分——必须按照咱们上面分析的顺序进行，不然会 Stack Overflow。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;div&amp;gt; &lt;br /&gt;
&amp;lt;pre&amp;gt;public partial class Form1 : Form&lt;br /&gt;
{&lt;br /&gt;
    private CancellationTokenSource _cancelTkSource;&lt;br /&gt;
 private CancellationChangeToken _changeToken;&lt;br /&gt;
 public Form1()&lt;br /&gt;
    {&lt;br /&gt;
        InitializeComponent();&lt;br /&gt;
        _cancelTkSource = new CancellationTokenSource();&lt;br /&gt;
        _changeToken = new(_cancelTkSource.Token);&lt;br /&gt;
        button1.Click += OnButton1Click;&lt;br /&gt;
        button2.Click += OnButton2Click;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private void OnButton2Click(object? sender, EventArgs e)&lt;br /&gt;
    {&lt;br /&gt;
        for(int t= 0; t &amp;lt; 5; t++)&lt;br /&gt;
        {&lt;br /&gt;
            TestForm frm = new(GetChangeToken);&lt;br /&gt;
            frm.Text = &amp;quot;窗口&amp;quot; + (t + 1);&lt;br /&gt;
            frm.Size = new Size(300, 240);&lt;br /&gt;
            frm.StartPosition = FormStartPosition.CenterParent;&lt;br /&gt;
            frm.Show(this);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // 这个地方就是触发token了，所以要先换上新的实例 private void OnButton1Click(object? sender, EventArgs e)&lt;br /&gt;
    {&lt;br /&gt;
        // 先创建新的实例 var oldsource = Interlocked.Exchange(ref _cancelTkSource, new CancellationTokenSource());&lt;br /&gt;
        Interlocked.Exchange(ref _changeToken, new CancellationChangeToken(_cancelTkSource.Token));&lt;br /&gt;
        // 只要CancellationTokenSource一取消，其他客户端会收到通知         oldsource.Cancel();&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // 这个方法传递给 TestForm 构造函数，再传给 OnChange 静态方法 public IChangeToken? GetChangeToken()&lt;br /&gt;
    {&lt;br /&gt;
        return _changeToken;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt; &lt;br /&gt;
按钮1的单击事件处理方法就是触发点，所以，CancellationTokenSource、CancellationChangeToken 要先换成新的实例，然后再用旧的实例去 Cancel。这里用 Interlocked 类会好一些，毕竟要考虑异步的情况，虽然咱这里都是在UI线程上传递的，但还是遵守这个习惯好一些。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
这样处理就能避免栈溢出了。运行后，先打开五个子窗口（多点击一次就能创建十个子窗口）。接着点击大大按钮，五个子窗口就能收到通知了。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
[[Image:a503e0ddaa257b71c8d1d77db1c17b68.png]]&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
 &lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
好了，这次就聊到这儿了。&lt;br /&gt;
&lt;br /&gt;
 &lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>Root</name></author>
	</entry>
</feed>