ASP.NET Core에서 변경 토큰을 사용하여 변경 내용 검색Detect changes with change tokens in ASP.NET Core

Luke Latham으로By Luke Latham

변경 토큰은 변경 내용을 추적하는 데 사용되는 범용의 하위 수준 구성 요소입니다.A change token is a general-purpose, low-level building block used to track changes.

예제 코드 살펴보기 및 다운로드(다운로드 방법)View or download sample code (how to download)

IChangeToken 인터페이스IChangeToken interface

IChangeToken은 변경이 발생했다는 알림을 전파합니다.IChangeToken propagates notifications that a change has occurred. IChangeTokenMicrosoft.Extensions.Primitives 네임스페이스에 있습니다.IChangeToken resides in the Microsoft.Extensions.Primitives namespace. Microsoft.AspNetCore.All 메타패키지를 사용하지 않는 앱인 경우, 프로젝트 파일에서 Microsoft.Extensions.Primitives NuGet 패키지를 참조합니다.For apps that don't use the Microsoft.AspNetCore.All metapackage, reference the Microsoft.Extensions.Primitives NuGet package in the project file.

IChangeToken에는 두 가지 속성이 있습니다.IChangeToken has two properties:

  • ActiveChangedCallbacks는 토큰이 사전에 콜백을 발생시키는지 여부를 나타냅니다.ActiveChangedCallbacks indicate if the token proactively raises callbacks. ActiveChangedCallbacksfalse로 설정된 경우 콜백은 호출되지 않고 앱은 변경에 대해 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 gets a value that indicates if a change has occurred.

이 인터페이스에는 RegisterChangeCallback(Action<Object>, Object)라는 한 개의 메서드가 포함되며 이는 토큰이 변경될 때 호출되는 콜백을 등록합니다.The interface has one method, RegisterChangeCallback(Action<Object>, Object), which registers a callback that's invoked when the token has changed. HasChanged는 콜백이 호출되기 전에 설정되어야 합니다.HasChanged must be set before the callback is invoked.

ChangeToken 클래스ChangeToken class

ChangeToken은 변경이 발생했다는 알림을 전파하는 데 사용되는 정적 클래스입니다.ChangeToken is a static class used to propagate notifications that a change has occurred. ChangeTokenMicrosoft.Extensions.Primitives 네임스페이스에 있습니다.ChangeToken resides in the Microsoft.Extensions.Primitives namespace. Microsoft.AspNetCore.All 메타패키지를 사용하지 않는 앱인 경우, 프로젝트 파일에서 Microsoft.Extensions.Primitives NuGet 패키지를 참조합니다.For apps that don't use the Microsoft.AspNetCore.All metapackage, reference the Microsoft.Extensions.Primitives NuGet package in the project file.

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에는 토큰 소비자 Action에게 전달된 추가 TState 매개 변수를 사용하는 OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) 오버로드가 있습니다.ChangeToken has an OnChange<TState>(Func<IChangeToken>, Action<TState>, TState) overload that takes an additional TState parameter that's passed into the token consumer Action.

OnChangeIDisposable을 반환합니다.OnChange returns an IDisposable. Dispose를 호출하면 토큰이 더 이상 변경 내용을 수신 대기하지 않고 토큰의 리소스가 해제됩니다.Calling Dispose stops the token from listening for further changes and releases the token's resources.

ASP.NET Core에서 변경 토큰의 사용 예Example uses of change tokens in ASP.NET Core

변경 토큰은 ASP.NET Core에서 개체의 변경 내용을 모니터링하는 주요 영역에 사용됩니다.Change tokens are used in prominent areas of ASP.NET Core monitoring changes to objects:

  • 파일에 대한 변경을 모니터링하기 위해 IFileProviderWatch 메서드는 지정된 파일 또는 조사할 폴더에 대해 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 변경의 경우, IOptionsMonitor의 기본 OptionsMonitor 구현은 IOptionsChangeTokenSource 인스턴스를 하나 이상 받아들이는 오버로드를 포함합니다.For TOptions changes, the default OptionsMonitor implementation of IOptionsMonitor has an overload that accepts one or more IOptionsChangeTokenSource instances. 각 인스턴스는 옵션 변경을 추적하기 위해 변경 알림 콜백을 등록하도록 IChangeToken을 반환합니다.Each instance returns an IChangeToken to register a change notification callback for tracking options changes.

구성 변경 모니터링Monitoring for configuration changes

기본적으로 ASP.NET Core 템플릿은 JSON 구성 파일(appsettings.json, appsettings.Development.json, and 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.

이러한 파일은 reloadOnChange 매개 변수를 받아들이는 ConfigurationBuilder에서 AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) 확장 메서드를 사용하여 구성합니다(ASP.NET Core 1.1 이상).These files are configured using the AddJsonFile(IConfigurationBuilder, String, Boolean, Boolean) extension method on ConfigurationBuilder that accepts a reloadOnChange parameter (ASP.NET Core 1.1 and later). reloadOnChange는 파일 변경 시 구성을 다시 로드해야 하는지를 나타냅니다.reloadOnChange indicates if configuration should be reloaded on file changes. WebHost 편의 메서드 CreateDefaultBuilder에서 이 설정을 참조하세요.See this setting 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. FileConfigurationSourceIFileProvider을 사용하여 파일을 모니터링합니다.FileConfigurationSource uses IFileProvider to monitor files.

기본적으로 IFileMonitorPhysicalFileProvider에서 제공하며 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.json 파일이 변경되거나 파일의 환경 버전이 변경되는 경우 각 구현에서 사용자 지정 코드를 실행합니다.If either the appsettings.json file changes or the Environment version of the file changes, each implementation executes 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. 샘플의 구현은 구성 파일에서 파일 해시를 확인하여 이 문제를 방지합니다.The sample's implementation guards against this problem by checking file hashes on the configuration files. 파일 해시를 확인하면 사용자 지정 코드를 실행하기 전에 적어도 하나의 구성 파일이 변경되었는지 확인할 수 있습니다.Checking file hashes ensures that at least one of the configuration files has changed before running the custom code. 샘플에서는 SHA1 파일 해시(Utilities/Utilities.cs)를 사용합니다.The sample uses SHA1 file hashing (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];
}

재시도는 지수 백오프로 구현됩니다.A retry is implemented with an exponential back-off. 파일 중 하나에서 새 해시를 일시적으로 계산하지 못하게 하는 파일 잠금이 발생할 수 있으므로 재시도가 제공됩니다.The re-try is present because file locking may occur that temporarily prevents computing a new hash on one of the files.

단순 시작 변경 토큰Simple startup change token

변경 알림을 위한 토큰 소비자 Action 콜백을 구성 다시 로드 토큰(Startup.cs)에 등록합니다.Register a token consumer Action callback for change notifications to the configuration reload token (Startup.cs):

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

콜백의 stateIHostingEnvironment에서 전달하는 데 사용됩니다.The state of the callback is used to pass in the IHostingEnvironment. 이것은 모니터링할 올바른 appsettings 구성 JSON 파일(appsettings.<Environment>.json)을 결정하는 데 유용합니다.This is useful to determine the correct appsettings configuration JSON file to monitor, appsettings.<Environment>.json. 파일 해시는 구성 파일이 한 번만 변경된 경우 여러 토큰 콜백으로 인해 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.

서비스로 구성 변경 모니터링Monitoring configuration changes as a service

이 샘플에서는 다음을 구현합니다.The sample implements:

  • 기본 시작 토큰 모니터링Basic startup token monitoring.
  • 서비스로 모니터링Monitoring as a service.
  • 모니터링을 사용 및 사용 안 함으로 설정하는 메커니즘A mechanism to enable and disable monitoring.

이 샘플은 IConfigurationMonitor 인터페이스(Extensions/ConfigurationMonitor.cs)를 설정합니다.The sample establishes an IConfigurationMonitor interface (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 is used to access the monitoring state. 두 개의 속성이 사용됩니다.Two properties are used:

  • MonitoringEnabled는 콜백이 사용자 지정 코드를 실행해야 하는지를 나타냅니다.MonitoringEnabled indicates if the callback should run its custom code.
  • CurrentState는 UI에서 사용하기 위해 현재 모니터링 상태를 설명합니다.CurrentState describes the current monitoring state for use in the UI.

InvokeChanged 메서드는 다음을 제외하고 이전 방법과 유사합니다.The InvokeChanged method is similar to the earlier approach, except that it:

  • MonitoringEnabledtrue가 아닌 경우 해당 코드를 실행하지 않습니다.Doesn't run its code unless MonitoringEnabled is true.
  • WriteConsole 출력의 현재 state를 기록합니다.Notes 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.csConfigureServices에서 서비스로 등록됩니다.An instance ConfigurationMonitor is registered as a service in ConfigureServices of Startup.cs:

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:

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

단추는 모니터링을 사용 및 사용하지 않도록 설정합니다.A button enables and disables monitoring:

<button class="btn btn-danger" asp-page-handler="StopMonitoring">Stop Monitoring</button>
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.

캐시된 파일 변경 모니터링Monitoring 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.

상대(sliding) 만료 기간을 갱신할 때 캐시된 소스 파일의 상태를 고려하지 않으면 부실 캐시 데이터가 발생합니다.Not taking into account the status of a cached source file when renewing a sliding expiration period leads to stale cache data. 데이터에 대한 각 요청은 상대(sliding) 만료 기간을 갱신하지만 파일은 캐시에 다시 로드되지 않습니다.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 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 is temporarily preventing a file from being read.

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. 상대(sliding) 만료 기간과 함께 파일 콘텐츠가 캐시됩니다.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.
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)
    {
        // For the purposes of this example, files are stored 
        // in the content root of the app. To obtain the physical
        // path to a file at the content root, use the
        // ContentRootFileProvider on IHostingEnvironment.
        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.cs)와 함께 서비스 컨테이너에 등록됩니다.The FileService is registered in the service container along with the memory caching service (Startup.cs):

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

페이지 모델은 서비스(Pages/Index.cshtml.cs)를 사용하여 파일의 콘텐츠를 로드합니다.The page model loads the file's content using the service (Pages/Index.cshtml.cs):

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

CompositeChangeToken 클래스CompositeChangeToken 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
        });

표시된 모든 토큰의 HasChangedtrue인 경우 복합 토큰의 HasChangedtrue를 보고합니다.HasChanged on the composite token reports true if any represented token HasChanged is true. 표시된 모든 토큰의 ActiveChangeCallbackstrue인 경우 복합 토큰의 ActiveChangeCallbackstrue를 보고합니다.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 exactly one time.

참고 항목See also