ASP.NET Core의 옵션 패턴

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

앤더슨.

옵션 패턴은 클래스를 사용하여 관련 설정 그룹에 대한 강력한 형식의 액세스를 제공합니다. 구성 설정이 시나리오에 따라 별도 클래스로 격리된 경우 앱은 두 가지 중요한 소프트웨어 엔지니어링 원칙을 따릅니다.

  • 캡슐화:
    • 구성 설정에 종속된 클래스는 사용하는 구성 설정에 의해서만 결정됩니다.
  • 우려 사항 분리:
    • 앱의 다른 부분에 대한 설정은 다른 설정에 종속되거나 연결되지 않습니다.

옵션은 구성 데이터의 유효성을 검사하는 메커니즘도 제공합니다. 자세한 내용은 옵션 유효성 검사 섹션을 참조하세요.

이 문서에서는 ASP.NET Core의 옵션 패턴 정보를 제공합니다. 콘솔 앱에서 옵션 패턴을 사용하는 방법에 관한 자세한 내용은 .NET의 옵션 패턴을 참조하세요.

계층적 구성 바인딩

관련 구성 값을 읽는 기본 방법은 옵션 패턴를 사용하는 것입니다. 예를 들어 다음 구성 값을 읽으려면:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

다음 PositionOptions 클래스를 만듭니다.

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

옵션 클래스:

  • 비 추상적이어야 합니다.
  • 구성에 해당 항목이 바인딩된 형식의 공용 읽기/쓰기 속성이 있습니다.
  • 구성에서 일치하는 항목에 바인딩된 읽기-쓰기 속성이 있습니다.
  • 해당 필드가 바인딩되어 있지 않습니다. 위 코드에서 Position은 바운딩되지 않습니다. Position 필드를 사용하므로 클래스를 구성 공급자에 바인딩할 때 문자열 "Position"을 앱에서 하드 코딩하지 않아도 됩니다.

코드는 다음과 같습니다.

  • ConfigurationBinder.Bind를 호출하여 PositionOptions 클래스를 Position 섹션에 바인딩합니다.
  • Position 구성 데이터를 표시합니다.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

ConfigurationBinder.Get<T>는 지정된 형식을 바인딩하고 반환합니다. ConfigurationBinder.Get<T>ConfigurationBinder.Bind를 사용하는 것보다 편리할 수 있습니다. 다음 코드에서는 ConfigurationBinder.Get<T>PositionOptions 클래스를 함께 사용하는 방법을 보여 줍니다.

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

또한 Bind를 사용하면 추상 클래스를 혼합할 수 있습니다. 추상 클래스 SomethingWithAName를 사용하는 다음 코드를 고려합니다.

namespace ConfigSample.Options;

public abstract class SomethingWithAName
{
    public abstract string? Name { get; set; }
}

public class NameTitleOptions(int age) : SomethingWithAName
{
    public const string NameTitle = "NameTitle";

    public override string? Name { get; set; }
    public string Title { get; set; } = string.Empty;

    public int Age { get; set; } = age;
}

다음 코드는 구성 값을 표시합니다 NameTitleOptions .

public class Test33Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test33Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var nameTitleOptions = new NameTitleOptions(22);
        Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);

        return Content($"Title: {nameTitleOptions.Title} \n" +
                       $"Name: {nameTitleOptions.Name}  \n" +
                       $"Age: {nameTitleOptions.Age}"
                       );
    }
}

호출은 다음을 호출하는 Bind 것보다 덜 엄격합니다.Get<>

  • Bind 는 추상화의 혼합을 허용합니다.
  • Get<> 인스턴스 자체를 만들어야 합니다.

옵션 패턴

옵션 패턴을 사용하는 경우 한 가지 대체 방법은 Position 섹션을 바인딩하고 종속성 주입 서비스 컨테이너에 추가하는 것입니다. 다음 코드에서 PositionOptionsConfigure를 통해 서비스 컨테이너에 추가되고 구성에 바인딩됩니다.

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

위의 코드를 사용하여 다음 코드는 위치 옵션을 읽습니다.

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

위 코드에서는 앱을 시작한 후의 JSON 구성 파일의 변경 사항은 읽지 않습니다. 앱을 시작한 후의 변경 사항을 읽으려면 IOptionsSnapshot을 사용합니다.

옵션 인터페이스

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

사후 구성 시나리오에서는 모든 IConfigureOptions<TOptions> 구성이 발생한 후에 옵션을 설정하거나 변경할 수 있습니다.

IOptionsFactory<TOptions>는 새로운 옵션 인스턴스를 만듭니다. 단일 Create 메서드가 있습니다. 기본 구현에서는 등록된 모든 IConfigureOptions<TOptions>IPostConfigureOptions<TOptions>를 사용하며 먼저 구성을 모두 실행한 다음, 사후 구성을 수행합니다. IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions>를 구별하며 적절한 인터페이스만 호출합니다.

IOptionsMonitorCache<TOptions>IOptionsMonitor<TOptions>에서 TOptions 인스턴스를 캐시하는 데 사용됩니다. IOptionsMonitorCache<TOptions>는 모니터의 옵션 인스턴스를 무효화하므로 값이 다시 계산됩니다(TryRemove). TryAdd를 사용하여 값을 수동으로 도입할 수 있습니다. 모든 명명된 인스턴스를 필요에 따라 다시 만들어야 하는 경우 Clear 메서드가 사용됩니다.

IOptionsSnapshot을 사용하여 업데이트된 데이터 읽기

IOptionsSnapshot<TOptions>사용:

  • 옵션은 엑세스될 때 요청 당 한 번씩 계산되고 요청의 수명 동안 캐시됩니다.
  • 범위가 지정된 서비스이며 요청마다 다시 계산되므로 상당한 성능 저하가 발생할 수 있습니다. 자세한 내용은 이 GitHub 이슈구성 바인딩의 성능 향상을 참조하세요.
  • 업데이트된 구성 값 읽기를 지원하는 구성 공급자를 사용하는 경우 앱을 시작한 후 구성 변경 내용을 읽습니다.

IOptionsMonitorIOptionsSnapshot의 차이점은 다음과 같습니다.

  • IOptionsMonitor는 언제든지 현재 옵션 값을 검색하는 싱글톤 서비스로, 싱글톤 종속성에서 특히 유용합니다.
  • IOptionsSnapshot범위가 지정된 서비스이며 IOptionsSnapshot<T> 개체가 생성될 때 옵션의 스냅샷을 제공합니다. 옵션 스냅숏은 임시 및 범위가 지정된 종속성과 함께 사용하도록 설계되었습니다.

다음 코드에서는 IOptionsSnapshot<TOptions> 메서드를 사용합니다.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

다음 코드에서는 MyOptions가 바인딩되는 구성 인스턴스를 등록합니다.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

위 코드에서는 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

IOptionsMonitor

다음 코드는 MyOptions가 바인딩되는 구성 인스턴스를 등록합니다.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

다음 예제에서는 IOptionsMonitor<TOptions>를 사용합니다.

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

명명된 옵션은 IConfigureNamedOptions 사용을 지원

명명된 옵션:

  • 여러 구성 섹션이 동일한 속성에 바인딩되는 경우에 유용합니다.
  • 대/소문자를 구분합니다.

다음 appsettings.json 파일을 살펴보세요.

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

TopItem:MonthTopItem:Year를 바인딩하는 두 개의 클래스를 만드는 대신 각 섹션에 다음 클래스가 사용됩니다.

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

다음 코드는 명명된 옵션을 구성합니다.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

다음 코드는 명명된 옵션을 표시합니다.

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

모든 옵션은 명명된 인스턴스입니다. IConfigureOptions<TOptions> 인스턴스는 string.EmptyOptions.DefaultName 인스턴스를 대상으로 지정한 것처럼 처리됩니다. 또한 IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions>를 구현합니다. IOptionsFactory<TOptions>의 기본 구현에는 각 옵션을 적절하게 사용하기 위한 논리가 있습니다. null 명명된 옵션은 특정 명명된 인스턴스 대신 모든 명명된 인스턴스를 대상으로 지정하는 데 사용됩니다. ConfigureAllPostConfigureAll에서 이 규칙을 사용합니다.

OptionsBuilder API

OptionsBuilder<TOptions>TOptions 인스턴스를 구성하는 데 사용됩니다. OptionsBuilderAddOptions<TOptions>(string optionsName) 호출에 대한 단일 매개 변수이므로 모든 후속 호출에 나타나는 대신 명명된 옵션 생성을 간소화합니다. 옵션 유효성 검사 및 서비스 종속성을 허용하는 ConfigureOptions 오버로드는 OptionsBuilder를 통해서만 사용할 수 있습니다.

OptionsBuilder옵션 유효성 검사 섹션에서 사용됩니다.

사용자 지정 리포지토리를 추가하는 방법에 대한 내용은 AddOptions를 사용하여 사용자 지정 리포지토리 구성을 참조하세요.

DI 서비스를 사용하여 옵션 구성

다음 두 가지 방법으로 옵션을 구성하는 동안 종속성 주입에서 서비스에 액세스할 수 있습니다.

  • 구성 대리자를 OptionsBuilder<TOptions>Configure에 전달합니다. OptionsBuilder<TOptions>는 최대 5개의 서비스를 사용하여 옵션을 구성할 수 있는 Configure의 오버로드를 제공합니다.

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • IConfigureOptions<TOptions> 또는 IConfigureNamedOptions<TOptions>를 구현하는 형식을 만들고 해당 형식을 서비스로 등록합니다.

서비스 만들기가 더 복잡하기 Configure때문에 구성 대리자를 전달하는 것이 좋습니다. 형식을 만드는 것은 프레임워크가 호출 Configure할 때 수행하는 작업과 동일합니다. Configure를 호출하면 지정된 일반 서비스 유형을 허용하는 생성자가 있는 일시적인 제네릭 IConfigureNamedOptions<TOptions>를 등록합니다.

옵션 유효성 검사

옵션 유효성 검사를 통해 옵션 값의 유효성을 검사할 수 있습니다.

다음 appsettings.json 파일을 살펴보세요.

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

다음 클래스는 "MyConfig" 구성 섹션에 바인딩되고 몇 가지 DataAnnotations 규칙을 적용합니다.

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

코드는 다음과 같습니다.

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

ValidateDataAnnotations 확장 메서드는 Microsoft.Extensions.Options.DataAnnotations NuGet 패키지에서 정의됩니다. Microsoft.NET.Sdk.Web SDK를 사용하는 웹앱의 경우 이 패키지는 공유 프레임워크에서 암시적으로 참조됩니다.

다음 코드는 구성 값 또는 유효성 검사 오류를 표시합니다.

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

다음 코드는 대리자를 사용하여 보다 복잡한 유효성 검사 규칙을 적용합니다.

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions>IValidatableObject

다음 클래스는 IValidateOptions<TOptions>를 구현합니다.

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions를 사용하면 유효성 검사 코드를 Program.cs에서 클래스로 이동할 수 있습니다.

위의 코드에서는 다음 코드를 사용하여 Program.cs에서 유효성 검사를 사용하도록 설정합니다.

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

옵션 유효성 검사도 IValidatableObject를 지원합니다. 클래스 자체 내에서 클래스의 클래스 수준 유효성 검사를 수행하려면 다음을 수행합니다.

  • 클래스 내에서 IValidatableObject 인터페이스 및 해당 Validate 메서드를 구현합니다.
  • ValidateDataAnnotations에서 Program.cs를 호출합니다.

ValidateOnStart

옵션 유효성 검사는 IOptions<TOptions>, IOptionsSnapshot<TOptions>을 처음 실행하거나 IOptionsMonitor<TOptions> 구현이 생성됩니다. 옵션 유효성 검사를 즉시 실행하려면 앱이 시작될 때 Program.cs에서 ValidateOnStart을 호출:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

옵션 사후 구성

IPostConfigureOptions<TOptions>를 사용하여 사후 구성을 설정합니다. 사후 구성은 모든 IConfigureOptions<TOptions> 구성이 수행된 후에 실행됩니다.

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure를 사용하여 명명된 옵션을 사후 구성할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

모든 구성 인스턴스를 사후 구성하려면 PostConfigureAll을 사용합니다.

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Program.cs의 액세스 옵션

Program.cs에서 IOptions<TOptions> 또는 IOptionsMonitor<TOptions>에 액세스하려면 WebApplication.Services에서 GetRequiredService를 호출합니다.

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

추가 리소스

작성자: Kirk LarkinRick Anderson.

옵션 패턴은 클래스를 사용하여 관련 설정 그룹에 대한 강력한 형식의 액세스를 제공합니다. 구성 설정이 시나리오에 따라 별도 클래스로 격리된 경우 앱은 두 가지 중요한 소프트웨어 엔지니어링 원칙을 따릅니다.

  • 캡슐화:
    • 구성 설정에 종속된 클래스는 사용하는 구성 설정에 의해서만 결정됩니다.
  • 우려 사항 분리:
    • 앱의 다른 부분에 대한 설정은 다른 설정에 종속되거나 연결되지 않습니다.

옵션은 구성 데이터의 유효성을 검사하는 메커니즘도 제공합니다. 자세한 내용은 옵션 유효성 검사 섹션을 참조하세요.

이 문서에서는 ASP.NET Core의 옵션 패턴 정보를 제공합니다. 콘솔 앱에서 옵션 패턴을 사용하는 방법에 관한 자세한 내용은 .NET의 옵션 패턴을 참조하세요.

계층적 구성 바인딩

관련 구성 값을 읽는 기본 방법은 옵션 패턴를 사용하는 것입니다. 예를 들어 다음 구성 값을 읽으려면:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

다음 PositionOptions 클래스를 만듭니다.

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

옵션 클래스:

  • 매개 변수가 없는 public 생성자를 사용하는 비추상이어야 합니다.
  • 형식의 모든 공용 읽기-쓰기 속성이 바인딩됩니다.
  • 필드가 바인딩되지 않습니다. 위 코드에서 Position은 바운딩되지 않습니다. Position 필드를 사용하므로 클래스를 구성 공급자에 바인딩할 때 문자열 "Position"을 앱에서 하드 코딩하지 않아도 됩니다.

코드는 다음과 같습니다.

  • ConfigurationBinder.Bind를 호출하여 PositionOptions 클래스를 Position 섹션에 바인딩합니다.
  • Position 구성 데이터를 표시합니다.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

ConfigurationBinder.Get<T>는 지정된 형식을 바인딩하고 반환합니다. ConfigurationBinder.Get<T>ConfigurationBinder.Bind를 사용하는 것보다 편리할 수 있습니다. 다음 코드에서는 ConfigurationBinder.Get<T>PositionOptions 클래스를 함께 사용하는 방법을 보여 줍니다.

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

옵션 패턴을 사용하는 경우 한 가지 대체 방법은 Position 섹션을 바인딩하고 종속성 주입 서비스 컨테이너에 추가하는 것입니다. 다음 코드에서 PositionOptionsConfigure를 통해 서비스 컨테이너에 추가되고 구성에 바인딩됩니다.

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

위의 코드를 사용하여 다음 코드는 위치 옵션을 읽습니다.

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

위 코드에서는 앱을 시작한 후의 JSON 구성 파일의 변경 사항은 읽지 않습니다. 앱을 시작한 후의 변경 사항을 읽으려면 IOptionsSnapshot을 사용합니다.

옵션 인터페이스

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

사후 구성 시나리오에서는 모든 IConfigureOptions<TOptions> 구성이 발생한 후에 옵션을 설정하거나 변경할 수 있습니다.

IOptionsFactory<TOptions>는 새로운 옵션 인스턴스를 만듭니다. 단일 Create 메서드가 있습니다. 기본 구현에서는 등록된 모든 IConfigureOptions<TOptions>IPostConfigureOptions<TOptions>를 사용하며 먼저 구성을 모두 실행한 다음, 사후 구성을 수행합니다. IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions>를 구별하며 적절한 인터페이스만 호출합니다.

IOptionsMonitorCache<TOptions>IOptionsMonitor<TOptions>에서 TOptions 인스턴스를 캐시하는 데 사용됩니다. IOptionsMonitorCache<TOptions>는 모니터의 옵션 인스턴스를 무효화하므로 값이 다시 계산됩니다(TryRemove). TryAdd를 사용하여 값을 수동으로 도입할 수 있습니다. 모든 명명된 인스턴스를 필요에 따라 다시 만들어야 하는 경우 Clear 메서드가 사용됩니다.

IOptionsSnapshot을 사용하여 업데이트된 데이터 읽기

IOptionsSnapshot<TOptions>사용:

  • 옵션은 엑세스될 때 요청 당 한 번씩 계산되고 요청의 수명 동안 캐시됩니다.
  • 범위가 지정된 서비스이며 요청마다 다시 계산되므로 상당한 성능 저하가 발생할 수 있습니다. 자세한 내용은 이 GitHub 이슈구성 바인딩의 성능 향상을 참조하세요.
  • 업데이트된 구성 값 읽기를 지원하는 구성 공급자를 사용하는 경우 앱을 시작한 후 구성 변경 내용을 읽습니다.

IOptionsMonitorIOptionsSnapshot의 차이점은 다음과 같습니다.

  • IOptionsMonitor는 언제든지 현재 옵션 값을 검색하는 싱글톤 서비스로, 싱글톤 종속성에서 특히 유용합니다.
  • IOptionsSnapshot범위가 지정된 서비스이며 IOptionsSnapshot<T> 개체가 생성될 때 옵션의 스냅샷을 제공합니다. 옵션 스냅숏은 임시 및 범위가 지정된 종속성과 함께 사용하도록 설계되었습니다.

다음 코드에서는 IOptionsSnapshot<TOptions> 메서드를 사용합니다.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

다음 코드에서는 MyOptions가 바인딩되는 구성 인스턴스를 등록합니다.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

위 코드에서는 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

IOptionsMonitor

다음 코드는 MyOptions가 바인딩되는 구성 인스턴스를 등록합니다.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

다음 예제에서는 IOptionsMonitor<TOptions>를 사용합니다.

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

명명된 옵션은 IConfigureNamedOptions 사용을 지원

명명된 옵션:

  • 여러 구성 섹션이 동일한 속성에 바인딩되는 경우에 유용합니다.
  • 대/소문자를 구분합니다.

다음 appsettings.json 파일을 살펴보세요.

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

TopItem:MonthTopItem:Year를 바인딩하는 두 개의 클래스를 만드는 대신 각 섹션에 다음 클래스가 사용됩니다.

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

다음 코드는 명명된 옵션을 구성합니다.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

다음 코드는 명명된 옵션을 표시합니다.

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

모든 옵션은 명명된 인스턴스입니다. IConfigureOptions<TOptions> 인스턴스는 string.EmptyOptions.DefaultName 인스턴스를 대상으로 지정한 것처럼 처리됩니다. 또한 IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions>를 구현합니다. IOptionsFactory<TOptions>의 기본 구현에는 각 옵션을 적절하게 사용하기 위한 논리가 있습니다. null 명명된 옵션은 특정 명명된 인스턴스 대신 모든 명명된 인스턴스를 대상으로 지정하는 데 사용됩니다. ConfigureAllPostConfigureAll에서 이 규칙을 사용합니다.

OptionsBuilder API

OptionsBuilder<TOptions>TOptions 인스턴스를 구성하는 데 사용됩니다. OptionsBuilderAddOptions<TOptions>(string optionsName) 호출에 대한 단일 매개 변수이므로 모든 후속 호출에 나타나는 대신 명명된 옵션 생성을 간소화합니다. 옵션 유효성 검사 및 서비스 종속성을 허용하는 ConfigureOptions 오버로드는 OptionsBuilder를 통해서만 사용할 수 있습니다.

OptionsBuilder옵션 유효성 검사 섹션에서 사용됩니다.

사용자 지정 리포지토리를 추가하는 방법에 대한 내용은 AddOptions를 사용하여 사용자 지정 리포지토리 구성을 참조하세요.

DI 서비스를 사용하여 옵션 구성

다음 두 가지 방법으로 옵션을 구성하는 동안 종속성 주입에서 서비스에 액세스할 수 있습니다.

  • 구성 대리자를 OptionsBuilder<TOptions>Configure에 전달합니다. OptionsBuilder<TOptions>는 최대 5개의 서비스를 사용하여 옵션을 구성할 수 있는 Configure의 오버로드를 제공합니다.

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • IConfigureOptions<TOptions> 또는 IConfigureNamedOptions<TOptions>를 구현하는 형식을 만들고 해당 형식을 서비스로 등록합니다.

서비스 만들기가 더 복잡하기 Configure때문에 구성 대리자를 전달하는 것이 좋습니다. 형식을 만드는 것은 프레임워크가 호출 Configure할 때 수행하는 작업과 동일합니다. Configure를 호출하면 지정된 일반 서비스 유형을 허용하는 생성자가 있는 일시적인 제네릭 IConfigureNamedOptions<TOptions>를 등록합니다.

옵션 유효성 검사

옵션 유효성 검사를 통해 옵션 값의 유효성을 검사할 수 있습니다.

다음 appsettings.json 파일을 살펴보세요.

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

다음 클래스는 "MyConfig" 구성 섹션에 바인딩되고 몇 가지 DataAnnotations 규칙을 적용합니다.

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

코드는 다음과 같습니다.

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

ValidateDataAnnotations 확장 메서드는 Microsoft.Extensions.Options.DataAnnotations NuGet 패키지에서 정의됩니다. Microsoft.NET.Sdk.Web SDK를 사용하는 웹앱의 경우 이 패키지는 공유 프레임워크에서 암시적으로 참조됩니다.

다음 코드는 구성 값 또는 유효성 검사 오류를 표시합니다.

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

다음 코드는 대리자를 사용하여 보다 복잡한 유효성 검사 규칙을 적용합니다.

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions>IValidatableObject

다음 클래스는 IValidateOptions<TOptions>를 구현합니다.

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions를 사용하면 유효성 검사 코드를 Program.cs에서 클래스로 이동할 수 있습니다.

위의 코드에서는 다음 코드를 사용하여 Program.cs에서 유효성 검사를 사용하도록 설정합니다.

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

옵션 유효성 검사도 IValidatableObject를 지원합니다. 클래스 자체 내에서 클래스의 클래스 수준 유효성 검사를 수행하려면 다음을 수행합니다.

  • 클래스 내에서 IValidatableObject 인터페이스 및 해당 Validate 메서드를 구현합니다.
  • ValidateDataAnnotations에서 Program.cs를 호출합니다.

ValidateOnStart

옵션 유효성 검사는 IOptions<TOptions>, IOptionsSnapshot<TOptions>을 처음 실행하거나 IOptionsMonitor<TOptions> 구현이 생성됩니다. 옵션 유효성 검사를 즉시 실행하려면 앱이 시작될 때 Program.cs에서 ValidateOnStart을 호출:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

옵션 사후 구성

IPostConfigureOptions<TOptions>를 사용하여 사후 구성을 설정합니다. 사후 구성은 모든 IConfigureOptions<TOptions> 구성이 수행된 후에 실행됩니다.

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure를 사용하여 명명된 옵션을 사후 구성할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

모든 구성 인스턴스를 사후 구성하려면 PostConfigureAll을 사용합니다.

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Program.cs의 액세스 옵션

Program.cs에서 IOptions<TOptions> 또는 IOptionsMonitor<TOptions>에 액세스하려면 WebApplication.Services에서 GetRequiredService를 호출합니다.

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

추가 리소스

작성자: Kirk LarkinRick Anderson.

옵션 패턴은 클래스를 사용하여 관련 설정 그룹에 대한 강력한 형식의 액세스를 제공합니다. 구성 설정이 시나리오에 따라 별도 클래스로 격리된 경우 앱은 두 가지 중요한 소프트웨어 엔지니어링 원칙을 따릅니다.

  • 캡슐화:
    • 구성 설정에 종속된 클래스는 사용하는 구성 설정에 의해서만 결정됩니다.
  • 우려 사항 분리:
    • 앱의 다른 부분에 대한 설정은 다른 설정에 종속되거나 연결되지 않습니다.

옵션은 구성 데이터의 유효성을 검사하는 메커니즘도 제공합니다. 자세한 내용은 옵션 유효성 검사 섹션을 참조하세요.

이 항목에서는 ASP.NET Core의 옵션 패턴 정보를 제공합니다. 콘솔 앱에서 옵션 패턴을 사용하는 방법에 관한 자세한 내용은 .NET의 옵션 패턴을 참조하세요.

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

계층적 구성 바인딩

관련 구성 값을 읽는 기본 방법은 옵션 패턴를 사용하는 것입니다. 예를 들어 다음 구성 값을 읽으려면:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

다음 PositionOptions 클래스를 만듭니다.

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; }
    public string Name { get; set; }
}

옵션 클래스:

  • 매개 변수가 없는 public 생성자를 사용하는 비추상이어야 합니다.
  • 형식의 모든 공용 읽기-쓰기 속성이 바인딩됩니다.
  • 필드가 바인딩되지 않습니다. 위 코드에서 Position은 바운딩되지 않습니다. Position 속성을 사용하므로 클래스를 구성 공급자에 바인딩할 때 문자열 "Position"을 앱에서 하드 코딩하지 않아도 됩니다.

코드는 다음과 같습니다.

  • ConfigurationBinder.Bind를 호출하여 PositionOptions 클래스를 Position 섹션에 바인딩합니다.
  • Position 구성 데이터를 표시합니다.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

ConfigurationBinder.Get<T>는 지정된 형식을 바인딩하고 반환합니다. ConfigurationBinder.Get<T>ConfigurationBinder.Bind를 사용하는 것보다 편리할 수 있습니다. 다음 코드에서는 ConfigurationBinder.Get<T>PositionOptions 클래스를 함께 사용하는 방법을 보여 줍니다.

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

옵션 패턴을 사용하는 경우 한 가지 대체 방법은 Position 섹션을 바인딩하고 종속성 주입 서비스 컨테이너에 추가하는 것입니다. 다음 코드에서 PositionOptionsConfigure를 통해 서비스 컨테이너에 추가되고 구성에 바인딩됩니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(Configuration.GetSection(
                                        PositionOptions.Position));
    services.AddRazorPages();
}

위의 코드를 사용하여 다음 코드는 위치 옵션을 읽습니다.

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

위 코드에서는 앱을 시작한 후의 JSON 구성 파일의 변경 사항은 읽지 않습니다. 앱을 시작한 후의 변경 사항을 읽으려면 IOptionsSnapshot을 사용합니다.

옵션 인터페이스

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

사후 구성 시나리오에서는 모든 IConfigureOptions<TOptions> 구성이 발생한 후에 옵션을 설정하거나 변경할 수 있습니다.

IOptionsFactory<TOptions>는 새로운 옵션 인스턴스를 만듭니다. 단일 Create 메서드가 있습니다. 기본 구현에서는 등록된 모든 IConfigureOptions<TOptions>IPostConfigureOptions<TOptions>를 사용하며 먼저 구성을 모두 실행한 다음, 사후 구성을 수행합니다. IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions>를 구별하며 적절한 인터페이스만 호출합니다.

IOptionsMonitorCache<TOptions>IOptionsMonitor<TOptions>에서 TOptions 인스턴스를 캐시하는 데 사용됩니다. IOptionsMonitorCache<TOptions>는 모니터의 옵션 인스턴스를 무효화하므로 값이 다시 계산됩니다(TryRemove). TryAdd를 사용하여 값을 수동으로 도입할 수 있습니다. 모든 명명된 인스턴스를 필요에 따라 다시 만들어야 하는 경우 Clear 메서드가 사용됩니다.

IOptionsSnapshot을 사용하여 업데이트된 데이터 읽기

IOptionsSnapshot<TOptions>을 사용하면, 옵션은 엑세스될 때 요청 당 한 번씩 계산되고 요청의 수명 동안 캐시됩니다. 업데이트된 구성 값 읽기를 지원하는 구성 공급자를 사용하는 경우 앱을 시작한 후 구성 변경 내용을 읽습니다.

IOptionsMonitorIOptionsSnapshot의 차이점은 다음과 같습니다.

  • IOptionsMonitor는 언제든지 현재 옵션 값을 검색하는 싱글톤 서비스로, 싱글톤 종속성에서 특히 유용합니다.
  • IOptionsSnapshot범위가 지정된 서비스이며 IOptionsSnapshot<T> 개체가 생성될 때 옵션의 스냅샷을 제공합니다. 옵션 스냅숏은 임시 및 범위가 지정된 종속성과 함께 사용하도록 설계되었습니다.

다음 코드에서는 IOptionsSnapshot<TOptions> 메서드를 사용합니다.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

다음 코드에서는 MyOptions가 바인딩되는 구성 인스턴스를 등록합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

위 코드에서는 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

IOptionsMonitor

다음 코드는 MyOptions가 바인딩되는 구성 인스턴스를 등록합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

다음 예제에서는 IOptionsMonitor<TOptions>를 사용합니다.

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

위 코드에서는 기본적으로 앱을 시작한 후의 JSON 구성 파일의 변경 사항을 읽습니다.

명명된 옵션은 IConfigureNamedOptions 사용을 지원

명명된 옵션:

  • 여러 구성 섹션이 동일한 속성에 바인딩되는 경우에 유용합니다.
  • 대/소문자를 구분합니다.

다음 appsettings.json 파일을 살펴보세요.

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

TopItem:MonthTopItem:Year를 바인딩하는 두 개의 클래스를 만드는 대신 각 섹션에 다음 클래스가 사용됩니다.

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; }
    public string Model { get; set; }
}

다음 코드는 명명된 옵션을 구성합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TopItemSettings>(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure<TopItemSettings>(TopItemSettings.Year,
                                        Configuration.GetSection("TopItem:Year"));

    services.AddRazorPages();
}

다음 코드는 명명된 옵션을 표시합니다.

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

모든 옵션은 명명된 인스턴스입니다. IConfigureOptions<TOptions> 인스턴스는 string.EmptyOptions.DefaultName 인스턴스를 대상으로 지정한 것처럼 처리됩니다. 또한 IConfigureNamedOptions<TOptions>IConfigureOptions<TOptions>를 구현합니다. IOptionsFactory<TOptions>의 기본 구현에는 각 옵션을 적절하게 사용하기 위한 논리가 있습니다. null 명명된 옵션은 특정 명명된 인스턴스 대신 모든 명명된 인스턴스를 대상으로 지정하는 데 사용됩니다. ConfigureAllPostConfigureAll에서 이 규칙을 사용합니다.

OptionsBuilder API

OptionsBuilder<TOptions>TOptions 인스턴스를 구성하는 데 사용됩니다. OptionsBuilderAddOptions<TOptions>(string optionsName) 호출에 대한 단일 매개 변수이므로 모든 후속 호출에 나타나는 대신 명명된 옵션 생성을 간소화합니다. 옵션 유효성 검사 및 서비스 종속성을 허용하는 ConfigureOptions 오버로드는 OptionsBuilder를 통해서만 사용할 수 있습니다.

OptionsBuilder옵션 유효성 검사 섹션에서 사용됩니다.

사용자 지정 리포지토리를 추가하는 방법에 대한 내용은 AddOptions를 사용하여 사용자 지정 리포지토리 구성을 참조하세요.

DI 서비스를 사용하여 옵션 구성

다음 두 가지 방법으로 옵션을 구성하는 동안 종속성 주입에서 서비스에 액세스할 수 있습니다.

  • 구성 대리자를 OptionsBuilder<TOptions>Configure에 전달합니다. OptionsBuilder<TOptions>는 최대 5개의 서비스를 사용하여 옵션을 구성할 수 있는 Configure의 오버로드를 제공합니다.

    services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • IConfigureOptions<TOptions> 또는 IConfigureNamedOptions<TOptions>를 구현하는 형식을 만들고 해당 형식을 서비스로 등록합니다.

서비스 만들기가 더 복잡하기 Configure때문에 구성 대리자를 전달하는 것이 좋습니다. 형식을 만드는 것은 프레임워크가 호출 Configure할 때 수행하는 작업과 동일합니다. Configure를 호출하면 지정된 일반 서비스 유형을 허용하는 생성자가 있는 일시적인 제네릭 IConfigureNamedOptions<TOptions>를 등록합니다.

옵션 유효성 검사

옵션 유효성 검사를 통해 옵션 값의 유효성을 검사할 수 있습니다.

다음 appsettings.json 파일을 살펴보세요.

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

다음 클래스는 "MyConfig" 구성 섹션에 바인딩되고 몇 가지 DataAnnotations 규칙을 적용합니다.

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

코드는 다음과 같습니다.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions<MyConfigOptions>()
            .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

        services.AddControllersWithViews();
    }

ValidateDataAnnotations 확장 메서드는 Microsoft.Extensions.Options.DataAnnotations NuGet 패키지에서 정의됩니다. Microsoft.NET.Sdk.Web SDK를 사용하는 웹앱의 경우 이 패키지는 공유 프레임워크에서 암시적으로 참조됩니다.

다음 코드는 구성 값 또는 유효성 검사 오류를 표시합니다.

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
             msg = $"Key1: {_config.Value.Key1} \n" +
                   $"Key2: {_config.Value.Key2} \n" +
                   $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

다음 코드는 대리자를 사용하여 보다 복잡한 유효성 검사 규칙을 적용합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<MyConfigOptions>()
        .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
        .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

    services.AddControllersWithViews();
}

복잡한 유효성 검사를 위한 IValidateOptions

다음 클래스는 IValidateOptions<TOptions>를 구현합니다.

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string vor=null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions를 사용하면 유효성 검사 코드를 StartUp에서 클래스로 이동할 수 있습니다.

위의 코드에서는 다음 코드를 사용하여 Startup.ConfigureServices에서 유효성 검사를 사용하도록 설정합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyConfigOptions>(Configuration.GetSection(
                                        MyConfigOptions.MyConfig));
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>());
    services.AddControllersWithViews();
}

옵션 사후 구성

IPostConfigureOptions<TOptions>를 사용하여 사후 구성을 설정합니다. 사후 구성은 모든 IConfigureOptions<TOptions> 구성이 수행된 후에 실행됩니다.

services.PostConfigure<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

PostConfigure를 사용하여 명명된 옵션을 사후 구성할 수 있습니다.

services.PostConfigure<MyOptions>("named_options_1", myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

모든 구성 인스턴스를 사후 구성하려면 PostConfigureAll을 사용합니다.

services.PostConfigureAll<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

시작하는 동안 옵션 액세스

Configure 메서드가 실행되기 전에 서비스가 빌드되므로 Startup.Configure에서 IOptions<TOptions>IOptionsMonitor<TOptions>를 사용할 수 있습니다.

public void Configure(IApplicationBuilder app, 
    IOptionsMonitor<MyOptions> optionsAccessor)
{
    var option1 = optionsAccessor.CurrentValue.Option1;
}

Startup.ConfigureServices에서는 IOptions<TOptions> 또는 IOptionsMonitor<TOptions>를 사용하지 마세요. 서비스 등록의 순서 지정으로 인해 일관성 없는 옵션 상태가 있을 수 있습니다.

Options.ConfigurationExtensions NuGet 패키지

Microsoft.Extensions.Options.ConfigurationExtensions 패키지는 ASP.NET Core 앱에서 암시적으로 참조됩니다.