ASP.NET Core에서 변경 토큰을 사용하여 변경 내용 검색

변경 토큰은 상태 변경 내용을 추적하는 데 사용되는 범용의 하위 수준 구성 요소입니다.

샘플 코드 보기 및 다운로드(다운로드 방법)

IChangeToken 인터페이스

IChangeToken은 변경이 발생했다는 알림을 전파합니다. IChangeTokenMicrosoft.Extensions.Primitives 네임스페이스에 상주합니다. Microsoft.Extensions.Primitives NuGet 패키지는 ASP.NET Core 앱에 암시적으로 제공됩니다.

IChangeToken에는 두 가지 속성이 있습니다.

  • ActiveChangeCallbacks는 토큰이 사전에 콜백을 발생시키는지 여부를 나타냅니다. ActiveChangedCallbacksfalse로 설정된 경우 콜백은 호출되지 않고 앱은 변경에 대해 HasChanged를 폴링해야 합니다. 또한 변경이 발생하지 않거나 기본 변경 리스너가 삭제되거나 사용하지 않도록 설정된 경우에도 토큰을 절대로 취소할 수 없습니다.
  • HasChanged는 변경이 발생했는지 나타내는 값을 수신합니다.

IChangeToken 인터페이스는 RegisterChangeCallback(Action<Object>, Object) 메서드를 포함하며 이는 토큰이 변경될 때 호출되는 콜백을 등록합니다. HasChanged는 콜백이 호출되기 전에 설정되어야 합니다.

ChangeToken 클래스

ChangeToken은 변경이 발생했다는 알림을 전파하는 데 사용되는 정적 클래스입니다. ChangeTokenMicrosoft.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) 오버로드는 토큰 소비자 Action에게 전달된 추가 TState 매개 변수를 사용합니다.

OnChangeIDisposable을 반환합니다. Dispose를 호출하면 토큰이 더 이상 변경 내용을 수신 대기하지 않고 토큰의 리소스가 해제됩니다.

ASP.NET Core에서 변경 토큰의 사용 예

변경 토큰은 ASP.NET Core에서 개체의 변경 내용을 모니터링하는 주요 영역에 사용됩니다.

  • 파일에 대한 변경을 모니터링하기 위해 IFileProviderWatch 메서드는 지정된 파일 또는 조사할 폴더에 대해 IChangeToken을 만듭니다.
  • IChangeToken 토큰을 캐시 항목에 추가하여 변경 시 캐시 제거를 트리거할 수 있습니다.
  • TOptions 변경의 경우, IOptionsMonitor<TOptions>의 기본 OptionsMonitor<TOptions> 구현은 둘 이상의 IOptionsChangeTokenSource<TOptions> 인스턴스를 받아들이는 오버로드를 포함합니다. 각 인스턴스는 옵션 변경을 추적하기 위해 변경 알림 콜백을 등록하도록 IChangeToken을 반환합니다.

구성 변경 모니터링

기본적으로 ASP.NET Core 템플릿은 JSON 구성 파(appsettings.json,appsettings.Development.json,appsettings.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로 표현됩니다. FileConfigurationSourceIFileProvider를 사용하여 파일을 모니터링합니다.

기본적으로 IFileMonitorFileSystemWatcher를 사용하여 구성 파일 변경을 모니터링하는 PhysicalFileProvider에서 제공합니다.

샘플 앱을 통해 구성 변경 모니터링을 위한 두 가지 구현을 설명합니다. 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는 모니터링할 정확한 appsettings의 구성 파일을 지정하는 데 유용한 IWebHostEnvironment를 전달하는 데 사용됩니다(예를 들어 개발 환경에서는 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}");
        }
    }
}

인스턴스 ConfigurationMonitorStartup.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를 사용하여 메모리 내에 캐시할 수 있습니다. 메모리 내 캐싱은 메모리 내 캐시 토픽에서 설명합니다. 아래에 설명된 구현과 같은 추가 단계를 수행하지 않으면 소스 데이터가 변경될 경우 캐시에서 부실(오래된) 데이터가 반환됩니다.

예를 들어 상대(sliding) 만료 기간을 갱신할 때 캐시된 소스 파일의 상태를 고려하지 않으면 부실 캐시 데이터가 발생합니다. 데이터에 대한 각 요청은 상대(sliding) 만료 기간을 갱신하지만 파일은 캐시에 다시 로드되지 않습니다. 파일의 캐시된 콘텐츠를 사용하는 모든 앱 기능은 부실 콘텐츠를 받을 수 있습니다.

파일 캐싱 시나리오에서 변경 토큰을 사용하면 캐시에 부실 파일 콘텐츠가 생기는 것을 방지할 수 있습니다. 샘플 앱에서 이러한 방법의 구현을 보여 줍니다.

이 샘플에서는 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. 상대(sliding) 만료 기간과 함께 파일 콘텐츠가 캐시됩니다. 변경 토큰은 MemoryCacheEntryExtensions.AddExpirationToken과 연결되어 파일이 캐시되는 동안 변경되면 캐시 항목을 제거합니다.

다음 예제에서 파일은 앱의 콘텐츠 루트에 저장됩니다. IWebHostEnvironment.ContentRootFileProvider는 앱의 IWebHostEnvironment.ContentRootPath를 가리키는 IFileProvider를 가져오는 데 사용됩니다. filePathIFileInfo.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인 경우 복합 토큰의 HasChangedtrue를 보고합니다. 표시된 모든 토큰의 ActiveChangeCallbackstrue인 경우 복합 토큰의 ActiveChangeCallbackstrue를 보고합니다. 여러 동시 변경 이벤트가 발생하면 복합 변경 콜백이 한 번 호출됩니다.

변경 토큰은 상태 변경 내용을 추적하는 데 사용되는 범용의 하위 수준 구성 요소입니다.

샘플 코드 보기 및 다운로드(다운로드 방법)

IChangeToken 인터페이스

IChangeToken은 변경이 발생했다는 알림을 전파합니다. IChangeTokenMicrosoft.Extensions.Primitives 네임스페이스에 상주합니다. Microsoft.AspNetCore.App 메타패키지를 사용하지 않는 앱의 경우 Microsoft.Extensions.Primitives NuGet 패키지에 대한 패키지 참조를 생성합니다.

IChangeToken에는 두 가지 속성이 있습니다.

  • ActiveChangeCallbacks는 토큰이 사전에 콜백을 발생시키는지 여부를 나타냅니다. ActiveChangedCallbacksfalse로 설정된 경우 콜백은 호출되지 않고 앱은 변경에 대해 HasChanged를 폴링해야 합니다. 또한 변경이 발생하지 않거나 기본 변경 리스너가 삭제되거나 사용하지 않도록 설정된 경우에도 토큰을 절대로 취소할 수 없습니다.
  • HasChanged는 변경이 발생했는지 나타내는 값을 수신합니다.

IChangeToken 인터페이스는 RegisterChangeCallback(Action<Object>, Object) 메서드를 포함하며 이는 토큰이 변경될 때 호출되는 콜백을 등록합니다. HasChanged는 콜백이 호출되기 전에 설정되어야 합니다.

ChangeToken 클래스

ChangeToken은 변경이 발생했다는 알림을 전파하는 데 사용되는 정적 클래스입니다. ChangeTokenMicrosoft.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) 오버로드는 토큰 소비자 Action에게 전달된 추가 TState 매개 변수를 사용합니다.

OnChangeIDisposable을 반환합니다. Dispose를 호출하면 토큰이 더 이상 변경 내용을 수신 대기하지 않고 토큰의 리소스가 해제됩니다.

ASP.NET Core에서 변경 토큰의 사용 예

변경 토큰은 ASP.NET Core에서 개체의 변경 내용을 모니터링하는 주요 영역에 사용됩니다.

  • 파일에 대한 변경을 모니터링하기 위해 IFileProviderWatch 메서드는 지정된 파일 또는 조사할 폴더에 대해 IChangeToken을 만듭니다.
  • IChangeToken 토큰을 캐시 항목에 추가하여 변경 시 캐시 제거를 트리거할 수 있습니다.
  • TOptions 변경의 경우, IOptionsMonitor<TOptions>의 기본 OptionsMonitor<TOptions> 구현은 둘 이상의 IOptionsChangeTokenSource<TOptions> 인스턴스를 받아들이는 오버로드를 포함합니다. 각 인스턴스는 옵션 변경을 추적하기 위해 변경 알림 콜백을 등록하도록 IChangeToken을 반환합니다.

구성 변경 모니터링

기본적으로 ASP.NET Core 템플릿은 JSON 구성 파(appsettings.json,appsettings.Development.json,appsettings.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로 표현됩니다. FileConfigurationSourceIFileProvider를 사용하여 파일을 모니터링합니다.

기본적으로 IFileMonitorFileSystemWatcher를 사용하여 구성 파일 변경을 모니터링하는 PhysicalFileProvider에서 제공합니다.

샘플 앱을 통해 구성 변경 모니터링을 위한 두 가지 구현을 설명합니다. 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는 모니터링할 정확한 appsettings의 구성 파일을 지정하는 데 유용한 IHostingEnvironment를 전달하는 데 사용됩니다(예를 들어 개발 환경에서는 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}");
        }
    }
}

인스턴스 ConfigurationMonitorStartup.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를 사용하여 메모리 내에 캐시할 수 있습니다. 메모리 내 캐싱은 메모리 내 캐시 토픽에서 설명합니다. 아래에 설명된 구현과 같은 추가 단계를 수행하지 않으면 소스 데이터가 변경될 경우 캐시에서 부실(오래된) 데이터가 반환됩니다.

예를 들어 상대(sliding) 만료 기간을 갱신할 때 캐시된 소스 파일의 상태를 고려하지 않으면 부실 캐시 데이터가 발생합니다. 데이터에 대한 각 요청은 상대(sliding) 만료 기간을 갱신하지만 파일은 캐시에 다시 로드되지 않습니다. 파일의 캐시된 콘텐츠를 사용하는 모든 앱 기능은 부실 콘텐츠를 받을 수 있습니다.

파일 캐싱 시나리오에서 변경 토큰을 사용하면 캐시에 부실 파일 콘텐츠가 생기는 것을 방지할 수 있습니다. 샘플 앱에서 이러한 방법의 구현을 보여 줍니다.

이 샘플에서는 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. 상대(sliding) 만료 기간과 함께 파일 콘텐츠가 캐시됩니다. 변경 토큰은 MemoryCacheEntryExtensions.AddExpirationToken과 연결되어 파일이 캐시되는 동안 변경되면 캐시 항목을 제거합니다.

다음 예제에서 파일은 앱의 콘텐츠 루트에 저장됩니다. IHostingEnvironment.ContentRootFileProvider는 앱의 ContentRootPath를 가리키는 IFileProvider를 가져오는 데 사용합니다. filePathIFileInfo.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인 경우 복합 토큰의 HasChangedtrue를 보고합니다. 표시된 모든 토큰의 ActiveChangeCallbackstrue인 경우 복합 토큰의 ActiveChangeCallbackstrue를 보고합니다. 여러 동시 변경 이벤트가 발생하면 복합 변경 콜백이 한 번 호출됩니다.

추가 리소스