使用 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 中监视对象更改:

监视配置更改

默认情况下,ASP.NET Core 模板使用 JSON 配置文件appsettings.jsonappsettings.Development.jsonappsettings.Production.json)来加载应用配置设置。

使用接受 reloadOnChange 参数的 ConfigurationBuilder 上的 AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) 扩展方法配置这些文件。 reloadOnChange 指示文件更改时是否应该重载配置。 此设置出现在 Host 便捷方法 CreateDefaultBuilder 中:

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

基于文件的配置由 FileConfigurationSource 表示。 FileConfigurationSource 使用 IFileProvider 来监视文件。

默认情况下,由使用 FileSystemWatcher 来监视配置文件更改的 PhysicalFileProvider 提供 IFileMonitor

示例应用演示监视配置更改的两个实现。 如果任一 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)。 只更改了一次配置文件时,文件哈希用于防止 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:描述在 UI 中使用的当前监视状态。

InvokeChanged 方法类似于前面的方法,不同之处在于:

  • 不运行其代码,除非 MonitoringEnabledtrue
  • 输出其 WriteConsole 输出中的当前 state
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) 用于启用或禁用监视,并设置 UI 反馈的当前状态:

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 时,会禁用监视并设置状态以反映未进行监视。

UI 启用和禁用监视的按钮。

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 用于获取指向应用的 IWebHostEnvironment.ContentRootPathIFileProviderfilePath 通过 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
        });

如果任何表示的令牌 HasChangedtrue,则复合令牌上的 HasChanged 报告 true。 如果任何表示的令牌 ActiveChangeCallbackstrue,则复合令牌上的 ActiveChangeCallbacks 报告 true。 如果发生多个并发更改事件,则调用一次复合更改回调。

更改令牌是用于跟踪状态更改的通用、低级别构建基块。

查看或下载示例代码如何下载

IChangeToken 接口

IChangeToken 传播已发生更改的通知。 IChangeToken 驻留在 Microsoft.Extensions.Primitives 命名空间中。 对于不使用 Microsoft.AspNetCore.App 元包的应用,将为 Microsoft.Extensions.Primitives NuGet 包创建一个包引用。

IChangeToken 具有以下两个属性:

  • ActiveChangeCallbacks,指示令牌是否主动引发回调。 如果将 ActiveChangedCallbacks 设置为 false,则不会调用回调,并且应用必须轮询 HasChanged 获取更改。 如果未发生任何更改,或者丢弃或禁用基础更改侦听器,还可能永远不会取消令牌。
  • HasChanged,接收一个指示是否发生更改的值。

IChangeToken 接口具有 RegisterChangeCallback(Action<Object>, Object) 方法,用于注册在令牌更改时调用的回调。 调用回调之前,必须设置 HasChanged

ChangeToken 类

ChangeToken 是静态类,用于传播已发生更改的通知。 ChangeToken 驻留在 Microsoft.Extensions.Primitives 命名空间中。 对于不使用 Microsoft.AspNetCore.App 元包的应用,将为 Microsoft.Extensions.Primitives NuGet 包创建一个包引用。

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 中监视对象更改:

监视配置更改

默认情况下,ASP.NET Core 模板使用 JSON 配置文件appsettings.jsonappsettings.Development.jsonappsettings.Production.json)来加载应用配置设置。

使用接受 reloadOnChange 参数的 ConfigurationBuilder 上的 AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) 扩展方法配置这些文件。 reloadOnChange 指示文件更改时是否应该重载配置。 此设置出现在 WebHost 便捷方法 CreateDefaultBuilder 中:

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

基于文件的配置由 FileConfigurationSource 表示。 FileConfigurationSource 使用 IFileProvider 来监视文件。

默认情况下,由使用 FileSystemWatcher 来监视配置文件更改的 PhysicalFileProvider 提供 IFileMonitor

示例应用演示监视配置更改的两个实现。 如果任一 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)。 只更改了一次配置文件时,文件哈希用于防止 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:描述在 UI 中使用的当前监视状态。

InvokeChanged 方法类似于前面的方法,不同之处在于:

  • 不运行其代码,除非 MonitoringEnabledtrue
  • 输出其 WriteConsole 输出中的当前 state
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) 用于启用或禁用监视,并设置 UI 反馈的当前状态:

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 时,会禁用监视并设置状态以反映未进行监视。

UI 启用和禁用监视的按钮。

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 用于获取指向应用的 ContentRootPathIFileProviderfilePath 通过 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
        });

如果任何表示的令牌 HasChangedtrue,则复合令牌上的 HasChanged 报告 true。 如果任何表示的令牌 ActiveChangeCallbackstrue,则复合令牌上的 ActiveChangeCallbacks 报告 true。 如果发生多个并发更改事件,则调用一次复合更改回调。

其他资源