惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
GbyAI
GbyAI
G
Google Developers Blog
Engineering at Meta
Engineering at Meta
月光博客
月光博客
腾讯CDC
Recent Announcements
Recent Announcements
酷 壳 – CoolShell
酷 壳 – CoolShell
爱范儿
爱范儿
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
阮一峰的网络日志
阮一峰的网络日志
博客园 - 【当耐特】
The GitHub Blog
The GitHub Blog
Last Week in AI
Last Week in AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Martin Fowler
Martin Fowler
A
About on SuperTechFans
博客园 - 叶小钗

Duende Software Official Site

The Backend for Frontend Pattern Is Now Official IETF Guidance: RFC 10017 Published WhatsApp One-Time Password (OTP) Login with Duende IdentityServer and User Management Planning a Successful Migration from IdentityServer3 to Duende IdentityServer Client Secrets, Mutual TLS and Private Key JWT, Oh My! How To Spell "Duende" Understanding .NET 11 Automatic CSRF Protection: A Guide for Identity Developers Security Lingo Explained: TOTP (Time-based One-Time Password) Custom Passkey Attestation Policies: Restricting Login to Hardware Keys OAuth Identity Chaining, Transaction Tokens, and Human-in-the-Loop: Summer 2026 Identity Standards Recap What is Identity? - The Question Every Team Should Answer Before Writing Code Security Is a Spectrum: How to Choose Session Lifetimes in Duende IdentityServer Passkeys and WebAuthn with Duende IdentityServer and User Management Authenticating Players in Godot 4 with OAuth 2.0 and OpenID Connect Hardening OAuth in the newest 2026-07-28 MCP Release Candidate Unify Your SAML and OIDC Signing Keys with Automatic Rotation and Duende IdentityServer Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Duende Software Stop AI Bots from Wasting Your Server How Duende IdentityServer Filters Claims (And Why It Matters) Core vs Extended Protocols in Duende IdentityServer v8: What You Get and When You Need More Your IdentityServer v8 Upgrade Checklist: A Quick Pre-Flight Guide Setting Up SAML Single Sign-On in ASP.NET with Duende IdentityServer Your Identity, Your Terms: Duende's Modular Identity Infrastructure and v8.x Release Duende Spring Launch '26: Identity Infrastructure That Expands With You SAML and OpenID Connect (OIDC): Coexistence, Not Competition
Duende Software
Khalid Abuhakmeh, AL Rodriguez · 2026-07-14 · via Duende Software Official Site
Summary: ChangeToken, a core primitive in the .NET Framework's Microsoft.Extensions.Primitives package, enables reactive patterns like configuration hot-reloading, options monitoring, and cache invalidation. The one-shot IChangeToken interface is the foundation, while the ChangeToken.OnChange() method simplifies continuous change monitoring by handling the automatic re-registration loop. Understanding this abstraction allows developers to build custom, framework-consistent change notification sources.

You've written reloadOnChange: true more times than you can count. You've used IOptionsMonitor<T> to pick up fresh configuration without restarting your app. Maybe you've tied a cache entry's lifetime to a file on disk. All of those features share a common mechanism under the hood: ChangeToken, a type in Microsoft.Extensions.Primitives package that has been quietly running your reactive patterns since Microsoft introduced ASP.NET Core.

Most developers never look at it directly. That changes today. Once you understand how ChangeToken works, you can build your own hot-reload, cache invalidation, and change-notification patterns with the same reliability that the framework uses internally.

Where ChangeToken Is Already in Your Code

ChangeToken shows up in four places you almost certainly use every day.

Configuration reloading. When you call AddJsonFile with reloadOnChange: true, the configuration system uses IFileProvider.Watch() to get an IChangeToken for appsettings.json. When the file changes on disk, that token signals a change, and your configuration reloads automatically.

The options monitor. IOptionsMonitor<T> tracks changes through IOptionsChangeTokenSource<T>. Every time you call monitor.CurrentValue and get fresh values without restarting; a change token is fired, triggering a reload behind the scenes.

Memory cache expiration. MemoryCacheEntryOptions has an AddExpirationToken(IChangeToken) method. Pass it any change token, and the cache entry evicts itself the moment that token signals. The expiration timer you set is just one option; change tokens are another.

File watching. IFileProvider.Watch("*.json") returns an IChangeToken directly. It fires when any matching file changes, moves, or is deleted.

These four APIs all speak the same language. The abstraction is IChangeToken, and because they share it, you can compose them, swap implementations, and slot your own logic in anywhere the framework expects one. You've probably written lines like these before:

Csharp

// Configuration reloading: ChangeToken fires when appsettings.json changes
builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

// Options monitor: CurrentValue is always fresh because ChangeToken triggered a reload
app.MapGet("/config", (IOptionsMonitor<MyOptions> monitor) => monitor.CurrentValue);

// Cache expiration tied to a change token: evicts when the token signals
var entry = cache.CreateEntry("key");
entry.AddExpirationToken(fileProvider.Watch("data.json"));

// File watching: returns an IChangeToken you can subscribe to
IChangeToken token = fileProvider.Watch("*.json");
// Each of these is abbreviated. The point is recognition, not a runnable program.

The IChangeToken Interface

The IChangeToken interface is intentionally small. Three members, nothing more:

Csharp

public interface IChangeToken
{
    bool HasChanged { get; }
    bool ActiveChangeCallbacks { get; }
    IDisposable RegisterChangeCallback(Action<object?> callback, object? state);
}

HasChanged flips to true when the source signals a change. Once it's true, it stays true. There's no reset. This approach is the one-shot design: a token represents a single change event rather than an ongoing subscription.

ActiveChangeCallbacks tells you whether the token will proactively invoke your callback. When this is true, the token fires your callback as soon as the change happens. When it's false, the token is polling-only, and you have to check HasChanged yourself. Most tokens you'll encounter in the framework have ActiveChangeCallbacks set to true.

RegisterChangeCallback wires up your handler. It returns an IDisposable you can use to unsubscribe. Because tokens are one-shot, you also need a new token after each change if you want to keep watching. That's the most common source of confusion with this API, and the reason ChangeToken.OnChange() exists.

ChangeToken.OnChange(): The Method That Does the Work

If you use RegisterChangeCallback directly, you're responsible for requesting a new token after every change and re-registering your callback. Miss that step once, and your watcher goes silent.

ChangeToken.OnChange() handles that loop for you:

Csharp

public static IDisposable OnChange(
    Func<IChangeToken?> changeTokenProducer,
    Action changeTokenConsumer);

The first argument is a factory function. Every time OnChange needs a token, it calls this. The second argument is your callback, invoked whenever a change is detected.

Here's what happens internally on each cycle:

sequenceDiagram
    participant OnChange as ChangeToken.OnChange()
    participant Producer as Token Producer (your code)
    participant Source as Change Source (e.g. file system)
    participant Callback as Your Callback

    OnChange->>Producer: Get IChangeToken
    Producer-->>OnChange: Token #1
    OnChange->>OnChange: Register on Token #1
    Source->>OnChange: Token #1 signals HasChanged = true
    OnChange->>Producer: Get IChangeToken (auto re-register)
    Producer-->>OnChange: Token #2
    OnChange->>Callback: Invoke callback
    OnChange->>OnChange: Register on Token #2
    Note over OnChange,Callback: Cycle repeats automatically

The re-registration step is the whole point. When a change fires, OnChange first fetches a new token from your producer, then invokes your callback, and then registers on the new token. Your watch continues without any extra code on your part. OnChange returns an IDisposable. Hold onto it and dispose of it when you want to stop watching, such as when your service shuts down.

Building a Custom Change Notification

Here's a concrete example. The program below monitors a directory and invokes a callback whenever any file in it changes. The whole thing is ChangeToken.OnChange() plus PhysicalFileProvider.

Add the NuGet package first:

Shell

dotnet add package Microsoft.Extensions.FileProviders.Physical

Then the code:

Csharp

using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;

var watchPath = Path.Combine(Path.GetTempPath(), "file-drop");
Directory.CreateDirectory(watchPath);

Console.WriteLine($"Watching: {watchPath}");
Console.WriteLine("Drop or modify files, then press Enter to stop.\n");

// PhysicalFileProvider wraps the directory we want to watch.
using var fileProvider = new PhysicalFileProvider(watchPath);

// The first argument is a factory: OnChange calls it every time it needs a fresh token.
// The second argument is your callback: it runs on every detected change.
// Re-registration is automatic. You don't need to call this again after each event.
using var watcher = ChangeToken.OnChange(
    () => fileProvider.Watch("*.*"),
    () => Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Change detected in {watchPath}")
);

Console.ReadLine();

That's the complete program. Drop a file into the temp directory, and you'll see the timestamp print. The producer lambda (() => fileProvider.Watch("*.*")) is called after each change, producing a fresh token and keeping the cycle alive automatically.

If you need to build a token from scratch without a file provider, CancellationChangeToken is the right tool. It wraps a CancellationTokenSource and signals a change when you cancel it:

Csharp

using Microsoft.Extensions.Primitives;

var cts = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cts.Token);

// Subscribe to the change
changeToken.RegisterChangeCallback(_ => Console.WriteLine("Change signaled!"), null);

// Later, when you want to signal:
cts.Cancel(); // prints "Change signaled!"

Because tokens are one-shot, you'd create a new CancellationTokenSource and a new CancellationChangeToken each time you want to signal again. That's the same re-registration pattern from before, and ChangeToken.OnChange() handles it the same way if you pass a factory that creates a fresh pair each time.

For watching multiple independent sources at once, CompositeChangeToken combines any number of IChangeToken instances into a single token that fires when any of them signal.

When to Use ChangeToken (and When Not To)

ChangeToken is a good fit when you need framework-consistent change notifications. If your code integrates with the configuration, options, or caching systems, it's the natural choice because those systems already understand IChangeToken. It's also useful when you want composable change sources: combine a file token, a timeout token, and a config token to get a single signal.

Reach for something else when the use case is different. High-frequency event streams belong in System.Threading.Channels or System.Reactive. Guaranteed delivery with retry and durability belongs in a message queue. Simple in-process notifications between two objects, where you control both sides, are fine as a plain C# event. ChangeToken is a notification primitive. It tells you something changed; it doesn't guarantee exactly once delivery, ordering, or backpressure.

Wrap-Up

ChangeToken is the observer pattern, standardized and shipped as part of the .NET Framework. It's been running your config reloads, options monitors, and cache invalidations from day one. The one-shot design keeps implementations simple and thread-safe, and ChangeToken.OnChange() handles the re-registration loop, so you don't have to.

Now that you know how it works, you can plug into the same abstraction. Any code that accepts an IChangeToken can work with your custom signal source, and any change source that produces an IChangeToken can drive your logic.

What's Next

For a hands-on challenge: find a spot in your codebase that polls for changes on a timer and replace it with a ChangeToken. Configuration files, feature flag stores, certificate rotation, and database-backed settings. Anything that follows the pattern "check if something changed, then react" is a candidate.