Introducing the next era of Duende IdentityServer.

Read our CEO’s announcement

You're Already Using .NET's ChangeToken (You Just Don't Know It)

Two blue circles
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.

Related Articles