聊一聊 .NET 中的 CompositeChangeToken
在 .NET 应用程序开发中,经常需要监控文件、配置或数据源的变更,并做出响应。CancellationToken是处理异步操作取消的常用工具,但单个令牌有时无法满足复杂场景的需求。例如,你可能需要同时监控多个配置文件的变化,或者当任何一项资源发生变更时触发某些操作。这时,CompositeChangeToken就派上了用场。它是 .NET 在Microsoft.Extensions.Primitives命名空间中提供的一个强大工具,用于组合多个IChangeToken实例,形成统一的变更通知机制。本文将深入剖析CompositeChangeToken的原理,并通过可运行的代码示例展示其实际应用场景,帮助你理解如何在项目中使用它来简化变更监控逻辑。## CompositeChangeToken 的底层原理CompositeChangeToken是IChangeToken接口的一个实现,它的核心思想是将多个IChangeToken(称为子令牌)组合成一个令牌。当任意一个子令牌报告变更时,CompositeChangeToken就会触发变更通知。它的内部实现是通过订阅所有子令牌的RegisterChangeCallback方法,并在任何一个子令牌变更时调用组合令牌的回调。从源码角度看(可参考 .NET 开源仓库),CompositeChangeToken维护一个IReadOnlyList<IChangeToken>集合,并重写了HasChanged和ActiveChangeCallbacks属性。HasChanged会检查是否有任何一个子令牌报告了变更,而ActiveChangeCallbacks则取决于是否有子令牌支持回调机制。当调用RegisterChangeCallback时,它会为每个子令牌注册回调,并创建一个复合的CancellationTokenSource,以便在任意子令牌触发时取消当前令牌。这种设计模式类似于“或”逻辑:只要有一个子令牌发生变化,整个复合令牌就认为发生了变更。这比手动管理多个回调要简洁得多,也避免了重复代码。## 典型应用场景CompositeChangeToken常用于以下场景:-配置热更新:监控多个配置文件(如appsettings.json、custom.json)的变化,当任意文件修改时重新加载配置。-资源依赖链:监控文件系统变化和数据库变更,触发缓存刷新。-组合监控:在微服务中监控多个外部服务的状态变化。让我们通过两个具体示例来理解它的用法。## 示例一:监控多个文件变化假设你有一个应用,需要同时监控config1.json和config2.json的变化。当任意文件被修改时,输出一条日志。下面的代码展示了如何使用CompositeChangeToken实现。csharpusing Microsoft.Extensions.Primitives;using System;using System.IO;using System.Threading;using System.Threading.Tasks;class Program{ static async Task Main(string[] args) { // 创建两个文件变化令牌 var file1 = "config1.json"; var file2 = "config2.json"; // 确保文件存在(这里简化处理,实际应检查) File.WriteAllText(file1, "{}"); File.WriteAllText(file2, "{}"); // 创建 IChangeToken 实例 var token1 = new PollingFileChangeToken(file1); var token2 = new PollingFileChangeToken(file2); // 组合令牌 var compositeToken = new CompositeChangeToken(new[] { token1, token2 }); // 注册回调 using (var cancellationTokenSource = new CancellationTokenSource()) { compositeToken.RegisterChangeCallback(state => { Console.WriteLine("检测到文件变化!"); // 取消主循环,避免重复触发 cancellationTokenSource.Cancel(); }, null); try { // 等待文件变化 Console.WriteLine("等待配置文件变化... 修改文件以测试。"); await Task.Delay(Timeout.Infinite, cancellationTokenSource.Token); } catch (OperationCanceledException) { Console.WriteLine("检测完成。"); } } // 清理 File.Delete(file1); File.Delete(file2); }}// 简单的轮询文件变化令牌(用于演示,实际可用 FileSystemWatcher)public class PollingFileChangeToken : IChangeToken{ private readonly string _filePath; private DateTime _lastWriteTime; public PollingFileChangeToken(string filePath) { _filePath = filePath; _lastWriteTime = File.GetLastWriteTimeUtc(filePath); } public bool HasChanged { get { var currentTime = File.GetLastWriteTimeUtc(_filePath); return currentTime != _lastWriteTime; } } public bool ActiveChangeCallbacks => true; public IDisposable RegisterChangeCallback(Action<object> callback, object state) { // 简化实现:每100ms检查一次 var timer = new Timer(_ => { if (HasChanged) { callback(state); } }, null, 100, 100); return timer; }}代码解释:- 我们创建了两个PollingFileChangeToken(自定义轮询令牌),分别监控两个文件。-CompositeChangeToken将它们组合起来,注册的回调会在任意文件变化时触发。- 使用CancellationTokenSource管理程序生命周期,避免无限等待。运行此代码后,修改任意一个文件,控制台会输出“检测到文件变化!”。## 示例二:在配置热更新中使用 CompositeChangeToken在实际的 ASP.NET Core 应用中,CompositeChangeToken常用于配置系统。例如,你可能需要同时监控默认配置文件和自定义配置文件。下面的示例模拟了这一过程。csharpusing Microsoft.Extensions.Primitives;using System;using System.Collections.Generic;using System.IO;using System.Threading;class ConfigurationMonitor{ private readonly List<string> _configFiles = new List<string>(); public void AddConfigFile(string path) { _configFiles.Add(path); } public IChangeToken CreateCompositeChangeToken() { var tokens = new List<IChangeToken>(); foreach (var file in _configFiles) { // 使用 FileSystemWatcher 实现实时监控 tokens.Add(new FileWatcherChangeToken(file)); } return new CompositeChangeToken(tokens); }}// 基于 FileSystemWatcher 的令牌public class FileWatcherChangeToken : IChangeToken, IDisposable{ private readonly FileSystemWatcher _watcher; private bool _hasChanged; public FileWatcherChangeToken(string filePath) { var directory = Path.GetDirectoryName(filePath); var fileName = Path.GetFileName(filePath); _watcher = new FileSystemWatcher(directory, fileName) { NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime }; _watcher.Changed += (s, e) => { _hasChanged = true; }; _watcher.EnableRaisingEvents = true; } public bool HasChanged => _hasChanged; public bool ActiveChangeCallbacks => true; public IDisposable RegisterChangeCallback(Action<object> callback, object state) { _watcher.Changed += (s, e) => { callback(state); // 重置标志,避免重复触发 _hasChanged = false; }; // 返回一个空 IDisposable(简化实现) return new DisposableAction(() => { }); } public void Dispose() { _watcher?.Dispose(); }}// 辅助类public class DisposableAction : IDisposable{ private readonly Action _action; public DisposableAction(Action action) => _action = action; public void Dispose() => _action();}// 主程序class Program{ static void Main(string[] args) { var monitor = new ConfigurationMonitor(); monitor.AddConfigFile("appsettings.json"); monitor.AddConfigFile("custom.json"); // 创建文件 File.WriteAllText("appsettings.json", "{}"); File.WriteAllText("custom.json", "{}"); var compositeToken = monitor.CreateCompositeChangeToken(); Console.WriteLine("监控配置变化... 修改任意文件以测试。"); compositeToken.RegisterChangeCallback(state => { Console.WriteLine($"配置已变更,重新加载配置..."); // 这里可以添加配置重载逻辑 }, null); Console.ReadLine(); // 保持程序运行 // 清理 File.Delete("appsettings.json"); File.Delete("custom.json"); }}代码解释:-FileWatcherChangeToken使用FileSystemWatcher实现实时监控,比轮询更高效。-ConfigurationMonitor类封装了配置文件的添加和组合令牌的创建。- 当任意文件被修改时,回调触发,模拟配置热更新。运行后,修改appsettings.json或custom.json,控制台会立即输出“配置已变更,重新加载配置…”。## 注意事项与最佳实践-线程安全:CompositeChangeToken本身不是线程安全的,但它的子令牌可能在不同线程上触发。在回调中处理共享资源时需加锁。-资源清理:注册回调返回的IDisposable应妥善释放,避免内存泄漏。-性能考量:如果子令牌数量过多,注册回调的开销会线性增长。建议将相关令牌分组,而非使用一个巨大的组合。## 总结CompositeChangeToken是 .NET 中一个优雅的设计模式实现,它将多个变更令牌组合成一个逻辑单元,简化了“任意资源变化时触发操作”的场景。通过深入理解其原理(基于“或”逻辑的订阅机制),并结合实际代码示例,你可以在配置管理、文件监控、缓存刷新等领域灵活运用它。相比手动管理多个回调,CompositeChangeToken提供了更清晰、更可维护的代码结构,是 .NET 开发中值得掌握的工具。希望本文能帮助你更好地理解和应用这一特性。