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

Note

Это не последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.

Warning

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.

Токен изменений — это низкоуровневый стандартный блок общего назначения, используемый для отслеживания изменений.

Просмотреть или скачать образец кода (описание загрузки)

Интерфейс IChangeToken

IChangeToken распространяет уведомления о том, что произошло изменение. IChangeToken находится в пространстве имен Microsoft.Extensions.Primitives. Пакет NuGet Microsoft.Extensions.Primitives неявно предоставляется для приложений ASP.NET Core.

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

  • ActiveChangeCallbacks указывает, инициирует ли токен обратные вызовы заранее. Если для ActiveChangedCallbacks задано значение false, обратный вызов не выполняется, а приложению нужно опросить HasChanged на предмет изменений. Токен также может никогда не быть отменён, если изменения не происходят или базовый обработчик изменений освобождён либо отключён.
  • HasChanged получает значение, указывающее, произошло ли изменение.

Интерфейс IChangeToken включает метод RegisterChangeCallback(Action<Object>, Object), который регистрирует обратный вызов, выполняемый при изменении токена. Перед выполнением обратного вызова нужно задать HasChanged.

Класс ChangeToken

ChangeToken — это статический класс, который распространяет уведомления о том, что произошло изменение. ChangeToken находится в пространстве имен Microsoft.Extensions.Primitives. Пакет NuGet Microsoft.Extensions.Primitives неявно включён в приложения ASP.NET Core.

Метод ChangeToken.OnChange(Func<IChangeToken>, Action) регистрирует объект Action, вызываемый при изменении токена:

  • Func<IChangeToken> создает токен.
  • Action вызывается при изменении токена.

Перегрузка ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) принимает дополнительный параметр TState, который передаётся потребителю токена Action.

OnChange возвращает IDisposable. При вызове Dispose токен прекращает прослушивать дальнейшие изменения и освобождает свои ресурсы.

Примеры использования токенов изменений в ASP.NET Core

Токены изменений используются в ключевых областях ASP.NET Core для отслеживания изменений в объектах:

  • Чтобы отслеживать изменения в файлах, метод Watch объекта IFileProvider создает объект IChangeToken для указанных файлов или папки, за которыми нужно следить.
  • IChangeToken токены можно добавлять к записям кэша, чтобы при изменении инициировать удаление записей из кэша.
  • Для изменений TOptions используемая по умолчанию реализация OptionsMonitor<TOptions> интерфейса IOptionsMonitor<TOptions> имеет перегрузку, которая принимает один или несколько экземпляров IOptionsChangeTokenSource<TOptions>. Каждый экземпляр возвращает IChangeToken, чтобы зарегистрировать обратный вызов уведомления об изменении для отслеживания изменений параметров.

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

По умолчанию шаблоны ASP.NET Core используют файлы конфигурации JSON (appsettings.json, appsettings.Development.json и appsettings.Production.json) для загрузки параметров конфигурации приложения.

Эти файлы настраиваются с помощью метода расширения AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) в ConfigurationBuilder, который принимает параметр reloadOnChange. reloadOnChange указывает, нужно ли перезагружать конфигурацию при изменении файла. Эта настройка отображается во вспомогательном методе HostCreateDefaultBuilder:

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

Файловая конфигурация представлена FileConfigurationSource. FileConfigurationSource использует IFileProvider для отслеживания файлов.

По умолчанию IFileMonitor предоставляется объектом PhysicalFileProvider, который использует FileSystemWatcher для отслеживания изменений в файле конфигурации.

Этот пример приложения демонстрирует две реализации для отслеживания изменений конфигурации. Если любой из appsettings файлов изменяется, обе реализации мониторинга файлов выполняют пользовательский код— пример приложения записывает сообщение в консоль.

FileSystemWatcher файла конфигурации может активировать несколько обратных вызовов токена для одного изменения файла конфигурации. Чтобы пользовательский код выполнялся один раз при активации нескольких обратных вызовов токена, реализация в примере проверяет хэши файлов. В примере используется хэширование файлов SHA1. Повторные попытки выполняются с экспоненциальным увеличением интервала между ними.

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];
}

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

Зарегистрируйте обратный вызов Action получателя токена для получения уведомлений об изменениях от токена перезагрузки конфигурации.

В Startup.Configure:

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

config.GetReloadToken() предоставляет токен. В качестве обратного вызова используется метод InvokeChanged:

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

Эта система выполняется, пока запущено приложение, и не может быть отключена пользователем.

Отслеживание изменений конфигурации в качестве службы

Этот пример реализует следующее:

  • Базовый мониторинг токена запуска.
  • Отслеживание как служба.
  • Механизм для включения и отключения отслеживания.

Этот пример определяет IConfigurationMonitor интерфейс.

Extensions/ConfigurationMonitor.cs:

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

Конструктор реализованного класса ConfigurationMonitor регистрирует обратный вызов для уведомлений об изменениях:

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() предоставляет токен. InvokeChanged является методом обратного вызова. В данном случае state — это ссылка на экземпляр IConfigurationMonitor, который используется для доступа к состоянию мониторинга. Используются два свойства:

  • MonitoringEnabled: указывает, должна ли функция обратного вызова выполнять свой пользовательский код.
  • CurrentState: описывает текущее состояние мониторинга для использования в пользовательском интерфейсе.

Метод InvokeChanged похож на описанный ранее подход, за исключением того, что он:

  • Не выполняет свой код, если только MonitoringEnabled не равно true.
  • выводит текущее значение state в своих выходных данных WriteConsole.
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:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

Страница индекса позволяет пользователю управлять отслеживанием конфигурации. Экземпляр IConfigurationMonitor внедряется в IndexModel.

Pages/Index.cshtml.cs:

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

Монитор конфигурации (_monitor) позволяет включить или отключить мониторинг и задать текущее состояние для обратной связи пользовательского интерфейса:

public IActionResult OnPostStartMonitoring()
{
    _monitor.MonitoringEnabled = true;
    _monitor.CurrentState = "Monitoring!";

    return RedirectToPage();
}

public IActionResult OnPostStopMonitoring()
{
    _monitor.MonitoringEnabled = false;
    _monitor.CurrentState = "Not monitoring";

    return RedirectToPage();
}

При активации OnPostStartMonitoring отслеживание включается, а текущее состояние сбрасывается. При активации OnPostStopMonitoring отслеживание отключается, а состояние указывает на отсутствие отслеживания.

Кнопки пользовательского интерфейса для включения и отключения отслеживания.

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>

Отслеживание изменений кэшированных файлов

Содержимое файла можно кэшировать в памяти с помощью IMemoryCache. Кэширование в памяти описано в разделе Кэш в памяти. Устаревшие (просроченные) данные возвращаются из кэша при изменении источника исходных данных без каких-либо дополнительных действий, таких как описанная ниже реализация.

Например, если не учитывать состояние кэшированного исходного файла при продлении скользящего срока действия, это приведет к появлению в кэше устаревших данных файлов. Каждый запрос к данным продляет скользящий срок действия, но этот файл никогда не загружается в кэш. Любые функции приложения, использующие кэшированное содержимое, могут получить устаревшее содержимое.

Использование токенов изменений в сценарии кэширования файлов предотвращает появление устаревшего содержимого файлов в кэше. Этот пример демонстрирует реализацию данного подхода.

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

  • Вернуть содержимое файла.
  • Реализуйте алгоритм повторных попыток с экспоненциальной задержкой для обработки случаев, когда проблема доступа к файлу временно препятствует чтению его содержимого.

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;
}

Для поиска кэшированных файлов создаётся FileService. Направленный в службу вызов метода GetFileContent пытается получить содержимое файла из кэша в памяти и вернуть его вызывающему объекту (Services/FileService.cs).

Если кэшированное содержимое не удается найти по ключу кэша, выполняются следующие действия:

  1. Содержимое файла получается с помощью GetFileContent.
  2. Токен изменений получается от поставщика файлов с помощью IFileProviders.Watch. При изменении файла активируется обратный вызов токена.
  3. Содержимое файла кэшируется с использованием скользящего срока действия. Токен изменений подключается к MemoryCacheEntryExtensions.AddExpirationToken для исключения записи кэша, если файл изменяется во время его кэширования.

В следующем примере файлы хранятся в корневом каталоге содержимого приложения. IWebHostEnvironment.ContentRootFileProvider используется для получения IFileProvider, указывающего на IWebHostEnvironment.ContentRootPath приложения. Для получения filePath используется 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 регистрируется в контейнере служб вместе со службой кэширования в памяти.

В Startup.ConfigureServices:

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

Страничная модель загружает содержимое файла с помощью службы.

В методе OnGet страницы Index (Pages/Index.cshtml.cs):

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

Класс CompositeChangeToken

Чтобы представить один или несколько экземпляров IChangeToken в одном объекте, используйте класс CompositeChangeToken.

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. ActiveChangeCallbacks у составного токена указывает true, если любой из представленных токенов ActiveChangeCallbacks имеет значение true. Если возникает несколько одновременных событий изменения, обратный вызов составного изменения выполняется один раз.

Токен изменений — это низкоуровневый стандартный блок общего назначения, используемый для отслеживания изменений.

Просмотреть или скачать образец кода (описание загрузки)

Интерфейс IChangeToken

IChangeToken распространяет уведомления о том, что произошло изменение. IChangeToken находится в пространстве имен Microsoft.Extensions.Primitives. Для приложений, которые не используют метапакет Microsoft.AspNetCore.App, создайте ссылку на пакет NuGet Microsoft.Extensions.Primitives.

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

  • ActiveChangeCallbacks указывает, инициирует ли токен обратные вызовы заранее. Если для ActiveChangedCallbacks задано значение false, обратный вызов не выполняется, а приложению нужно опросить HasChanged на предмет изменений. Токен также может никогда не быть отменён, если изменения не происходят или базовый обработчик изменений освобождён либо отключён.
  • HasChanged получает значение, указывающее, произошло ли изменение.

Интерфейс IChangeToken включает метод RegisterChangeCallback(Action<Object>, Object), который регистрирует обратный вызов, выполняемый при изменении токена. Перед выполнением обратного вызова нужно задать HasChanged.

Класс ChangeToken

ChangeToken — это статический класс, который распространяет уведомления о том, что произошло изменение. ChangeToken находится в пространстве имен Microsoft.Extensions.Primitives. Для приложений, которые не используют метапакет Microsoft.AspNetCore.App, создайте ссылку на пакет NuGet Microsoft.Extensions.Primitives.

Метод ChangeToken.OnChange(Func<IChangeToken>, Action) регистрирует объект Action, вызываемый при изменении токена:

  • Func<IChangeToken> создает токен.
  • Action вызывается при изменении токена.

Перегрузка ChangeToken.OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) принимает дополнительный параметр TState, который передаётся потребителю токена Action.

OnChange возвращает IDisposable. При вызове Dispose токен прекращает прослушивать дальнейшие изменения и освобождает свои ресурсы.

Примеры использования токенов изменений в ASP.NET Core

Токены изменений используются в ключевых областях ASP.NET Core для отслеживания изменений в объектах:

  • Чтобы отслеживать изменения в файлах, метод Watch объекта IFileProvider создает объект IChangeToken для указанных файлов или папки, за которыми нужно следить.
  • IChangeToken токены можно добавлять к записям кэша, чтобы при изменении инициировать удаление записей из кэша.
  • Для изменений TOptions используемая по умолчанию реализация OptionsMonitor<TOptions> интерфейса IOptionsMonitor<TOptions> имеет перегрузку, которая принимает один или несколько экземпляров IOptionsChangeTokenSource<TOptions>. Каждый экземпляр возвращает IChangeToken, чтобы зарегистрировать обратный вызов уведомления об изменении для отслеживания изменений параметров.

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

По умолчанию шаблоны ASP.NET Core используют файлы конфигурации JSON (appsettings.json, appsettings.Development.json и appsettings.Production.json) для загрузки параметров конфигурации приложения.

Эти файлы настраиваются с помощью метода расширения AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) в ConfigurationBuilder, который принимает параметр reloadOnChange. reloadOnChange указывает, нужно ли перезагружать конфигурацию при изменении файла. Эта настройка отображается во вспомогательном методе WebHostCreateDefaultBuilder:

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

Файловая конфигурация представлена FileConfigurationSource. FileConfigurationSource использует IFileProvider для отслеживания файлов.

По умолчанию IFileMonitor предоставляется объектом PhysicalFileProvider, который использует FileSystemWatcher для отслеживания изменений в файле конфигурации.

Этот пример приложения демонстрирует две реализации для отслеживания изменений конфигурации. Если любой из appsettings файлов изменяется, обе реализации мониторинга файлов выполняют пользовательский код— пример приложения записывает сообщение в консоль.

FileSystemWatcher файла конфигурации может активировать несколько обратных вызовов токена для одного изменения файла конфигурации. Чтобы пользовательский код выполнялся один раз при активации нескольких обратных вызовов токена, реализация в примере проверяет хэши файлов. В примере используется хэширование файлов SHA1. Повторные попытки выполняются с экспоненциальным увеличением интервала между ними.

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];
}

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

Зарегистрируйте обратный вызов Action получателя токена для получения уведомлений об изменениях от токена перезагрузки конфигурации.

В Startup.Configure:

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

config.GetReloadToken() предоставляет токен. В качестве обратного вызова используется метод InvokeChanged:

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

Эта система выполняется, пока запущено приложение, и не может быть отключена пользователем.

Отслеживание изменений конфигурации в качестве службы

Этот пример реализует следующее:

  • Базовый мониторинг токена запуска.
  • Отслеживание как служба.
  • Механизм для включения и отключения отслеживания.

Этот пример определяет IConfigurationMonitor интерфейс.

Extensions/ConfigurationMonitor.cs:

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

Конструктор реализованного класса ConfigurationMonitor регистрирует обратный вызов для уведомлений об изменениях:

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() предоставляет токен. InvokeChanged является методом обратного вызова. В данном случае state — это ссылка на экземпляр IConfigurationMonitor, который используется для доступа к состоянию мониторинга. Используются два свойства:

  • MonitoringEnabled: указывает, должна ли функция обратного вызова выполнять свой пользовательский код.
  • CurrentState: описывает текущее состояние мониторинга для использования в пользовательском интерфейсе.

Метод InvokeChanged похож на описанный ранее подход, за исключением того, что он:

  • Не выполняет свой код, если только MonitoringEnabled не равно true.
  • выводит текущее значение state в своих выходных данных WriteConsole.
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:

services.AddSingleton<IConfigurationMonitor, ConfigurationMonitor>();

Страница индекса позволяет пользователю управлять отслеживанием конфигурации. Экземпляр IConfigurationMonitor внедряется в IndexModel.

Pages/Index.cshtml.cs:

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

Монитор конфигурации (_monitor) позволяет включить или отключить мониторинг и задать текущее состояние для обратной связи пользовательского интерфейса:

public IActionResult OnPostStartMonitoring()
{
    _monitor.MonitoringEnabled = true;
    _monitor.CurrentState = "Monitoring!";

    return RedirectToPage();
}

public IActionResult OnPostStopMonitoring()
{
    _monitor.MonitoringEnabled = false;
    _monitor.CurrentState = "Not monitoring";

    return RedirectToPage();
}

При активации OnPostStartMonitoring отслеживание включается, а текущее состояние сбрасывается. При активации OnPostStopMonitoring отслеживание отключается, а состояние указывает на отсутствие отслеживания.

Кнопки пользовательского интерфейса для включения и отключения отслеживания.

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>

Отслеживание изменений кэшированных файлов

Содержимое файла можно кэшировать в памяти с помощью IMemoryCache. Кэширование в памяти описано в разделе Кэш в памяти. Устаревшие (просроченные) данные возвращаются из кэша при изменении источника исходных данных без каких-либо дополнительных действий, таких как описанная ниже реализация.

Например, если не учитывать состояние кэшированного исходного файла при продлении скользящего срока действия, это приведет к появлению в кэше устаревших данных файлов. Каждый запрос к данным продляет скользящий срок действия, но этот файл никогда не загружается в кэш. Любые функции приложения, использующие кэшированное содержимое, могут получить устаревшее содержимое.

Использование токенов изменений в сценарии кэширования файлов предотвращает появление устаревшего содержимого файлов в кэше. Этот пример демонстрирует реализацию данного подхода.

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

  • Вернуть содержимое файла.
  • Реализуйте алгоритм повторных попыток с экспоненциальной задержкой для обработки случаев, когда проблема доступа к файлу временно препятствует чтению его содержимого.

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. Направленный в службу вызов метода GetFileContent пытается получить содержимое файла из кэша в памяти и вернуть его вызывающему объекту (Services/FileService.cs).

Если кэшированное содержимое не удается найти по ключу кэша, выполняются следующие действия:

  1. Содержимое файла получается с помощью GetFileContent.
  2. Токен изменений получается от поставщика файлов с помощью IFileProviders.Watch. При изменении файла активируется обратный вызов токена.
  3. Содержимое файла кэшируется с использованием скользящего срока действия. Токен изменений подключается к MemoryCacheEntryExtensions.AddExpirationToken для исключения записи кэша, если файл изменяется во время его кэширования.

В следующем примере файлы хранятся в корневом каталоге содержимого приложения. IHostingEnvironment.ContentRootFileProvider используется для получения объекта IFileProvider, который указывает на ContentRootPath приложения. Для получения filePath используется 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 регистрируется в контейнере служб вместе со службой кэширования в памяти.

В Startup.ConfigureServices:

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

Страничная модель загружает содержимое файла с помощью службы.

В методе OnGet страницы Index (Pages/Index.cshtml.cs):

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

Класс CompositeChangeToken

Чтобы представить один или несколько экземпляров IChangeToken в одном объекте, используйте класс CompositeChangeToken.

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. ActiveChangeCallbacks у составного токена указывает true, если любой из представленных токенов ActiveChangeCallbacks имеет значение true. Если возникает несколько одновременных событий изменения, обратный вызов составного изменения выполняется один раз.

Дополнительные ресурсы