.NET 라이브러리 작성자를 위한 옵션 패턴 지침

종속성 주입의 도움으로 서비스 및 해당 구성을 등록하면 ‘옵션 패턴’을 사용할 수 있습니다. 옵션 패턴을 사용하면 라이브러리(및 서비스)의 소비자가 옵션 인터페이스 인스턴스를 요구할 수 있습니다. 여기서 TOptions는 옵션 클래스입니다. 강력한 형식의 개체를 통해 구성 옵션을 사용하면 일관된 값 표현을 보장하고, 데이터 주석으로 유효성을 검사할 수 있으며, 문자열 값을 수동으로 구문 분석해야 하는 부담이 없어집니다. 라이브러리의 소비자가 사용할 수 있는 구성 공급자가 많이 있습니다. 이러한 공급자를 통해 소비자는 여러 방법으로 라이브러리를 구성할 수 있습니다.

이 문서에서는 .NET 라이브러리 작성자가 라이브러리의 소비자에게 옵션 패턴을 올바르게 노출하는 방법에 대한 일반적인 지침을 알아봅니다. 여러 방법으로 동일한 목적을 달성할 수 있으며 몇 가지 고려할 사항이 있습니다.

명명 규칙

규칙에 따라 서비스 등록을 담당하는 확장 메서드는 이름이 Add{Service}로 지정됩니다. 여기서 {Service}는 의미 있고 설명이 포함된 이름입니다. Add{Service} 확장 메서드는 ASP.NET Core와 .NET 모두에서 일반적입니다.

✔️ 서비스를 다른 제품과 명확하게 구분하는 이름을 고려하세요.

❌ 공식 Microsoft 패키지의 .NET 에코시스템에 이미 포함된 이름은 사용하지 마세요.

✔️ 확장 메서드를 노출하는 정적 클래스의 이름을 {Type}Extensions로 지정하는 것이 좋습니다. 여기서 {Type}는 확장하는 형식입니다.

네임스페이스 지침

Microsoft 패키지는 Microsoft.Extensions.DependencyInjection 네임스페이스를 활용하여 다양한 서비스 제품의 등록을 통합합니다.

✔️ 패키지 제품을 명확하게 식별하는 네임스페이스를 고려하세요.

❌ 비공식 Microsoft 패키지에 Microsoft.Extensions.DependencyInjection 네임스페이스를 사용하지 마세요.

매개 변수가 없음

서비스를 최소한의 구성으로 또는 명시적 구성 없이 사용할 수 있는 경우에는 매개 변수가 없는 확장 메서드를 사용하는 것이 좋습니다.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
        this IServiceCollection services)
    {
        services.AddOptions<LibraryOptions>()
            .Configure(options =>
            {
                // Specify default option values
            });

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

위의 코드에서 AddMyLibraryService

IConfiguration 매개 변수

소비자에게 많은 옵션을 노출하는 라이브러리를 작성하는 경우 IConfiguration 매개 변수 확장 메서드를 요구하는 것이 좋습니다. 필요한 IConfiguration 인스턴스는 IConfiguration.GetSection 함수를 사용하여 구성의 명명된 섹션으로 범위가 지정되어야 합니다.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      IConfiguration namedConfigurationSection)
    {
        // Default library options are overridden
        // by bound configuration values.
        services.Configure<LibraryOptions>(namedConfigurationSection);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

위의 코드에서 AddMyLibraryService

이 패턴의 소비자는 명명된 섹션의 범위가 지정된 IConfiguration 인스턴스를 제공합니다.

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(
    builder.Configuration.GetSection("LibraryOptions"));

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

.AddMyLibraryService 호출은 IServiceCollection 형식에서 이루어집니다.

기본값 지정은 라이브러리 작성자가 해야 합니다.

참고 항목

구성을 옵션 인스턴스에 바인딩할 수 있습니다. 그러나 이름 충돌 때문에 오류가 발생할 위험이 있습니다. 또한 이렇게 수동으로 바인딩하는 경우 옵션 패턴 소비를 한 번만 읽기로 제한합니다. 이러한 소비자는 IOptionsMonitor 인터페이스를 사용할 수 없기 때문에 설정 변경 사항이 다시 바인딩되지 않습니다.

services.AddOptions<LibraryOptions>()
    .Configure<IConfiguration>(
        (options, configuration) =>
            configuration.GetSection("LibraryOptions").Bind(options));

대신 BindConfiguration 확장 메서드를 사용해야 합니다. 이 확장 메서드는 구성을 옵션 인스턴스에 바인딩하고 구성 섹션에 대한 변경 토큰 원본도 등록합니다. 이를 통해 소비자는 IOptionsMonitor 인터페이스를 사용할 수 있습니다.

구성 섹션 경로 매개 변수

라이브러리의 소비자는 기본 TOptions 형식을 바인딩하는 구성 섹션 경로를 지정할 수 있습니다. 이 시나리오에서는 확장 메서드에서 string 매개 변수를 정의합니다.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      string configSectionPath)
    {
        services.AddOptions<SupportOptions>()
            .BindConfiguration(configSectionPath)
            .ValidateDataAnnotations()
            .ValidateOnStart();

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

위의 코드에서 AddMyLibraryService

  • IServiceCollection의 인스턴스를 확장합니다.
  • string 매개 변수 configSectionPath를 정의합니다.
  • 호출:
    • SupportOptions의 제네릭 형식 매개 변수를 사용하여 AddOptions
    • 지정된 configSectionPath 매개 변수를 사용하여 BindConfiguration
    • 데이터 주석 유효성 검사를 사용하도록 설정하기 위해 ValidateDataAnnotations
    • 런타임이 아닌 시작 시 유효성 검사를 적용하기 위해 ValidateOnStart

다음 예제에서는 Microsoft.Extensions.Options.DataAnnotations NuGet 패키지를 사용하여 데이터 주석 유효성 검사를 사용하도록 설정합니다. SupportOptions 클래스는 다음과 같이 정의됩니다.

using System.ComponentModel.DataAnnotations;

public sealed class SupportOptions
{
    [Url]
    public string? Url { get; set; }

    [Required, EmailAddress]
    public required string Email { get; set; }

    [Required, DataType(DataType.PhoneNumber)]
    public required string PhoneNumber { get; set; }
}

다음 JSON appsettings.json 파일이 사용된다고 상상해 보세요.

{
    "Support": {
        "Url": "https://support.example.com",
        "Email": "help@support.example.com",
        "PhoneNumber": "+1(888)-SUPPORT"
    }
}

Action<TOptions> 매개 변수

라이브러리 소비자가 옵션 클래스의 인스턴스를 생성하는 람다 식을 제공하는 데 관심이 있을 수 있습니다. 이 시나리오에서는 확장 메서드에서 Action<LibraryOptions> 매개 변수를 정의합니다.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
        this IServiceCollection services,
        Action<LibraryOptions> configureOptions)
    {
        services.Configure(configureOptions);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

위의 코드에서 AddMyLibraryService

  • IServiceCollection의 인스턴스를 확장합니다.
  • Action<T> 매개 변수 configureOptions를 정의합니다. 여기서 TLibraryOptions입니다.
  • configureOptions 작업이 지정되면 Configure를 호출합니다.

이 패턴의 소비자는 람다 식(또는 Action<LibraryOptions> 매개 변수를 충족하는 대리자)을 제공합니다.

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(options =>
{
    // User defined option values
    // options.SomePropertyValue = ...
});
                                                                        
using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

옵션 인스턴스 매개 변수

라이브러리 소비자가 인라인 옵션 인스턴스를 제공하는 것을 선호할 수 있습니다. 이 시나리오에서는 옵션 개체 LibraryOptions의 인스턴스를 사용하는 확장 메서드를 노출합니다.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      LibraryOptions userOptions)
    {
        services.AddOptions<LibraryOptions>()
            .Configure(options =>
            {
                // Overwrite default option values
                // with the user provided options.
                // options.SomeValue = userOptions.SomeValue;
            });

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

위의 코드에서 AddMyLibraryService

이 패턴의 소비자는 LibraryOptions 클래스의 인스턴스를 제공하여 원하는 속성 값을 인라인으로 정의합니다.

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(new LibraryOptions
{
    // Specify option values
    // SomePropertyValue = ...
});

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

구성 후

모든 구성 옵션 값을 바인딩하거나 지정한 후에는 사후 구성 기능을 사용할 수 있습니다. 앞서 설명한 것과 동일한 Action<TOptions> 매개 변수를 노출하여 PostConfigure를 호출하도록 선택할 수 있습니다. 사후 구성은 모든 .Configure 호출 후에 실행됩니다. PostConfigure를 사용하는 것을 고려하려는 몇 가지 이유가 있습니다.

  • 실행 순서: .Configure 호출에 설정된 구성 값을 재정의할 수 있습니다.
  • 유효성 검사: 다른 모든 구성이 적용된 후 기본값이 설정되었는지 확인할 수 있습니다.
using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      Action<LibraryOptions> configureOptions)
    {
        services.PostConfigure(configureOptions);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

위의 코드에서 AddMyLibraryService

  • IServiceCollection의 인스턴스를 확장합니다.
  • Action<T> 매개 변수 configureOptions를 정의합니다. 여기서 TLibraryOptions입니다.
  • configureOptions 작업이 지정되면 PostConfigure를 호출합니다.

이 패턴의 소비자는 사후 구성을 사용하지 않는 시나리오의 Action<TOptions> 매개 변수와 마찬가지로 람다 식(또는 Action<LibraryOptions> 매개 변수를 충족하는 대리자)을 제공합니다.

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(options =>
{
    // Specify option values
    // options.SomePropertyValue = ...
});

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

참고 항목