Have you ever heard of or used the change tokens before? Basically, it helps us to track state changes (that’s it!).
.NET makes use of change tokens in a crucial part of the system, in configuration providers! Let’s say you want to reload configuration values in runtime and decided to use IOptionsMonitor or IOptionsSnapshot interface. Whenever you update your configuration source (appsettings.json file, keyvault, etc.) your classes will use the updated version of it. And as you may already guess this is the place where change tokens come into play.
Now, I am going to share some code snippets to show you how to use a change token to track changes in your model.
In this sample, we will have a guest list and want to keep track of that list. First, we’ll create the GuestListSource class.
GuestListSource.cs
// GuestListSource.cs
namespace Starfish.Core.Resources;
public class GuestListSource {
private static readonly HashSet < string > Guests = new() {
{
"Jeramiah Novako"
}, {
"Mina Ryan"
}, {
"Serena Gillespie"
}, {
"Marco Medina"
}
};
public static IEnumerable < string > Get() => Guests;
public static void Add(string fullName) {
Guests.Add(fullName);
}
public static void Remove(string fullName) {
Guests.Remove(fullName);
}
public static void Clean() {
Guests.Clear();
}
public static bool Exists(string fullName) => Guests.Contains(fullName);
}
As you see the class has some logic in it so it’s a rich domain model. I added some methods here so users of this class can use them to manipulate the data. In this way, it will be easier to track changes.
Now, let’s update the class to add a change-tracking feature. A Watch() method will be added. And later on, we will call that method in the Program.cs to track changes.
GuestListSource.cs
// GuestListSource.cs
using Microsoft.Extensions.Primitives;
namespace Starfish.Core.Resources;
public static class GuestListSource{
private static readonly HashSet < string > Guests = new() {
{
"Jeramiah Novako"
}, {
"Mina Ryan"
}, {
"Serena Gillespie"
}, {
"Marco Medina"
}
};
public static IEnumerable < string > Get() => Guests;
public static void Add(string fullName) {
Guests.Add(fullName);
Changed(); // NEW
}
public static void Remove(string fullName) {
Guests.Remove(fullName);
Changed(); // NEW
}
public static void Clean() {
Guests.Clear();
Changed(); // NEW
}
public static bool Exists(string fullName) => Guests.Contains(fullName);
///
/// <summary>
/// Rest of the class related to the change tracking feature.
/// If you need to track the guest list you can use the Watch() method.
/// </summary>
///
private static CancellationTokenSource ? _cancellationTokenSource;
private static Guid ? _watcherId;
public static IChangeToken Watch(Guid watcherId) {
if (_watcherId is not null && _watcherId != watcherId) {
throw new ArgumentException($ "There is already an active guest list watcher. watcherId: {_watcherId}");
}
_watcherId = watcherId;
_cancellationTokenSource?.Dispose()
_cancellationTokenSource = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);
return changeToken;
}
private static void Changed() {
_cancellationTokenSource?.Cancel();
}
}
I used the CancellationTokenSource to create a new change token. Our goal is whenever the list got updated the Changed() method will be called and the cancellation token will be canceled.
In short, a canceled token means the data has changed.
The user of the Watch() method will get the created IChangeToken so it will be aware of when the token is canceled. Let’s show the Watcher part of it.
Program.cs
// Program.cs
var builder = WebApplication.CreateBuilder(args);
...
...
// Watchers (Just to try Change Token feature)
WatcherHelper.AddGuestListWatcher();
var app = builder.Build();
...
...
WatchHelper.cs
// WatchHelper.cs
using System.Diagnostics;
using Microsoft.Extensions.Primitives;
using Starfish.Core.Resources;
namespace Starfish.Web.Watchers;
public static class WatcherHelper {
private static readonly Guid WatcherId = new("3E36307F-E703-420A-BB59-BEEE16717F26");
public static void AddGuestListWatcher() {
ChangeToken.OnChange(changeTokenProducer: () => GuestListSource.Watch(WatcherId), changeTokenConsumer: () => {
Debug.WriteLine("Guest list has been changed");
// Notify someone that the guest list has been changed
// Trigger some other awesome methods..
});
}
}
ChangeToken class has the OnChange method that accepts a producer and a consumer. Flow works like this;
- You get the change token from the producer (GuestListSource.Watch()) and wait until the token gets canceled which means the list has been updated.
- The token gets canceled and the OnChange method calls the Watch() method again and gets a new token and also runs the consumer part of the code, in our case Debug.WriteLine(“Guest list has been changed”).
Important Note
This is just one way of using change tokens, you will see different implementations in the wild. I’ve shared this example to give you some context around it.
And that’s all I have for this article. It’s really nice to have this kind of feature under the hood so you don’t need to import a new library or write the whole logic for your use case.
I tried to use a simple example to show how to use a ChangeToken in your model. I hope it helps and please feel free to make additions/suggestions.