【.NET Core框架】ChangeToken

场景

一个对象A,希望它的某些状态在发生改变时通知到B(或C、D)。常见的做法是在A中定义一个事件(或委托),当状态改变时A去触发这个事件。而B直接订阅这个事件
这种设计有点问题,B由于要订阅A的事件,所以B得完全引用A,其实有时候没必要,因为我只关心A的状态变化而已
解决方案就是加个中间层ChangeToken,A、B都引用ChangeToken,A通知ChangeToken状态改变,B向ChangeToken注册委托
这样ChangeToken可以作为一个通用组件,在很多需要更改通知是场景中使用,如:asp.net core的配置系统、终结点路由、 ....

实现

微软定义了一个IChangeToken
HasChanged:表示当前这个ChangeToken是否变化过了
ActiveChangeCallbacks:当 A触发ChangeToken发生变化时是否主动回调B塞进来的委托
RegisterChangeCallback(Action callback, object state):提供一个方法,允许调用方塞入委托,B就是调用这个方法向ChangeToken塞入委托的。有一种情况是B希望在塞入委托的同时附带一个状态对象,将来委托被执行时这个状态对象作为执行委托的参数

在“物理文件”这个圈子里面,IChangeToken的真身叫做PollingFileChangeToken
在“配置系统”这个圈子里面,IChangeToken的真身叫做ConfigurationReloadToken
如果不需要IChangeToken可以使用NullChangeToken.Singleton

CancellationChangeToken

CancellationChangeToken是一个用的比较多的实现类,它包含一个CancellationToken属性,这个属性是通过构造函数来初始化的,简化代码如下:

public class CancellationChangeToken : IChangeToken
{
        public CancellationChangeToken(CancellationToken cancellationToken)
        {
            Token = cancellationToken;
        }

        public bool ActiveChangeCallbacks { get; private set; } = true;

        public bool HasChanged => Token.IsCancellationRequested;

        private CancellationToken Token { get; }

        public IDisposable RegisterChangeCallback(Action<object> callback, object state)
        {
              return Token.Register(callback, state);
        }
    }

CancellationChangeToken只是对CancellationToken的包装。那为啥不直接让CancellationToken实现IChangeToken呢?我感觉是设计意图不同,CancellationToken设计时主要是考虑应用在取消异步操作这件事上的,只是碰巧取消异步操作与更改通知设计思路是相似的,所以才出现CancellationChangeToken

CompositeChangeToken

CompositeChangeToken代表由多个IChangeToken组合而成的复合型IChangeToken对象。如下面的代码片段所示,我们在调用构造函数创建一个CompositeChangeToken对象的时候,需要提供这些IChangeToken对象。对于一个CompositeChangeToken对象来说,只要组成它的任何一个IChangeToken发生改变,其HasChanged属性将会变成True,而注册的回调自然会被执行。至于ActiveChangeCallbacks属性,只要任何一个IChangeToken的同名属性返回True,该属性就会返回True。

public class CompositeChangeToken : IChangeToken
{
    public bool  ActiveChangeCallbacks { get; }
    public IReadOnlyList<IChangeToken>  ChangeTokens { get; }
    public bool  HasChanged { get; }
   
    public CompositeChangeToken(IReadOnlyList<IChangeToken> changeTokens);   
    public IDisposable RegisterChangeCallback(Action<object> callback, object state);   
}

IChangeToken演示

 static void Main(string[] args)
        {
            A a = new A();
            IChangeToken changeToken = a.CreateToken();
            B b = new B(changeToken);
            C c = new C(changeToken);

            Console.ReadKey();
        }
    public class A
    {
        CancellationTokenSource _tokenSource = null;
        public A()
        {
            Task.Run(() =>
            {
                while (true)
                {
                    Task.Delay(1000).Wait();
                    Console.WriteLine("A状态已修改");
                    this.ChangeState();
                }
            });

        }
        public void ChangeState()
        {
            _tokenSource.Cancel();
        }
        public IChangeToken CreateToken()
        {
            _tokenSource = new CancellationTokenSource();
            return new CancellationChangeToken(_tokenSource.Token);
        }
    }
    public class B
    {
        public B(IChangeToken changeToken)
        {
            changeToken.RegisterChangeCallback((state) =>
            {
                Console.WriteLine("B收到通知");
            }, null);
        }
    }
    public class C
    {
        public C(IChangeToken changeToken)
        {
            changeToken.RegisterChangeCallback((state) =>
            {
                Console.WriteLine("C收到通知");
            }, null);
        }
    }

A轮询修改状态,但是B、C只会调用一次。需要借助ChangeToken,可以永远监控A状态

static void Main(string[] args)
        {
            A a = new A();
            ChangeToken.OnChange(() => a.CreateToken(), () => { Console.WriteLine("监听到A状态已修改"); });
            Console.ReadKey();
        }

搞懂了IChangeToken我们就很轻松就能理解了ChangeToken,作为静态类的它,肯定是作为一个工具类的实现。

 ChangeToken.OnChange(
    () => physicalFileProvider.Watch("*.*"),
    () => Console.WriteLine("检测到文件夹有变化!")
);

那么您可能会说,我直接使用pysicalFileProvider.Watch()方法返回的IChangeToken的RegisterChangeCallback方法订阅不行吗?他们有什么区别吗? 答案是:“调用次数”。使用RegisterChangeCallback的方法,只会执行一次回调内容,因为当“令牌”用了一次之后,其实它就失效了。所以上面那个监控文件改动的代码,当第二次文件改动的时候,它其实是不会再执行回调的。
而使用ChangeToken这个静态类,它就可以帮助您不断的去获取新“令牌”然后注册对应的回调,所以就能够保证咱们多次改变也能触发回调了。
() => physicalFileProvider.Watch("*.*")这部分代码我们可以称它为“令牌生产过程”,而() => Console.WriteLine("检测到文件夹有变化!")叫做“令牌的消费过程”。
ChangeToken 干的事情就是:当消费者消费之后,就又会去让“生产过程”再生成一个令牌出来,并且在该令牌上挂载“消费过程”,这样就能保证能够一直“观察”下去了。

public static void Main(string[] args)
        {
            string dir = Directory.GetCurrentDirectory();
            string fileName = Path.Combine(dir, "a.txt");
            if (!File.Exists(fileName))
            {
                var fs = File.Create(fileName);
                fs.Dispose();                
            }
            PhysicalFileProvider fileProvider = new PhysicalFileProvider(dir);
            IChangeToken changeToken = fileProvider.Watch("*.*");

            //只会执行一次
            changeToken.RegisterChangeCallback((state) => {
                Console.WriteLine("1文件被修改");
            },"fan");
            //每次修改都会执行(正确方式)
            ChangeToken.OnChange(()=>fileProvider.Watch("*.*"), () => {
                Console.WriteLine("2文件被修改");
            });
            Console.ReadKey();
        }

案例1:添加缓存时提供IChangeToken,当依赖文件修改时,删除缓存

var fileProvider = new PhysicalFileProvider(Path.GetDirectoryName(dependencyFile));
var changeToken = fileProvider.Watch(Path.GetFileName(dependencyFile));
cache.Set(key, value, new MemoryCacheEntryOptions().AddExpirationToken(changeToken);

案例2:配置文件统一读取类,当配置文件修改时,利用ChangeToken重新读取配置

    public class ConfigSetting
    {
        private IConfiguration _configuration = null;
        public ConfigSetting(IConfiguration configuration)
        {
            _configuration = configuration;
            ChangeToken.OnChange(()=>configuration.GetReloadToken(), () =>
            {
                reloadConfiguration();
            });
            reloadConfiguration();
        }
        private void reloadConfiguration()
        {
            _siteName = _configuration["SiteName"];
            _siteID = _configuration.GetValue<int>("SiteID");
        }

        private string _siteName = null;
        private int _siteID = 0;
        public string SiteName
        {
            get{return _siteName;            }
        }
        public int SiteID
        {
            get{return _siteID;            }
        }
    }

参考:
https://www.cnblogs.com/artech/p/inside-asp-net-core-04-02.html
https://www.cnblogs.com/uoyo/p/12509871.html
https://www.cnblogs.com/jionsoft/p/12249326.html

posted @ 2020-04-25 13:30  .Neterr  阅读(867)  评论(0编辑  收藏  举报