Обнаружение изменений с помощью токенов изменений в ASP.NET CoreDetect changes with change tokens in ASP.NET Core

Токен изменений — это низкоуровневый стандартный блок общего назначения, используемый для отслеживания изменений.A change token is a general-purpose, low-level building block used to track state changes.

Просмотреть или скачать образец кода (как скачивать)View or download sample code (how to download)

Интерфейс IChangeTokenIChangeToken interface

IChangeToken распространяет уведомления о том, что произошло изменение.IChangeToken propagates notifications that a change has occurred. IChangeToken находится в пространстве имен Microsoft.Extensions.Primitives.IChangeToken resides in the Microsoft.Extensions.Primitives namespace. Пакет Microsoft.Extensions.Primitives NuGet явно предоставляется приложениям ASP.NET Core.The Microsoft.Extensions.Primitives NuGet package is implicitly provided to the ASP.NET Core apps.

IChangeToken имеет два свойства:IChangeToken has two properties:

  • ActiveChangeCallbacks указывает, выполняет ли токен обратные вызовы с упреждением.ActiveChangeCallbacks indicate if the token proactively raises callbacks. Если для ActiveChangedCallbacks задано значение false, обратный вызов не выполняется, а приложению нужно опросить HasChanged на предмет изменений.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 получает значение, указывающее, произошло ли изменение.HasChanged receives a value that indicates if a change has occurred.

Интерфейс IChangeToken включает метод RegisterChangeCallback(Action<Object>, Object), который регистрирует обратный вызов, выполняемый при изменении токена.The IChangeToken interface includes the RegisterChangeCallback(Action<Object>, Object) method, which registers a callback that's invoked when the token has changed. Перед выполнением обратного вызова нужно задать HasChanged.HasChanged must be set before the callback is invoked.

Класс ChangeTokenChangeToken class

ChangeToken — это статический класс, который распространяет уведомления о том, что произошло изменение.ChangeToken is a static class used to propagate notifications that a change has occurred. ChangeToken находится в пространстве имен Microsoft.Extensions.Primitives.ChangeToken resides in the Microsoft.Extensions.Primitives namespace. Пакет Microsoft.Extensions.Primitives NuGet явно предоставляется приложениям ASP.NET Core.The Microsoft.Extensions.Primitives NuGet package is implicitly provided to the ASP.NET Core apps.

Метод ChangeToken.OnChange(Func<IChangeToken>, Action) регистрирует объект Action, вызываемый при изменении токена:The ChangeToken.OnChange(Func<IChangeToken>, Action) method registers an Action to call whenever the token changes:

  • Func<IChangeToken> создает токен.Func<IChangeToken> produces the token.
  • Action вызывается при изменении токена.Action is called when the token changes.

Перегрузка ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) принимает дополнительный параметр TState, передаваемый в объект-получатель токена Action.The ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) overload takes an additional TState parameter that's passed into the token consumer Action.

OnChange возвращает IDisposable.OnChange returns an IDisposable. При вызове Dispose токен прекращает прослушивать дальнейшие изменения и освобождает свои ресурсы.Calling Dispose stops the token from listening for further changes and releases the token's resources.

Примеры использования токенов изменений в ASP.NET CoreExample uses of change tokens in ASP.NET Core

Токены изменений используются в ключевых областях ASP.NET Core для отслеживания изменений в объектах:Change tokens are used in prominent areas of ASP.NET Core to monitor for changes to objects:

  • Чтобы отслеживать изменения в файлах, метод Watch интерфейса IFileProvider создает IChangeToken для указанных файлов или папки.For monitoring changes to files, IFileProvider's Watch method creates an IChangeToken for the specified files or folder to watch.
  • Токены IChangeToken можно добавлять в записи кэша, чтобы активировать вытеснения кэша при изменении.IChangeToken tokens can be added to cache entries to trigger cache evictions on change.
  • Для изменений TOptions используемая по умолчанию реализация OptionsMonitor<TOptions> интерфейса IOptionsMonitor<TOptions> имеет перегрузку, которая принимает один или несколько экземпляров IOptionsChangeTokenSource<TOptions>.For TOptions changes, the default OptionsMonitor<TOptions> implementation of IOptionsMonitor<TOptions> has an overload that accepts one or more IOptionsChangeTokenSource<TOptions> instances. Каждый экземпляр возвращает IChangeToken, чтобы зарегистрировать обратный вызов уведомления об изменении для отслеживания изменений параметров.Each instance returns an IChangeToken to register a change notification callback for tracking options changes.

Отслеживание изменений конфигурацииMonitor for configuration changes

По умолчанию шаблоны ASP.NET Core используют файлы конфигурации JSON (appsettings.json, appsettings.Development.json и appsettings.Production.json) для загрузки параметров конфигурации приложения.By default, ASP.NET Core templates use JSON configuration files (appsettings.json, appsettings.Development.json, and appsettings.Production.json) to load app configuration settings.

Эти файлы настраиваются с помощью метода расширения AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) в ConfigurationBuilder, который принимает параметр reloadOnChange.These files are configured using the AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) extension method on ConfigurationBuilder that accepts a reloadOnChange parameter. reloadOnChange указывает, нужно ли перезагружать конфигурацию при изменении файла.reloadOnChange indicates if configuration should be reloaded on file changes. Этот параметр отображается в удобном методе CreateDefaultBuilder класса Host: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);

Конфигурация на основе файла представлена FileConfigurationSource.File-based configuration is represented by FileConfigurationSource. FileConfigurationSource использует IFileProvider для отслеживания файлов.FileConfigurationSource uses IFileProvider to monitor files.

По умолчанию IFileMonitor предоставляется объектом PhysicalFileProvider, который использует FileSystemWatcher для отслеживания изменений в файле конфигурации.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. Если какой-либо из файлов appsettings был изменен, каждая реализация отслеживания файлов выполняет пользовательский код — пример приложения записывает сообщение на консоль.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.

FileSystemWatcher файла конфигурации может активировать несколько обратных вызовов токена для одного изменения файла конфигурации.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. В примере используется хэширование файлов SHA1.The sample uses SHA1 file hashing. Повторная попытка реализуется посредством экспоненциальной задержки.A retry is implemented with an exponential back-off. Повторная попытка нужна, так как может возникать блокировка файлов, препятствующая вычислению нового хэша в файле.The retry is present because file locking may occur that temporarily prevents computing a new hash on a file.

Utilities/Utilities.cs: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 || ex.HResult != -2147024864)
            {
                throw;
            }
            else
            {
                Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
                runCount++;
            }
        }
    }

    return new byte[20];
}

Токен изменений для простого запускаSimple startup change token

Зарегистрируйте обратный вызов Action объекта-получателя токена для уведомлений об изменениях в токене перезагрузки конфигурации.Register a token consumer Action callback for change notifications to the configuration reload token.

В Startup.Configure:In Startup.Configure:

ChangeToken.OnChange(
    () => config.GetReloadToken(),
    (state) => InvokeChanged(state),
    env);

config.GetReloadToken() предоставляет токен.config.GetReloadToken() provides the token. Обратный вызов является методом InvokeChanged: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)");
    }
}

state обратного вызова используется для передачи IWebHostEnvironment. Это удобно для определения правильного JSON-файла конфигурации appsettings, который требуется отслеживать (например, appsettings.Development.json при работе в среде разработки).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). Хэши файлов используются для предотвращения многократного выполнения оператора WriteConsole из-за нескольких обратных вызовов токена при всего одном изменении файла конфигурации.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.

Этот пример задает интерфейс IConfigurationMonitor.The sample establishes an IConfigurationMonitor interface.

Extensions/ConfigurationMonitor.cs:Extensions/ConfigurationMonitor.cs:

public interface IConfigurationMonitor
{
    bool MonitoringEnabled { get; set; }
    string CurrentState { get; set; }
}

Конструктор реализованного класса ConfigurationMonitor регистрирует обратный вызов для уведомлений об изменениях: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() предоставляет токен.config.GetReloadToken() supplies the token. InvokeChanged является методом обратного вызова.InvokeChanged is the callback method. state в этом экземпляре является ссылкой на экземпляр IConfigurationMonitor, используемый для доступа к состоянию мониторинга.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 – указывает, нужно ли обратному вызову выполнять свой настраиваемый код.MonitoringEnabled – Indicates if the callback should run its custom code.
  • CurrentState – описывает текущее состояние отслеживания для использования в пользовательском интерфейсе.CurrentState – Describes the current monitoring state for use in the UI.

Метод InvokeChanged похож на описанный ранее подход, за исключением того, что он:The InvokeChanged method is similar to the earlier approach, except that it:

  • не выполняет свой код, если только MonitoringEnabled не имеет значение true;Doesn't run its code unless MonitoringEnabled is true.
  • выводит текущее значение state в своих выходных данных WriteConsole.Outputs the current state in its WriteConsole 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}");
        }
    }
}

Экземпляр ConfigurationMonitor регистрируется как служба в Startup.ConfigureServices: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. Экземпляр IConfigurationMonitor внедряется в IndexModel.The instance of IConfigurationMonitor is injected into the IndexModel.

Pages/Index.cshtml.cs:Pages/Index.cshtml.cs:

public IndexModel(
    IConfiguration config, 
    IConfigurationMonitor monitor, 
    FileService fileService)
{
    _config = config;
    _monitor = monitor;
    _fileService = fileService;
}

Монитор конфигурации (_monitor) позволяет включить или отключить мониторинг и задать текущее состояние для обратной связи пользовательского интерфейса: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();
}

При активации OnPostStartMonitoring отслеживание включается, а текущее состояние сбрасывается.When OnPostStartMonitoring is triggered, monitoring is enabled, and the current state is cleared. При активации OnPostStopMonitoring отслеживание отключается, а состояние указывает на отсутствие отслеживания.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: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

Содержимое файла можно кэшировать в памяти с помощью IMemoryCache.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.

GetFileContent в примере используется для:The sample uses GetFileContent to:

  • возврата содержимого файла;Return file content.
  • реализации алгоритма повторных попыток с экспоненциальной задержкой, чтобы охватить ситуации, когда блокировка файла временно препятствует его считыванию.Implement a retry algorithm with exponential back-off to cover cases where a file lock temporarily prevents reading a file.

Utilities/Utilities.cs: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;
}

Для обработки поиска кэшированных файлов создается FileService.A FileService is created to handle cached file lookups. Направленный в службу вызов метода GetFileContent пытается получить содержимое файла из кэша в памяти и вернуть его вызывающему объекту (Services/FileService.cs).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. Содержимое файла получается с помощью GetFileContent.The file content is obtained using GetFileContent.
  2. Токен изменений извлекается из файла поставщика с помощью IFileProviders.Watch.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. Токен изменений подключается к MemoryCacheEntryExtensions.AddExpirationToken для исключения записи кэша, если файл изменяется во время его кэширования.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 используется для получения IFileProvider с указанием на приложение IWebHostEnvironment.ContentRootPath.IWebHostEnvironment.ContentRootFileProvider is used to obtain an IFileProvider pointing at the app's IWebHostEnvironment.ContentRootPath. Для получения filePath используется IFileInfo.PhysicalPath.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;
    }
}

FileService регистрируется в контейнере службы вместе со службой кэширования памяти.The FileService is registered in the service container along with the memory caching service.

В Startup.ConfigureServices:In Startup.ConfigureServices:

services.AddMemoryCache();
services.AddSingleton<FileService>();

Страничная модель загружает содержимое файла с помощью службы.The page model loads the file's content using the service.

На странице индексов используется метод OnGet (Pages/Index.cshtml.cs):In the Index page's OnGet method (Pages/Index.cshtml.cs):

var fileContent = await _fileService.GetFileContents("poem.txt");

Класс CompositeChangeTokenCompositeChangeToken class

Чтобы представить один или несколько экземпляров IChangeToken в одном объекте, используйте класс CompositeChangeToken.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 для составного токена указывает true, если HasChanged любой из представленных токенов имеет значение true.HasChanged on the composite token reports true if any represented token HasChanged is true. ActiveChangeCallbacks для составного токена указывает true, если ActiveChangeCallbacks любой из представленных токенов имеет значение 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)

Интерфейс IChangeTokenIChangeToken interface

IChangeToken распространяет уведомления о том, что произошло изменение.IChangeToken propagates notifications that a change has occurred. IChangeToken находится в пространстве имен Microsoft.Extensions.Primitives.IChangeToken resides in the Microsoft.Extensions.Primitives namespace. Для приложений, которые не используют метапакет Microsoft.AspNetCore.App, создайте ссылку на пакет NuGet Microsoft.Extensions.Primitives.For apps that don't use the Microsoft.AspNetCore.App metapackage, create a package reference for the Microsoft.Extensions.Primitives NuGet package.

IChangeToken имеет два свойства:IChangeToken has two properties:

  • ActiveChangeCallbacks указывает, выполняет ли токен обратные вызовы с упреждением.ActiveChangeCallbacks indicate if the token proactively raises callbacks. Если для ActiveChangedCallbacks задано значение false, обратный вызов не выполняется, а приложению нужно опросить HasChanged на предмет изменений.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 получает значение, указывающее, произошло ли изменение.HasChanged receives a value that indicates if a change has occurred.

Интерфейс IChangeToken включает метод RegisterChangeCallback(Action<Object>, Object), который регистрирует обратный вызов, выполняемый при изменении токена.The IChangeToken interface includes the RegisterChangeCallback(Action<Object>, Object) method, which registers a callback that's invoked when the token has changed. Перед выполнением обратного вызова нужно задать HasChanged.HasChanged must be set before the callback is invoked.

Класс ChangeTokenChangeToken class

ChangeToken — это статический класс, который распространяет уведомления о том, что произошло изменение.ChangeToken is a static class used to propagate notifications that a change has occurred. ChangeToken находится в пространстве имен Microsoft.Extensions.Primitives.ChangeToken resides in the Microsoft.Extensions.Primitives namespace. Для приложений, которые не используют метапакет Microsoft.AspNetCore.App, создайте ссылку на пакет NuGet Microsoft.Extensions.Primitives.For apps that don't use the Microsoft.AspNetCore.App metapackage, create a package reference for the Microsoft.Extensions.Primitives NuGet package.

Метод ChangeToken.OnChange(Func<IChangeToken>, Action) регистрирует объект Action, вызываемый при изменении токена:The ChangeToken.OnChange(Func<IChangeToken>, Action) method registers an Action to call whenever the token changes:

  • Func<IChangeToken> создает токен.Func<IChangeToken> produces the token.
  • Action вызывается при изменении токена.Action is called when the token changes.

Перегрузка ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) принимает дополнительный параметр TState, передаваемый в объект-получатель токена Action.The ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) overload takes an additional TState parameter that's passed into the token consumer Action.

OnChange возвращает IDisposable.OnChange returns an IDisposable. При вызове Dispose токен прекращает прослушивать дальнейшие изменения и освобождает свои ресурсы.Calling Dispose stops the token from listening for further changes and releases the token's resources.

Примеры использования токенов изменений в ASP.NET CoreExample uses of change tokens in ASP.NET Core

Токены изменений используются в ключевых областях ASP.NET Core для отслеживания изменений в объектах:Change tokens are used in prominent areas of ASP.NET Core to monitor for changes to objects:

  • Чтобы отслеживать изменения в файлах, метод Watch интерфейса IFileProvider создает IChangeToken для указанных файлов или папки.For monitoring changes to files, IFileProvider's Watch method creates an IChangeToken for the specified files or folder to watch.
  • Токены IChangeToken можно добавлять в записи кэша, чтобы активировать вытеснения кэша при изменении.IChangeToken tokens can be added to cache entries to trigger cache evictions on change.
  • Для изменений TOptions используемая по умолчанию реализация OptionsMonitor<TOptions> интерфейса IOptionsMonitor<TOptions> имеет перегрузку, которая принимает один или несколько экземпляров IOptionsChangeTokenSource<TOptions>.For TOptions changes, the default OptionsMonitor<TOptions> implementation of IOptionsMonitor<TOptions> has an overload that accepts one or more IOptionsChangeTokenSource<TOptions> instances. Каждый экземпляр возвращает IChangeToken, чтобы зарегистрировать обратный вызов уведомления об изменении для отслеживания изменений параметров.Each instance returns an IChangeToken to register a change notification callback for tracking options changes.

Отслеживание изменений конфигурацииMonitor for configuration changes

По умолчанию шаблоны ASP.NET Core используют файлы конфигурации JSON (appsettings.json, appsettings.Development.json и appsettings.Production.json) для загрузки параметров конфигурации приложения.By default, ASP.NET Core templates use JSON configuration files (appsettings.json, appsettings.Development.json, and appsettings.Production.json) to load app configuration settings.

Эти файлы настраиваются с помощью метода расширения AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) в ConfigurationBuilder, который принимает параметр reloadOnChange.These files are configured using the AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) extension method on ConfigurationBuilder that accepts a reloadOnChange parameter. reloadOnChange указывает, нужно ли перезагружать конфигурацию при изменении файла.reloadOnChange indicates if configuration should be reloaded on file changes. Этот параметр отображается в удобном методе CreateDefaultBuilder класса WebHost: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);

Конфигурация на основе файла представлена FileConfigurationSource.File-based configuration is represented by FileConfigurationSource. FileConfigurationSource использует IFileProvider для отслеживания файлов.FileConfigurationSource uses IFileProvider to monitor files.

По умолчанию IFileMonitor предоставляется объектом PhysicalFileProvider, который использует FileSystemWatcher для отслеживания изменений в файле конфигурации.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. Если какой-либо из файлов appsettings был изменен, каждая реализация отслеживания файлов выполняет пользовательский код — пример приложения записывает сообщение на консоль.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.

FileSystemWatcher файла конфигурации может активировать несколько обратных вызовов токена для одного изменения файла конфигурации.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. В примере используется хэширование файлов SHA1.The sample uses SHA1 file hashing. Повторная попытка реализуется посредством экспоненциальной задержки.A retry is implemented with an exponential back-off. Повторная попытка нужна, так как может возникать блокировка файлов, препятствующая вычислению нового хэша в файле.The retry is present because file locking may occur that temporarily prevents computing a new hash on a file.

Utilities/Utilities.cs: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 || ex.HResult != -2147024864)
            {
                throw;
            }
            else
            {
                Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, runCount)));
                runCount++;
            }
        }
    }

    return new byte[20];
}

Токен изменений для простого запускаSimple startup change token

Зарегистрируйте обратный вызов Action объекта-получателя токена для уведомлений об изменениях в токене перезагрузки конфигурации.Register a token consumer Action callback for change notifications to the configuration reload token.

В Startup.Configure:In Startup.Configure:

ChangeToken.OnChange(
    () => config.GetReloadToken(),
    (state) => InvokeChanged(state),
    env);

config.GetReloadToken() предоставляет токен.config.GetReloadToken() provides the token. Обратный вызов является методом InvokeChanged: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)");
    }
}

state обратного вызова используется для передачи IHostingEnvironment. Это удобно для определения правильного JSON-файла конфигурации appsettings, который требуется отслеживать (например, appsettings.Development.json при работе в среде разработки).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). Хэши файлов используются для предотвращения многократного выполнения оператора WriteConsole из-за нескольких обратных вызовов токена при всего одном изменении файла конфигурации.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.

Этот пример задает интерфейс IConfigurationMonitor.The sample establishes an IConfigurationMonitor interface.

Extensions/ConfigurationMonitor.cs:Extensions/ConfigurationMonitor.cs:

public interface IConfigurationMonitor
{
    bool MonitoringEnabled { get; set; }
    string CurrentState { get; set; }
}

Конструктор реализованного класса ConfigurationMonitor регистрирует обратный вызов для уведомлений об изменениях: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() предоставляет токен.config.GetReloadToken() supplies the token. InvokeChanged является методом обратного вызова.InvokeChanged is the callback method. state в этом экземпляре является ссылкой на экземпляр IConfigurationMonitor, используемый для доступа к состоянию мониторинга.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 – указывает, нужно ли обратному вызову выполнять свой настраиваемый код.MonitoringEnabled – Indicates if the callback should run its custom code.
  • CurrentState – описывает текущее состояние отслеживания для использования в пользовательском интерфейсе.CurrentState – Describes the current monitoring state for use in the UI.

Метод InvokeChanged похож на описанный ранее подход, за исключением того, что он:The InvokeChanged method is similar to the earlier approach, except that it:

  • не выполняет свой код, если только MonitoringEnabled не имеет значение true;Doesn't run its code unless MonitoringEnabled is true.
  • выводит текущее значение state в своих выходных данных WriteConsole.Outputs the current state in its WriteConsole 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}");
        }
    }
}

Экземпляр ConfigurationMonitor регистрируется как служба в Startup.ConfigureServices: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. Экземпляр IConfigurationMonitor внедряется в IndexModel.The instance of IConfigurationMonitor is injected into the IndexModel.

Pages/Index.cshtml.cs:Pages/Index.cshtml.cs:

public IndexModel(
    IConfiguration config, 
    IConfigurationMonitor monitor, 
    FileService fileService)
{
    _config = config;
    _monitor = monitor;
    _fileService = fileService;
}

Монитор конфигурации (_monitor) позволяет включить или отключить мониторинг и задать текущее состояние для обратной связи пользовательского интерфейса: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();
}

При активации OnPostStartMonitoring отслеживание включается, а текущее состояние сбрасывается.When OnPostStartMonitoring is triggered, monitoring is enabled, and the current state is cleared. При активации OnPostStopMonitoring отслеживание отключается, а состояние указывает на отсутствие отслеживания.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: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

Содержимое файла можно кэшировать в памяти с помощью IMemoryCache.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.

GetFileContent в примере используется для:The sample uses GetFileContent to:

  • возврата содержимого файла;Return file content.
  • реализации алгоритма повторных попыток с экспоненциальной задержкой, чтобы охватить ситуации, когда блокировка файла временно препятствует его считыванию.Implement a retry algorithm with exponential back-off to cover cases where a file lock temporarily prevents reading a file.

Utilities/Utilities.cs: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;
}

Для обработки поиска кэшированных файлов создается FileService.A FileService is created to handle cached file lookups. Направленный в службу вызов метода GetFileContent пытается получить содержимое файла из кэша в памяти и вернуть его вызывающему объекту (Services/FileService.cs).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. Содержимое файла получается с помощью GetFileContent.The file content is obtained using GetFileContent.
  2. Токен изменений извлекается из файла поставщика с помощью IFileProviders.Watch.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. Токен изменений подключается к MemoryCacheEntryExtensions.AddExpirationToken для исключения записи кэша, если файл изменяется во время его кэширования.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 используется для получения объекта IFileProvider, который указывает на ContentRootPath приложения.IHostingEnvironment.ContentRootFileProvider is used to obtain an IFileProvider pointing at the app's ContentRootPath. Для получения filePath используется IFileInfo.PhysicalPath.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;
    }
}

FileService регистрируется в контейнере службы вместе со службой кэширования памяти.The FileService is registered in the service container along with the memory caching service.

В Startup.ConfigureServices:In Startup.ConfigureServices:

services.AddMemoryCache();
services.AddSingleton<FileService>();

Страничная модель загружает содержимое файла с помощью службы.The page model loads the file's content using the service.

На странице индексов используется метод OnGet (Pages/Index.cshtml.cs):In the Index page's OnGet method (Pages/Index.cshtml.cs):

var fileContent = await _fileService.GetFileContents("poem.txt");

Класс CompositeChangeTokenCompositeChangeToken class

Чтобы представить один или несколько экземпляров IChangeToken в одном объекте, используйте класс CompositeChangeToken.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 для составного токена указывает true, если HasChanged любой из представленных токенов имеет значение true.HasChanged on the composite token reports true if any represented token HasChanged is true. ActiveChangeCallbacks для составного токена указывает true, если ActiveChangeCallbacks любой из представленных токенов имеет значение 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