1. IChangeToken 接口概述
IChangeToken 是 .NET 中一个强大的接口,用于监视和响应变化。它提供了一种标准化的方式来观察资源(如文件、配置、缓存等)的变更,并在发生变化时触发回调。这个接口在 ASP.NET Core 的配置系统、文件监视、缓存失效等场景中都有广泛应用。
注意:IChangeToken 是一个轻量级的抽象,它本身不包含具体的监视逻辑,而是提供了一个统一的接口来消费变化通知。
IChangeToken 的核心价值在于它解耦了变化的生产者和消费者。生产者(如文件系统监视器)负责检测变化并生成通知,消费者(如配置重新加载逻辑)只需要订阅这些通知,而不需要关心具体的监视实现细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. IChangeToken 接口定义与成员解析
2.1 接口定义
IChangeToken 接口定义非常简单但功能强大:
csharp复制public interface IChangeToken
{
bool HasChanged { get; }
bool ActiveChangeCallbacks { get; }
IDisposable RegisterChangeCallback(Action<object> callback, object state);
}
2.2 成员详解
2.2.1 HasChanged 属性
这是一个只读布尔属性,用于指示自上次检查以来是否发生了更改。典型的用法模式是:
csharp复制if (changeToken.HasChanged)
{
// 执行重新加载或其他响应逻辑
}
重要提示:HasChanged 的实现应该是幂等的。多次连续调用应该返回相同的结果,除非在这期间又发生了新的变化。
2.2.2 ActiveChangeCallbacks 属性
这个布尔属性指示此令牌是否会主动调用回调。当为 false 时,表示消费者需要主动轮询 HasChanged 属性来检测变化。
一些变化源(如配置文件监视)可以主动通知变化,而有些(如基于时间的过期)则需要被动轮询。这个属性让消费者知道他们可以依赖回调还是需要自己轮询。
2.2.3 RegisterChangeCallback 方法
这是接口中最重要的方法,用于注册在发生变化时要调用的回调。它返回一个 IDisposable,当不再需要回调时应该调用其 Dispose 方法取消注册。
方法签名:
csharp复制IDisposable RegisterChangeCallback(Action<object> callback, object state);
参数说明:
callback:当变化发生时调用的委托state:传递给回调的状态对象
3. IChangeToken 的实现模式
3.1 基本实现示例
下面是一个简单的 IChangeToken 实现,它在指定时间后标记为已更改:
csharp复制public class TimerChangeToken : IChangeToken
{
private readonly TimeSpan _delay;
private bool _hasChanged;
public TimerChangeToken(TimeSpan delay)
{
_delay = delay;
Task.Delay(delay).ContinueWith(_ => _hasChanged = true);
}
public bool HasChanged => _hasChanged;
public bool ActiveChangeCallbacks => false;
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
// 这个简单实现不支持回调
return EmptyDisposable.Instance;
}
private class EmptyDisposable : IDisposable
{
public static readonly EmptyDisposable Instance = new();
public void Dispose() { }
}
}
3.2 支持回调的实现
更完整的实现应该支持回调通知。下面是一个支持回调的版本:
csharp复制public class CallbackTimerChangeToken : IChangeToken
{
private readonly TimeSpan _delay;
private bool _hasChanged;
private List<(Action<object>, object)> _callbacks = new();
public CallbackTimerChangeToken(TimeSpan delay)
{
_delay = delay;
Task.Delay(delay).ContinueWith(_ =>
{
_hasChanged = true;
foreach (var (callback, state) in _callbacks)
{
callback(state);
}
});
}
public bool HasChanged => _hasChanged;
public bool ActiveChangeCallbacks => true;
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
_callbacks.Add((callback, state));
return new DisposableRegistration(this, callback, state);
}
private class DisposableRegistration : IDisposable
{
private readonly CallbackTimerChangeToken _token;
private readonly Action<object> _callback;
private readonly object _state;
public DisposableRegistration(CallbackTimerChangeToken token, Action<object> callback, object state)
{
_token = token;
_callback = callback;
_state = state;
}
public void Dispose()
{
_token._callbacks.Remove((_callback, _state));
}
}
}
4. IChangeToken 的实际应用场景
4.1 配置系统重载
ASP.NET Core 的配置系统广泛使用 IChangeToken 来实现配置源的热重载。例如,当 appsettings.json 文件被修改时,配置系统会自动重新加载配置。
csharp复制var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
// 获取一个监视配置变化的令牌
var changeToken = config.GetReloadToken();
changeToken.RegisterChangeCallback(state =>
{
Console.WriteLine("配置已更改,新值: " + ((IConfiguration)state)["Key"]);
}, config);
4.2 文件监视
FileSystemWatcher 可以与 IChangeToken 集成,提供更高级的文件变化通知:
csharp复制public class FileChangeToken : IChangeToken
{
private readonly FileSystemWatcher _watcher;
private bool _changed;
private readonly List<(Action<object>, object)> _callbacks = new();
public FileChangeToken(string path, string filter)
{
_watcher = new FileSystemWatcher(path, filter);
_watcher.Changed += (s, e) =>
{
_changed = true;
foreach (var (callback, state) in _callbacks)
{
callback(state);
}
};
_watcher.EnableRaisingEvents = true;
}
public bool HasChanged => _changed;
public bool ActiveChangeCallbacks => true;
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
_callbacks.Add((callback, state));
return new DisposableRegistration(this, callback, state);
}
private class DisposableRegistration : IDisposable
{
// 类似于前面的实现
}
}
4.3 缓存失效
在缓存场景中,IChangeToken 可以用来实现缓存项的自动失效:
csharp复制public class CacheEntry<T>
{
private T _value;
private readonly IChangeToken _expirationToken;
public CacheEntry(T value, IChangeToken expirationToken)
{
_value = value;
_expirationToken = expirationToken;
if (_expirationToken.ActiveChangeCallbacks)
{
_expirationToken.RegisterChangeCallback(_ => _value = default, null);
}
}
public T Value
{
get
{
if (_expirationToken.HasChanged)
{
_value = default;
}
return _value;
}
}
}
5. 高级用法与最佳实践
5.1 组合多个 ChangeToken
有时我们需要监视多个变化源。可以使用 ChangeToken 静态类的 OnChange 方法或 CompositeChangeToken 类来组合多个令牌:
csharp复制var token1 = new FileChangeToken("path1", "*.*");
var token2 = new FileChangeToken("path2", "*.*");
// 方式1:使用 CompositeChangeToken
var compositeToken = new CompositeChangeToken(new[] { token1, token2 });
// 方式2:使用 ChangeToken.OnChange
ChangeToken.OnChange(() => compositeToken, () =>
{
Console.WriteLine("任一文件已更改");
});
5.2 避免回调内存泄漏
注册回调时如果不正确管理,可能会导致内存泄漏。最佳实践是:
- 总是存储返回的 IDisposable
- 在不再需要回调时调用 Dispose
- 避免在回调中捕获可能导致循环引用的对象
csharp复制var token = GetChangeToken();
var registration = token.RegisterChangeCallback(state =>
{
// 回调逻辑
// 完成后取消注册
((IDisposable)state).Dispose();
}, registration);
// 或者使用局部函数
void Callback(object state)
{
// 处理变化
registration.Dispose();
}
registration = token.RegisterChangeCallback(Callback, null);
5.3 性能考虑
- 回调执行时间:保持回调逻辑简短,避免长时间运行的操作
- 回调频率:高频变化场景可能需要去抖动(debounce)
- 线程安全:确保实现是线程安全的,特别是 HasChanged 属性和回调列表
下面是一个带去抖动的实现:
csharp复制public class DebouncingChangeToken : IChangeToken
{
private readonly IChangeToken _sourceToken;
private readonly TimeSpan _delay;
private bool _hasChanged;
private DateTime _lastChangeTime = DateTime.MinValue;
public DebouncingChangeToken(IChangeToken sourceToken, TimeSpan delay)
{
_sourceToken = sourceToken;
_delay = delay;
_sourceToken.RegisterChangeCallback(_ =>
{
var now = DateTime.UtcNow;
if (now - _lastChangeTime >= _delay)
{
_hasChanged = true;
_lastChangeTime = now;
}
}, null);
}
public bool HasChanged => _hasChanged;
public bool ActiveChangeCallbacks => _sourceToken.ActiveChangeCallbacks;
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
return _sourceToken.RegisterChangeCallback(callback, state);
}
}
6. 常见问题与解决方案
6.1 回调未被触发
问题现象:注册了回调但从未被调用。
可能原因及解决方案:
- ActiveChangeCallbacks 为 false:需要定期检查 HasChanged 属性
- 令牌生命周期结束:某些令牌(如配置重载令牌)在触发一次后就会失效
- 回调注册太晚:在注册回调前变化已经发生
6.2 多次触发回调
问题现象:单个变化导致回调被多次调用。
解决方案:
- 在实现中添加去抖动逻辑(如前面的 DebouncingChangeToken)
- 在回调中添加防重入检查
csharp复制private bool _isHandlingChange;
changeToken.RegisterChangeCallback(state =>
{
if (_isHandlingChange) return;
try
{
_isHandlingChange = true;
// 处理变化
}
finally
{
_isHandlingChange = false;
}
}, null);
6.3 性能问题
问题现象:系统在高频变化场景下响应变慢。
优化建议:
- 使用更粗粒度的变化检测(如检测目录而非单个文件)
- 增加去抖动时间
- 将多个变化合并处理
7. 扩展与自定义场景
7.1 数据库变更通知
我们可以创建一个监视数据库表变化的 IChangeToken 实现:
csharp复制public class SqlTableChangeToken : IChangeToken
{
private readonly string _connectionString;
private readonly string _tableName;
private readonly TimeSpan _pollingInterval;
private DateTime _lastCheck;
private DateTime? _lastChangeTime;
public SqlTableChangeToken(string connectionString, string tableName, TimeSpan pollingInterval)
{
_connectionString = connectionString;
_tableName = tableName;
_pollingInterval = pollingInterval;
_lastCheck = DateTime.UtcNow;
Task.Run(async () =>
{
while (true)
{
await Task.Delay(_pollingInterval);
CheckForChanges();
}
});
}
private void CheckForChanges()
{
var lastChange = GetLastChangeTimeFromDb();
if (lastChange > _lastCheck)
{
_lastChangeTime = lastChange;
// 触发回调...
}
_lastCheck = DateTime.UtcNow;
}
private DateTime GetLastChangeTimeFromDb()
{
// 实现查询表的最后修改时间
using var connection = new SqlConnection(_connectionString);
return connection.QuerySingle<DateTime>(
$"SELECT CHANGE_TRACKING_CURRENT_VERSION() AS LastChange FROM {_tableName}");
}
// 其他接口成员实现...
}
7.2 跨进程变更通知
对于需要跨进程通信的场景,可以使用内存映射文件或 IPC 机制:
csharp复制public class InterProcessChangeToken : IChangeToken
{
private readonly MemoryMappedFile _mmf;
private bool _hasChanged;
public InterProcessChangeToken(string mapName)
{
_mmf = MemoryMappedFile.CreateOrOpen(mapName, 1024);
// 启动一个线程来监视共享内存的变化
}
// 其他实现...
}
7.3 与 async/await 集成
标准的 IChangeToken 使用基于回调的模型,但我们可以创建一个适配器来支持 async/await:
csharp复制public static class ChangeTokenExtensions
{
public static Task WaitForChangeAsync(this IChangeToken changeToken, CancellationToken cancellationToken = default)
{
if (changeToken.HasChanged)
{
return Task.CompletedTask;
}
var tcs = new TaskCompletionSource<bool>();
var registration = changeToken.RegisterChangeCallback(_ => tcs.TrySetResult(true), null);
cancellationToken.Register(() =>
{
registration.Dispose();
tcs.TrySetCanceled();
});
return tcs.Task;
}
}
// 使用示例
await changeToken.WaitForChangeAsync();
Console.WriteLine("变化已发生");
