AspNetCore.Docs/aspnetcore/fundamentals/primitives/change-tokens.md

15 KiB

title author description manager ms.author ms.date ms.devlang ms.prod ms.technology ms.topic uid
Detect changes with change tokens in ASP.NET Core guardrex Learn how to use change tokens to track changes. wpickett riande 11/10/2017 csharp asp.net-core aspnet article fundamentals/primitives/change-tokens

Detect changes with change tokens in ASP.NET Core

By Luke Latham

A change token is a general-purpose, low-level building block used to track changes.

View or download sample code (how to download)

IChangeToken interface

IChangeToken propagates notifications that a change has occurred. IChangeToken resides in the Microsoft.Extensions.Primitives namespace. For apps that don't use the Microsoft.AspNetCore.All metapackage, reference the Microsoft.Extensions.Primitives NuGet package in the project file.

IChangeToken has two properties:

  • ActiveChangedCallbacks indicate if the token proactively raises callbacks. If ActiveChangedCallbacks is set to false, a callback is never called, and the app must poll HasChanged for changes. It's also possible for a token to never be cancelled if no changes occur or the underlying change listener is disposed or disabled.
  • HasChanged gets a value that indicates if a change has occurred.

The interface has one method, RegisterChangeCallback(Action<Object>, Object), which registers a callback that's invoked when the token has changed. HasChanged must be set before the callback is invoked.

ChangeToken class

ChangeToken is a static class used to propagate notifications that a change has occurred. ChangeToken resides in the Microsoft.Extensions.Primitives namespace. For apps that don't use the Microsoft.AspNetCore.All metapackage, reference the Microsoft.Extensions.Primitives NuGet package in the project file.

The ChangeToken OnChange(Func<IChangeToken>, Action) method registers an Action to call whenever the token changes:

  • Func<IChangeToken> produces the token.
  • Action is called when the token changes.

ChangeToken has an OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) overload that takes an additional TState parameter that's passed into the token consumer Action.

OnChange returns an IDisposable. Calling Dispose stops the token from listening for further changes and releases the token's resources.

Example uses of change tokens in ASP.NET Core

Change tokens are used in prominent areas of ASP.NET Core monitoring changes to objects:

  • For monitoring changes to files, IFileProvider's Watch method creates an IChangeToken for the specified files or folder to watch.
  • IChangeToken tokens can be added to cache entries to trigger cache evictions on change.
  • For TOptions changes, the default OptionsMonitor implementation of IOptionsMonitor has an overload that accepts one or more IOptionsChangeTokenSource instances. Each instance returns an IChangeToken to register a change notification callback for tracking options changes.

Monitoring for configuration changes

By default, ASP.NET Core templates use JSON configuration files (appsettings.json, appsettings.Development.json, and appsettings.Production.json) to load app configuration settings.

These files are configured using the AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) extension method on ConfigurationBuilder that accepts a reloadOnChange parameter (ASP.NET Core 1.1 and later). reloadOnChange indicates if configuration should be reloaded on file changes. See this setting in the WebHost convenience method CreateDefaultBuilder (reference source):

config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

File-based configuration is represented by FileConfigurationSource. FileConfigurationSource uses IFileProvider (reference source) to monitor files.

By default, the IFileMonitor is provided by a PhysicalFileProvider (reference source), which uses FileSystemWatcher to monitor for configuration file changes.

The sample app demonstrates two implementations for monitoring configuration changes. If either the appsettings.json file changes or the Environment version of the file changes, each implementation executes custom code. The sample app writes a message to the console.

A configuration file's FileSystemWatcher can trigger multiple token callbacks for a single configuration file change. The sample's implementation guards against this problem by checking file hashes on the configuration files. Checking file hashes ensures that at least one of the configuration files has changed before running the custom code. The sample uses SHA1 file hashing (Utilities/Utilities.cs):

[!code-csharpMain]

A retry is implemented with an exponential back-off. The re-try is present because file locking may occur that temporarily prevents computing a new hash on one of the files.

Simple startup change token

Register a token consumer Action callback for change notifications to the configuration reload token (Startup.cs):

[!code-csharpMain]

config.GetReloadToken() provides the token. The callback is the InvokeChanged method:

[!code-csharpMain]

The state of the callback is used to pass in the IHostingEnvironment. This is useful to determine the correct appsettings configuration JSON file to monitor, appsettings.<Environment>.json. File hashes are used to prevent the WriteConsole statement from running multiple times due to multiple token callbacks when the configuration file has only changed once.

This system runs as long as the app is running and can't be disabled by the user.

Monitoring configuration changes as a service

The sample implements:

  • Basic startup token monitoring.
  • Monitoring as a service.
  • A mechanism to enable and disable monitoring.

The sample establishes an IConfigurationMonitor interface (Extensions/ConfigurationMonitor.cs):

[!code-csharpMain]

The constructor of the implemented class, ConfigurationMonitor, registers a callback for change notifications:

[!code-csharpMain]

config.GetReloadToken() supplies the token. InvokeChanged is the callback method. The state in this instance is a string that describes the monitoring state. Two properties are used:

  • MonitoringEnabled indicates if the callback should run its custom code.
  • CurrentState describes the current monitoring state for use in the UI.

The InvokeChanged method is similar to the earlier approach, except that it:

  • Doesn't run its code unless MonitoringEnabled is true.
  • Sets the CurrentState property string to a descriptive message that records the time that the code ran.
  • Notes the current state in its WriteConsole output.

[!code-csharpMain]

An instance ConfigurationMonitor is registered as a service in ConfigureServices of Startup.cs:

[!code-csharpMain]

The Index page offers the user control over configuration monitoring. The instance of IConfigurationMonitor is injected into the IndexModel:

[!code-csharpMain]

A button enables and disables monitoring:

[!code-cshtmlMain]

[!code-csharpMain]

When OnPostStartMonitoring is triggered, monitoring is enabled, and the current state is cleared. When OnPostStopMonitoring is triggered, monitoring is disabled, and the state is set to reflect that monitoring isn't occurring.

Monitoring cached file changes

File content can be cached in-memory using IMemoryCache. In-memory caching is described in the In-memory caching topic. Without taking additional steps, such as the implementation described below, stale (outdated) data is returned from a cache if the source data changes.

Not taking into account the status of a cached source file when renewing a sliding expiration period leads to stale cache data. Each request for the data renews the sliding expiration period, but the file is never reloaded into the cache. Any app features that use the file's cached content are subject to possibly receiving stale content.

Using change tokens in a file caching scenario prevents stale file content in the cache. The sample app demonstrates an implementation of the approach.

The sample uses GetFileContent to:

  • Return file content.
  • Implement a retry algorithm with exponential back-off to cover cases where a file lock is temporarily preventing a file from being read.

Utilities/Utilities.cs:

[!code-csharpMain]

A FileService is created to handle cached file lookups. The GetFileContent method call of the service attempts to obtain file content from the in-memory cache and return it to the caller (Services/FileService.cs).

If cached content isn't found using the cache key, the following actions are taken:

  1. The file content is obtained using GetFileContent.
  2. A change token is obtained from the file provider with IFileProviders.Watch. The token's callback is triggered when the file is modified.
  3. The file content is cached with a sliding expiration period. The change token is attached with MemoryCacheEntryExtensions.AddExpirationToken to evict the cache entry if the file changes while it's cached.

[!code-csharpMain]

The FileService is registered in the service container along with the memory caching service (Startup.cs):

[!code-csharpMain]

The page model loads the file's content using the service (Pages/Index.cshtml.cs):

[!code-csharpMain]

CompositeChangeToken class

For representing one or more IChangeToken instances in a single object, use the CompositeChangeToken class (reference source).

var firstCancellationTokenSource = new CancellationTokenSource();
var secondCancellationTokenSource = new CancellationTokenSource();

var firstCancellationToken = firstCancellationTokenSource.Token;
var secondCancellationToken = secondCancellationTokenSource.Token;

var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationToken);
var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationToken);

var compositeChangeToken = 
    new CompositeChangeToken(
        new List<IChangeToken> 
        { 
            firstCancellationChangeToken, 
            secondCancellationChangeToken
        });

HasChanged on the composite token reports true if any represented token HasChanged is true. ActiveChangeCallbacks on the composite token reports true if any represented token ActiveChangeCallbacks is true. If multiple concurrent change events occur, the composite change callback is invoked exactly one time.

See also