Detect changes with change tokens in ASP.NET Core
Note
This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see .NET and .NET Core Support Policy. For the current release, see the .NET 8 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 8 version of this article.
A change token is a general-purpose, low-level building block used to track state 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. The Microsoft.Extensions.Primitives NuGet package is implicitly provided to the ASP.NET Core apps.
IChangeToken
has two properties:
- ActiveChangeCallbacks indicate if the token proactively raises callbacks. If
ActiveChangedCallbacks
is set tofalse
, a callback is never called, and the app must pollHasChanged
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 receives a value that indicates if a change has occurred.
The IChangeToken
interface includes the RegisterChangeCallback(Action<Object>, Object) method, 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. The Microsoft.Extensions.Primitives NuGet package is implicitly provided to the ASP.NET Core apps.
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.
The ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) overload 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 to monitor for 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<TOptions> implementation of IOptionsMonitor<TOptions> has an overload that accepts one or more IOptionsChangeTokenSource<TOptions> instances. Each instance returns anIChangeToken
to register a change notification callback for tracking options changes.
Monitor 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. reloadOnChange
indicates if configuration should be reloaded on file changes. This setting appears in the Host convenience method CreateDefaultBuilder:
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 to monitor files.
By default, the IFileMonitor
is provided by a PhysicalFileProvider, which uses FileSystemWatcher to monitor for configuration file changes.
The sample app demonstrates two implementations for monitoring configuration changes. If any of the appsettings
files change, both of the file monitoring implementations execute 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. To ensure that the custom code is only run once when multiple token callbacks are triggered, the sample's implementation checks file hashes. The sample uses SHA1 file hashing. A retry is implemented with an exponential back-off.
Utilities/Utilities.cs
:
public static byte[] ComputeHash(string filePath)
{
var runCount = 1;
while(runCount < 4)
{
try
{
if (File.Exists(filePath))
{
using (var fs = File.OpenRead(filePath))
{
return System.Security.Cryptography.SHA1
.Create().ComputeHash(fs);
}
}
else
{
throw new FileNotFoundException();
}
}
catch (IOException ex)
{
if (runCount == 3)
{
throw;
}
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
runCount++;
}
}
return new byte[20];
}
Simple startup change token
Register a token consumer Action
callback for change notifications to the configuration reload token.
In Startup.Configure
:
ChangeToken.OnChange(
() => config.GetReloadToken(),
(state) => InvokeChanged(state),
env);
config.GetReloadToken()
provides the token. The callback is the InvokeChanged
method:
private void InvokeChanged(IWebHostEnvironment env)
{
byte[] appsettingsHash = ComputeHash("appSettings.json");
byte[] appsettingsEnvHash =
ComputeHash($"appSettings.{env.EnvironmentName}.json");
if (!_appsettingsHash.SequenceEqual(appsettingsHash) ||
!_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
{
_appsettingsHash = appsettingsHash;
_appsettingsEnvHash = appsettingsEnvHash;
WriteConsole("Configuration changed (Simple Startup Change Token)");
}
}
The state
of the callback is used to pass in the IWebHostEnvironment
, which is useful for specifying the correct appsettings
configuration file to monitor (for example, appsettings.Development.json
when in the Development environment). 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.
Monitor 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
:
public interface IConfigurationMonitor
{
bool MonitoringEnabled { get; set; }
string CurrentState { get; set; }
}
The constructor of the implemented class, ConfigurationMonitor
, registers a callback for change notifications:
public ConfigurationMonitor(IConfiguration config, IWebHostEnvironment env)
{
_env = env;
ChangeToken.OnChange<IConfigurationMonitor>(
() => config.GetReloadToken(),
InvokeChanged,
this);
}
public bool MonitoringEnabled { get; set; } = false;
public string CurrentState { get; set; } = "Not monitoring";
config.GetReloadToken()
supplies the token. InvokeChanged
is the callback method. The state
in this instance is a reference to the IConfigurationMonitor
instance that's used to access 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
istrue
. - Outputs the current
state
in itsWriteConsole
output.
private void InvokeChanged(IConfigurationMonitor state)
{
if (MonitoringEnabled)
{
byte[] appsettingsHash = ComputeHash("appSettings.json");
byte[] appsettingsEnvHash =
ComputeHash($"appSettings.{_env.EnvironmentName}.json");
if (!_appsettingsHash.SequenceEqual(appsettingsHash) ||
!_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
{
string message = $"State updated at {DateTime.Now}";
_appsettingsHash = appsettingsHash;
_appsettingsEnvHash = appsettingsEnvHash;
WriteConsole("Configuration changed (ConfigurationMonitor Class) " +
$"{message}, state:{state.CurrentState}");
}
}
}
An instance ConfigurationMonitor
is registered as a service in Startup.ConfigureServices
:
services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();
The Index page offers the user control over configuration monitoring. The instance of IConfigurationMonitor
is injected into the IndexModel
.
Pages/Index.cshtml.cs
:
public IndexModel(
IConfiguration config,
IConfigurationMonitor monitor,
FileService fileService)
{
_config = config;
_monitor = monitor;
_fileService = fileService;
}
The configuration monitor (_monitor
) is used to enable or disable monitoring and set the current state for UI feedback:
public IActionResult OnPostStartMonitoring()
{
_monitor.MonitoringEnabled = true;
_monitor.CurrentState = "Monitoring!";
return RedirectToPage();
}
public IActionResult OnPostStopMonitoring()
{
_monitor.MonitoringEnabled = false;
_monitor.CurrentState = "Not monitoring";
return RedirectToPage();
}
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.
Buttons in the UI enable and disable monitoring.
Pages/Index.cshtml
:
<button class="btn btn-success" asp-page-handler="StartMonitoring">
Start Monitoring
</button>
<button class="btn btn-danger" asp-page-handler="StopMonitoring">
Stop Monitoring
</button>
Monitor cached file changes
File content can be cached in-memory using IMemoryCache. In-memory caching is described in the Cache in-memory topic. Without taking additional steps, such as the implementation described below, stale (outdated) data is returned from a cache if the source data changes.
For example, not taking into account the status of a cached source file when renewing a sliding expiration period leads to stale cached file 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 the presence of 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 access problem temporarily delays reading the file's content.
Utilities/Utilities.cs
:
public async static Task<string> GetFileContent(string filePath)
{
var runCount = 1;
while(runCount < 4)
{
try
{
if (File.Exists(filePath))
{
using (var fileStreamReader = File.OpenText(filePath))
{
return await fileStreamReader.ReadToEndAsync();
}
}
else
{
throw new FileNotFoundException();
}
}
catch (IOException ex)
{
if (runCount == 3)
{
throw;
}
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
runCount++;
}
}
return null;
}
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:
- The file content is obtained using
GetFileContent
. - A change token is obtained from the file provider with IFileProviders.Watch. The token's callback is triggered when the file is modified.
- 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.
In the following example, files are stored in the app's content root. IWebHostEnvironment.ContentRootFileProvider
is used to obtain an IFileProvider pointing at the app's IWebHostEnvironment.ContentRootPath
. The filePath
is obtained with IFileInfo.PhysicalPath.
public class FileService
{
private readonly IMemoryCache _cache;
private readonly IFileProvider _fileProvider;
private List<string> _tokens = new List<string>();
public FileService(IMemoryCache cache, IWebHostEnvironment env)
{
_cache = cache;
_fileProvider = env.ContentRootFileProvider;
}
public async Task<string> GetFileContents(string fileName)
{
var filePath = _fileProvider.GetFileInfo(fileName).PhysicalPath;
string fileContent;
// Try to obtain the file contents from the cache.
if (_cache.TryGetValue(filePath, out fileContent))
{
return fileContent;
}
// The cache doesn't have the entry, so obtain the file
// contents from the file itself.
fileContent = await GetFileContent(filePath);
if (fileContent != null)
{
// Obtain a change token from the file provider whose
// callback is triggered when the file is modified.
var changeToken = _fileProvider.Watch(fileName);
// Configure the cache entry options for a five minute
// sliding expiration and use the change token to
// expire the file in the cache if the file is
// modified.
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
.AddExpirationToken(changeToken);
// Put the file contents into the cache.
_cache.Set(filePath, fileContent, cacheEntryOptions);
return fileContent;
}
return string.Empty;
}
}
The FileService
is registered in the service container along with the memory caching service.
In Startup.ConfigureServices
:
services.AddMemoryCache();
services.AddSingleton<FileService>();
The page model loads the file's content using the service.
In the Index page's OnGet
method (Pages/Index.cshtml.cs
):
var fileContent = await _fileService.GetFileContents("poem.txt");
CompositeChangeToken class
For representing one or more IChangeToken
instances in a single object, use the CompositeChangeToken class.
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 one time.
A change token is a general-purpose, low-level building block used to track state 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.App metapackage, create a package reference for the Microsoft.Extensions.Primitives NuGet package.
IChangeToken
has two properties:
- ActiveChangeCallbacks indicate if the token proactively raises callbacks. If
ActiveChangedCallbacks
is set tofalse
, a callback is never called, and the app must pollHasChanged
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 receives a value that indicates if a change has occurred.
The IChangeToken
interface includes the RegisterChangeCallback(Action<Object>, Object) method, 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.App metapackage, create a package reference for the Microsoft.Extensions.Primitives NuGet package.
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.
The ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) overload 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 to monitor for 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<TOptions> implementation of IOptionsMonitor<TOptions> has an overload that accepts one or more IOptionsChangeTokenSource<TOptions> instances. Each instance returns anIChangeToken
to register a change notification callback for tracking options changes.
Monitor 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. reloadOnChange
indicates if configuration should be reloaded on file changes. This setting appears in the WebHost convenience method CreateDefaultBuilder:
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 to monitor files.
By default, the IFileMonitor
is provided by a PhysicalFileProvider, which uses FileSystemWatcher to monitor for configuration file changes.
The sample app demonstrates two implementations for monitoring configuration changes. If any of the appsettings
files change, both of the file monitoring implementations execute 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. To ensure that the custom code is only run once when multiple token callbacks are triggered, the sample's implementation checks file hashes. The sample uses SHA1 file hashing. A retry is implemented with an exponential back-off.
Utilities/Utilities.cs
:
public static byte[] ComputeHash(string filePath)
{
var runCount = 1;
while(runCount < 4)
{
try
{
if (File.Exists(filePath))
{
using (var fs = File.OpenRead(filePath))
{
return System.Security.Cryptography.SHA1
.Create().ComputeHash(fs);
}
}
else
{
throw new FileNotFoundException();
}
}
catch (IOException ex)
{
if (runCount == 3)
{
throw;
}
Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
runCount++;
}
}
return new byte[20];
}
Simple startup change token
Register a token consumer Action
callback for change notifications to the configuration reload token.
In Startup.Configure
:
ChangeToken.OnChange(
() => config.GetReloadToken(),
(state) => InvokeChanged(state),
env);
config.GetReloadToken()
provides the token. The callback is the InvokeChanged
method:
private void InvokeChanged(IHostingEnvironment env)
{
byte[] appsettingsHash = ComputeHash("appSettings.json");
byte[] appsettingsEnvHash =
ComputeHash($"appSettings.{env.EnvironmentName}.json");
if (!_appsettingsHash.SequenceEqual(appsettingsHash) ||
!_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
{
_appsettingsHash = appsettingsHash;
_appsettingsEnvHash = appsettingsEnvHash;
WriteConsole("Configuration changed (Simple Startup Change Token)");
}
}
The state
of the callback is used to pass in the IHostingEnvironment
, which is useful for specifying the correct appsettings
configuration file to monitor (for example, appsettings.Development.json
when in the Development environment). 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.
Monitor 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
:
public interface IConfigurationMonitor
{
bool MonitoringEnabled { get; set; }
string CurrentState { get; set; }
}
The constructor of the implemented class, ConfigurationMonitor
, registers a callback for change notifications:
public ConfigurationMonitor(IConfiguration config, IHostingEnvironment env)
{
_env = env;
ChangeToken.OnChange<IConfigurationMonitor>(
() => config.GetReloadToken(),
InvokeChanged,
this);
}
public bool MonitoringEnabled { get; set; } = false;
public string CurrentState { get; set; } = "Not monitoring";
config.GetReloadToken()
supplies the token. InvokeChanged
is the callback method. The state
in this instance is a reference to the IConfigurationMonitor
instance that's used to access 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
istrue
. - Outputs the current
state
in itsWriteConsole
output.
private void InvokeChanged(IConfigurationMonitor state)
{
if (MonitoringEnabled)
{
byte[] appsettingsHash = ComputeHash("appSettings.json");
byte[] appsettingsEnvHash =
ComputeHash($"appSettings.{_env.EnvironmentName}.json");
if (!_appsettingsHash.SequenceEqual(appsettingsHash) ||
!_appsettingsEnvHash.SequenceEqual(appsettingsEnvHash))
{
string message = $"State updated at {DateTime.Now}";
_appsettingsHash = appsettingsHash;
_appsettingsEnvHash = appsettingsEnvHash;
WriteConsole("Configuration changed (ConfigurationMonitor Class) " +
$"{message}, state:{state.CurrentState}");
}
}
}
An instance ConfigurationMonitor
is registered as a service in Startup.ConfigureServices
:
services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();
The Index page offers the user control over configuration monitoring. The instance of IConfigurationMonitor
is injected into the IndexModel
.
Pages/Index.cshtml.cs
:
public IndexModel(
IConfiguration config,
IConfigurationMonitor monitor,
FileService fileService)
{
_config = config;
_monitor = monitor;
_fileService = fileService;
}
The configuration monitor (_monitor
) is used to enable or disable monitoring and set the current state for UI feedback:
public IActionResult OnPostStartMonitoring()
{
_monitor.MonitoringEnabled = true;
_monitor.CurrentState = "Monitoring!";
return RedirectToPage();
}
public IActionResult OnPostStopMonitoring()
{
_monitor.MonitoringEnabled = false;
_monitor.CurrentState = "Not monitoring";
return RedirectToPage();
}
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.
Buttons in the UI enable and disable monitoring.
Pages/Index.cshtml
:
<button class="btn btn-success" asp-page-handler="StartMonitoring">
Start Monitoring
</button>
<button class="btn btn-danger" asp-page-handler="StopMonitoring">
Stop Monitoring
</button>
Monitor cached file changes
File content can be cached in-memory using IMemoryCache. In-memory caching is described in the Cache in-memory topic. Without taking additional steps, such as the implementation described below, stale (outdated) data is returned from a cache if the source data changes.
For example, not taking into account the status of a cached source file when renewing a sliding expiration period leads to stale cached file 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 the presence of 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 access problem temporarily delays reading the file's content.
Utilities/Utilities.cs
:
public async static Task<string> GetFileContent(string filePath)
{
var runCount = 1;
while(runCount < 4)
{
try
{
if (File.Exists(filePath))
{
using (var fileStreamReader = File.OpenText(filePath))
{
return await fileStreamReader.ReadToEndAsync();
}
}
else
{
throw new FileNotFoundException();
}
}
catch (IOException ex)
{
if (runCount == 3 || ex.HResult != -2147024864)
{
throw;
}
else
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
runCount++;
}
}
}
return null;
}
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:
- The file content is obtained using
GetFileContent
. - A change token is obtained from the file provider with IFileProviders.Watch. The token's callback is triggered when the file is modified.
- 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.
In the following example, files are stored in the app's content root. IHostingEnvironment.ContentRootFileProvider is used to obtain an IFileProvider pointing at the app's ContentRootPath. The filePath
is obtained with IFileInfo.PhysicalPath.
public class FileService
{
private readonly IMemoryCache _cache;
private readonly IFileProvider _fileProvider;
private List<string> _tokens = new List<string>();
public FileService(IMemoryCache cache, IHostingEnvironment env)
{
_cache = cache;
_fileProvider = env.ContentRootFileProvider;
}
public async Task<string> GetFileContents(string fileName)
{
var filePath = _fileProvider.GetFileInfo(fileName).PhysicalPath;
string fileContent;
// Try to obtain the file contents from the cache.
if (_cache.TryGetValue(filePath, out fileContent))
{
return fileContent;
}
// The cache doesn't have the entry, so obtain the file
// contents from the file itself.
fileContent = await GetFileContent(filePath);
if (fileContent != null)
{
// Obtain a change token from the file provider whose
// callback is triggered when the file is modified.
var changeToken = _fileProvider.Watch(fileName);
// Configure the cache entry options for a five minute
// sliding expiration and use the change token to
// expire the file in the cache if the file is
// modified.
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5))
.AddExpirationToken(changeToken);
// Put the file contents into the cache.
_cache.Set(filePath, fileContent, cacheEntryOptions);
return fileContent;
}
return string.Empty;
}
}
The FileService
is registered in the service container along with the memory caching service.
In Startup.ConfigureServices
:
services.AddMemoryCache();
services.AddSingleton<FileService>();
The page model loads the file's content using the service.
In the Index page's OnGet
method (Pages/Index.cshtml.cs
):
var fileContent = await _fileService.GetFileContents("poem.txt");
CompositeChangeToken class
For representing one or more IChangeToken
instances in a single object, use the CompositeChangeToken class.
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 one time.
Additional resources
ASP.NET Core