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

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

Просмотреть или скачать образец кода (как скачивать)

Интерфейс IChangeToken

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

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

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

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

Класс ChangeToken

ChangeToken — это статический класс, который распространяет уведомления о том, что произошло изменение. ChangeToken находится в пространстве имен Microsoft.Extensions.Primitives. Пакет Microsoft.Extensions.Primitives NuGet явно предоставляется приложениям 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 указывает, нужно ли перезагружать конфигурацию при изменении файла. Этот параметр отображается в удобном методе CreateDefaultBuilder класса Host:

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 || ex.HResult != -2147024864)
            {
                throw;
            }
            else
            {
                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 при работе в среде разработки). Хэши файлов используются для предотвращения многократного выполнения оператора 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 || 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 для исключения записи кэша, если файл изменяется во время его кэширования.

В следующем примере файлы хранятся в корневом каталоге содержимого приложения. 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 (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 указывает, нужно ли перезагружать конфигурацию при изменении файла. Этот параметр отображается в удобном методе CreateDefaultBuilder класса WebHost:

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 || ex.HResult != -2147024864)
            {
                throw;
            }
            else
            {
                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 при работе в среде разработки). Хэши файлов используются для предотвращения многократного выполнения оператора 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 (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. Если возникает несколько одновременных событий изменения, обратный вызов составного изменения выполняется один раз.

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