ASP.NET Core 구성하기

등록일시: 2017-08-28 08:00,  수정일시: 2017-10-24 14:03
조회수: 7,251
이 문서는 ASP.NET Core 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 한 가지 이상의 구성 공급자를 추가해서 ASP.NET Core 응용 프로그램을 구성하고, 그 설정 정보에 다양한 방식으로 접근할 수 있는 방법을 살펴봅니다.

구성 API는 이름-값 쌍 목록을 기반으로 하는 응용 프로그램 구성 방법을 제공합니다. 구성은 런타임 시에 다양한 원본으로부터 읽어 들여지며, 이름-값 쌍을 다단계 계층 구조로 그룹화시킬 수도 있습니다. 다음은 몇 가지 구성 공급자들입니다:

각각의 구성 값은 문자열 키에 매핑되며 설정을 사용자 지정 POCO 개체로 (즉, 속성이 존재하는 간단한 .NET 클래스로) 역직렬화하는 내장 바인딩 기능도 지원됩니다.

예제 코드 살펴보기 및 다운로드

간단한 구성

다음 예제 콘솔 응용 프로그램은 JSON 구성 공급자를 사용합니다:

using Microsoft.Extensions.Configuration;
using System;
using System.IO;

// Add NuGet <package id="Microsoft.Extensions.Configuration" and
// <package id="Microsoft.Extensions.Configuration.Json"
// .NET Framework 4.x use the following path:
// .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), @"..\.."))

public class Program
{
    public static IConfigurationRoot Configuration { get; set; }
    public static void Main(string[] args = null)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json");

        Configuration = builder.Build();

        Console.WriteLine($"option1 = {Configuration["option1"]}");
        Console.WriteLine($"option2 = {Configuration["option2"]}");
        Console.WriteLine($"suboption1 = {Configuration["subsection:suboption1"]}");
        Console.WriteLine();

        Console.WriteLine("Wizards:");
        Console.Write($"{Configuration["wizards:0:Name"]}, ");
        Console.WriteLine($"age {Configuration["wizards:0:Age"]}");
        Console.Write($"{Configuration["wizards:1:Name"]}, ");
        Console.WriteLine($"age {Configuration["wizards:1:Age"]}");
    }
}

이 응용 프로그램은 다음 구성 설정을 읽고 출력합니다:

{
  "option1": "value1_from_json",
  "option2": 2,

  "subsection": {
    "suboption1": "subvalue1_from_json"
  },
  "wizards": [
    {
      "Name": "Gandalf",
      "Age": "1000"
    },
    {
      "Name": "Harry",
      "Age": "17"
    }
  ]
}

구성은 노드가 콜론으로 구분되는 이름-값 쌍의 계층적 목록으로 구성됩니다. 값을 조회하려면 대상 항목의 키를 이용해서 Configuration 인덱서에 접근하면 됩니다:

Console.WriteLine($"option1 = {Configuration["subsection:suboption1"]}");

JSON 형식의 구성 원본에 포함된 배열에 접근하려면 콜론으로 구분된 문자열의 일부로 배열의 인덱스를 사용합니다. 가령, 다음 예제는 위의 wizards 배열 중 첫 번째 항목의 Name 속성 값을 가져옵니다:

Console.Write($"{Configuration["wizards:0:Name"]}, ");

기본 제공되는 내장 Configuration 공급자를 사용해서 기록된 이름-값 쌍은 저장되지 않으며, 대신 값을 저장하는 사용자 지정 공급자를 직접 구현할 수 있습니다. 자세한 정보는 사용자 지정 구성 공급자 절을 참고하시기 바랍니다.

방금 살펴본 예제는 Configuration 인덱서를 이용해서 값을 읽습니다. Startup 클래스의 외부에서 구성에 접근하려면 Options 패턴을 사용하십시오. Options 패턴에 관해서는 본문의 뒷부분에서 다시 살펴봅니다.

일반적으로 개발, 테스트, 그리고 운영 같은 다양한 환경에 대해 서로 개별적인 구성 설정을 지정하는 것이 보통입니다. 다음 코드에 강조된 코드는 두 가지 구성 공급자를 세 가지 원본에 연결합니다:

  1. JSON 공급자, appsettings.json 파일을 읽습니다.
  2. JSON 공급자, appsettings.<EnvironmentName>.json 파일을 읽습니다.
  3. 환경 변수 공급자
public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

메서드의 매개 변수에 대한 자세한 설명은 AddJsonFile 문서를 참고하십시오. 참고로 reloadOnChange 매개 변수는 ASP.NET Core 1.1 이상에서만 지원됩니다.

구성 원본은 지정된 순서대로 읽혀지므로, 이 코드에서는 환경 변수가 가장 마지막으로 읽혀집니다. 그리고 환경을 통해서 설정된 모든 구성 값이 이전의 두 공급자에서 설정된 값을 대체합니다.

일반적으로 환경은 Development, Staging 또는 Production 중 하나로 설정합니다. 이에 대한 보다 자세한 설명은 다양한 환경에서 작업하기 문서를 참고하시기 바랍니다.

구성 시 고려사항:

  • IOptionsSnapshot을 사용하면 구성 데이터가 변경됐을 때 구성을 다시 로드할 수 있습니다. 구성 데이터를 다시 로드해야 할 필요성이 있다면 IOptionsSnapshot을 사용하십시오. 더 자세한 정보는 IOptionsSnapshot 절을 참고하시기 바랍니다.
  • 구성 키는 대소문자를 구분하지 않습니다.
  • 가급적 환경 변수를 가장 마지막에 지정해서, 배포된 구성 파일에 설정된 모든 항목을 로컬 환경이 재지정할 수 있게 만드는 것이 좋습니다.
  • 구성 공급자 코드 또는 일반 텍스트 구성 파일에는 절대로 비밀번호나 기타 민감한 데이터를 저장하면 안됩니다. 보안에 민감한 운영 환경 정보를 개발 환경이나 테스트 환경에서 사용해서도 안됩니다. 대신 프로젝트 트리 외부에 민감한 정보를 지정함으로써 실수로 저장소에 커밋되는 일이 없도록 하십시오. 보다 자세한 내용은 다양한 환경에서 작업하기 문서와 개발 중 민감한 응용 프로그램 정보 안전하게 저장하기 문서를 참고하시기 바랍니다.
  • 만약 사용 중인 시스템의 환경 변수에서 : 문자를 사용할 수 없다면, :__(이중 밑줄)로 대체합니다.

Options 패턴 및 구성 개체 사용하기

Options 패턴은 사용자 지정 옵션 클래스를 이용해서 관련된 설정 그룹을 나타냅니다. 응용 프로그램이 갖고 있는 각각의 기능마다 개별적으로 분리된 클래스를 생성하는 것이 좋습니다. 그리고 분리된 클래스는 다음과 같은 원칙을 준수해야 합니다:

다음과 같이 옵션 클래스는 매개 변수가 없는 public 생성자를 갖고 있어야 하고, 추상 클래스가 아니어야 합니다:

public class MyOptions
{
    public MyOptions()
    {
        // Set default value.
        Option1 = "value1_from_ctor";
    }

    public string Option1 { get; set; }
    public int Option2 { get; set; } = 5;
}

다음 코드는 JSON 구성 공급자를 활성화시킵니다. 그런 다음, MyOptions 클래스를 서비스 컨테이너에 추가하고 구성과 바인딩합니다.

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        // Set up configuration sources.
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; set; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Adds services required for using options.
        services.AddOptions();

        // Register the IConfiguration instance which MyOptions binds against.
        services.Configure<MyOptions>(Configuration);

        // Add framework services.
        services.AddMvc();
    }

그리고 컨트롤러에서는 생성자 의존성 주입으로 전달받은 IOptions<TOptions> 개체를 통해서 설정에 접근합니다:

public class HomeController : Controller
{
    private readonly MyOptions _options;

    public HomeController(IOptions<MyOptions> optionsAccessor)
    {
        _options = optionsAccessor.Value;
    }

    public IActionResult Index()
    {
        var option1 = _options.Option1;
        var option2 = _options.Option2;
        return Content($"option1 = {option1}, option2 = {option2}");
    }
}

이번 예제에서 다음과 같은 appsettings.json 파일을 사용할 경우:

{
  "option1": "value1_from_json",
  "option2": 2
}

HomeController.Index 메서드는 option1 = value1_from_json, option2 = 2를 반환합니다.

일반적으로 대부분의 응용 프로그램은 전체 구성을 단일 옵션 파일에 바인딩하지 않습니다. 잠시 후 GetSection 메서드를 사용해서 특정 섹션을 바인딩하는 방법을 살펴보겠습니다.

다음 코드는 두 번째 IConfigureOptions<TOptions> 서비스를 서비스 컨테이너에 추가합니다. 또한 이번에는 대리자를 사용해서 MyOptions에 대한 바인딩을 구성하고 있습니다.

public void ConfigureServices(IServiceCollection services)
{
    // Adds services required for using options.
    services.AddOptions();

    // Register the ConfigurationBuilder instance which MyOptions binds against.
    services.Configure<MyOptions>(Configuration);

    // Registers the following lambda used to configure options.
    services.Configure<MyOptions>(myOptions =>
    {
        myOptions.Option1 = "value1_from_action";
    });

    // Add framework services.
    services.AddMvc();
}

이 코드에서 볼 수 있는 것처럼 필요하다면 동시에 여러 개의 구성 공급자를 추가할 수 있습니다. 구성 공급자는 NuGet 패키지로 제공되며, 등록된 순서대로 적용됩니다.

그리고 Configure<TOptions> 메서드를 호출할 때마다 서비스 컨테이너에 IConfigureOptions<TOptions> 서비스가 추가됩니다. 이번 예제의 경우, Option1Option2의 값은 모두 appsettings.json 파일에 지정되어 있지만 Option1의 값은 구성된 대리자에 의해서 재정의됩니다.

두 개 이상의 구성 서비스가 활성화된 경우, 마지막으로 지정된 구성 원본의 구성 값이 적용됩니다. 따라서 HomeController.Index 메서드는 option1 = value1_from_action, option2 = 2를 반환합니다.

옵션을 구성과 바인딩하면 옵션 형식의 각 속성과 property[:sub-property:] 형식의 구성 키가 바인딩됩니다. 예를 들어서, MyOptions.Option1 속성은 appsettings.json 파일의 option1 속성에서 읽은 Option1 키에 바인딩됩니다. 하위 속성에 관한 예제는 잠시 후에 살펴봅니다.

다음 코드는 세 번째 IConfigureOptions<TOptions> 서비스를 서비스 컨테이너에 추가하고, MySubOptionsappsettings.json 파일의 subsection 섹션과 바인딩합니다:

public void ConfigureServices(IServiceCollection services)
{
    // Adds services required for using options.
    services.AddOptions();

    // Configure with Microsoft.Extensions.Options.ConfigurationExtensions
    // Binding the whole configuration should be rare, subsections are more typical.
    services.Configure<MyOptions>(Configuration);

    // Configure MyOptions using code.
    services.Configure<MyOptions>(myOptions =>
    {
        myOptions.Option1 = "value1_from_action";
    });

    // Configure using a sub-section of the appsettings.json file.
    services.Configure<MySubOptions>(Configuration.GetSection("subsection"));

    // Add framework services.
    services.AddMvc();
}

노트: 이 확장 메서드를 사용하려면 Microsoft.Extensions.Options.ConfigurationExtensions NuGet 패키지가 필요합니다.

만약 다음과 같은 appsettings.json 파일을 사용하고:

{
  "option1": "value1_from_json",
  "option2": -1,

  "subsection": {
    "suboption1": "subvalue1_from_json",
    "suboption2": 200
  }
}

MySubOptions 클래스가 다음과 같다면:

public class MySubOptions
{
    public MySubOptions()
    {
        // Set default values.
        SubOption1 = "value1_from_ctor";
        SubOption2 = 5;
    }

    public string SubOption1 { get; set; }
    public int SubOption2 { get; set; }
}

다음 컨트롤러는:

public class HomeController : Controller
{
    private readonly MySubOptions _subOptions;

    public HomeController(IOptions<MySubOptions> subOptionsAccessor)
    {
        _subOptions = subOptionsAccessor.Value;
    }

    public IActionResult Index()
    {
        var subOption1 = _subOptions.SubOption1;
        var subOption2 = _subOptions.SubOption2;
        return Content($"subOption1 = {subOption1}, subOption2 = {subOption2}");
    }
}

subOption1 = subvalue1_from_json, subOption2 = 200을 반환합니다.

IOptionsSnapshot

이 기능을 사용하려면 ASP.NET Core 1.1 이상이 필요합니다.

IOptionsSnapshot을 이용하면 구성 파일이 변경될 때 구성 데이터를 다시 로드할 수 있습니다. 이 기능은 적은 부하만 발생시킵니다. IOptionsSnapshotreloadOnChange: true 인자와 함께 사용하면, 옵션이 IConfiguration에 바인딩되고 변경 시 다시 로드됩니다.

다음 예제는 config.json 파일이 변경됐을 때, 새로운 IOptionsSnapshot이 생성되는 방법을 보여줍니다. config.json 파일이 변경되지 않은 경우에는 서버 요청의 응답으로 항상 동일한 시간이 반환됩니다. 그러나 config.json 파일이 변경되고 난 이후, 첫 번째 요청 시에는 새로운 시간이 반환됩니다.

public class TimeOptions
{
    // Records the time when the options are created.
    public DateTime CreationTime { get; set; } = DateTime.Now;

    // Bound to config. Changes to the value of "Message"
    // in config.json will be reflected in this property.
    public string Message { get; set; }
}

public class Controller
{
    public readonly TimeOptions _options;

    public Controller(IOptionsSnapshot<TimeOptions> options)
    {
        _options = options.Value;
    }

    public Task DisplayTimeAsync(HttpContext context)
    {
        return context.Response.WriteAsync(_options.Message + _options.CreationTime);
    }
}

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            // reloadOnChange: true is required for config changes to be detected.
            .AddJsonFile("config.json", optional: false, reloadOnChange: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; set; }

    public void Configure(IApplicationBuilder app)
    {
        // Simple mockup of a simple per request controller that writes
        // the creation time and message of TimeOptions.
        app.Run(DisplayTimeAsync);
    }

    public void ConfigureServices(IServiceCollection services)
    {
        // Simple mockup of a simple per request controller.
        services.AddScoped<Controller>();

        // Binds config.json to the options and setups the change tracking.
        services.Configure<TimeOptions>(Configuration.GetSection("Time"));
    }

    public Task DisplayTimeAsync(HttpContext context)
    {
        context.Response.ContentType = "text/plain";
        return context.RequestServices.GetRequiredService<Controller>().DisplayTimeAsync(context);
    }

    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();
        host.Run();
    }
}

다음 그림은 서버가 반환한 결과를 보여줍니다:

browser image showing "Last Updated: 11/22/2016 4:43 PM"

반복해서 브라우저를 새로 고침하더라도 config.json 파일이 변경되지 않는한 출력된 메시지 값이나 시간은 변경되지 않습니다.

그러나 config.json 파일을 변경하고 저장한 다음 브라우저를 새로 고침하면 결과가 변경됩니다:

browser image showing "Last Updated to,e: 11/22/2016 4:53 PM"

메모리 내 공급자 및 POCO 클래스와 바인딩하기

다음 예제는 메모리 내 공급자를 사용해서 클래스에 바인딩하는 방법을 보여줍니다:

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;

// Add NuGet  <package id="Microsoft.Extensions.Configuration.Binder"
public class Program
{   
    public static IConfigurationRoot Configuration { get; set; }
    public static void Main(string[] args = null)
    {
        var dict = new Dictionary<string, string>
            {
                {"Profile:MachineName", "Rick"},
                {"App:MainWindow:Height", "11"},
                {"App:MainWindow:Width", "11"},
                {"App:MainWindow:Top", "11"},
                {"App:MainWindow:Left", "11"}
            };

        var builder = new ConfigurationBuilder();
        builder.AddInMemoryCollection(dict);
        Configuration = builder.Build();
        Console.WriteLine($"Hello {Configuration["Profile:MachineName"]}");

        var window = new MyWindow();
        Configuration.GetSection("App:MainWindow").Bind(window);
        Console.WriteLine($"Left {window.Left}");
    }
}

구성 값은 문자열 형식으로 반환되지만, 바인딩을 사용하면 개체의 생성할 수 있습니다. 바인딩을 통해서 POCO 개체 또는 전체 개체 그래프까지 조회할 수 있습니다. 다음 예제는 MyWindow에 바인딩하는 방법과 ASP.NET Core MVC 응용 프로그램에서 Options 패턴을 사용하는 방법을 보여줍니다:

public class MyWindow
{
    public int Height { get; set; }
    public int Width { get; set; }
    public int Top { get; set; }
    public int Left { get; set; }
}
{
  "AppConfiguration": {
    "MainWindow": {
      "Height": "400",
      "Width": "600",
      "Top": "5",
      "Left": "11"
    }
  }
}

Startup 클래스의 ConfigureServices 메서드에서 사용자 지정 클래스를 바인딩합니다:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyWindow>(Configuration.GetSection("AppConfiguration:MainWindow"));
    services.AddMvc();
}

그리고 HomeController에서 설정을 출력합니다:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

public class HomeController : Controller
{
    private readonly IOptions<MyWindow> _optionsAccessor;

    public HomeController(IOptions<MyWindow> optionsAccessor)
    {
        _optionsAccessor = optionsAccessor;
    }

    public IActionResult Index()
    {
        var height = _optionsAccessor.Value.Height;
        var width = _optionsAccessor.Value.Width;
        var left = _optionsAccessor.Value.Left;
        var top = _optionsAccessor.Value.Top;

        return Content($"height = {height}, width = {width}, "
                        + $"Left = {left}, Top = {top}");
    }
}

GetValue

다음 예제는 GetValue 확장 메서드의 사용법을 보여줍니다:

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;

// Add NuGet  <package id="Microsoft.Extensions.Configuration.Binder"
public class Program
{   
    public static IConfigurationRoot Configuration { get; set; }
    public static void Main(string[] args = null)
    {
        var dict = new Dictionary<string, string>
            {
                {"Profile:MachineName", "Rick"},
                {"App:MainWindow:Height", "11"},
                {"App:MainWindow:Width", "11"},
                {"App:MainWindow:Top", "11"},
                {"App:MainWindow:Left", "11"}
            };

        var builder = new ConfigurationBuilder();
        builder.AddInMemoryCollection(dict);
        Configuration = builder.Build();
        Console.WriteLine($"Hello {Configuration["Profile:MachineName"]}");

        // Show GetValue overload and set the default value to 80
        // Requires NuGet package "Microsoft.Extensions.Configuration.Binder"
        var left = Configuration.GetValue<int>("App:MainWindow:Left", 80);
        Console.WriteLine($"Left {left}");

        var window = new MyWindow();
        Configuration.GetSection("App:MainWindow").Bind(window);
        Console.WriteLine($"Left {window.Left}");
    }
}

ConfigurationBinderGetValue<T> 메서드에 기본 값을 지정할 수도 있습니다 (예제에서는 80). GetValue<T> 메서드는 단순한 시나리오를 대상으로 하고 있으며 전체 섹션에 바인딩되지 않습니다. GetValue<T> 메서드는 GetSection(key).Value로부터 특정 형식으로 변환된 스칼라 값을 가져옵니다.

개체 그래프와 바인딩하기

클래스 내부의 각 객체에 재귀적으로 바인딩 할 수도 있습니다. 다음과 같은 AppOptions 클래스를 가정해보겠습니다:

public class AppOptions
{
    public Window Window { get; set; }
    public Connection Connection { get; set; }
    public Profile Profile { get; set; }
}

public class Window
{
    public int Height { get; set; }
    public int Width { get; set; }
}

public class Connection
{
    public string Value { get; set; }
}

public class Profile
{
    public string Machine { get; set; }
}

다음 예제는 AppOptions 클래스에 바인딩합니다:

using Microsoft.Extensions.Configuration;
using System;
using System.IO;

// Add these NuGet packages:
// "Microsoft.Extensions.Configuration.Binder"
// "Microsoft.Extensions.Configuration.FileExtensions"
// "Microsoft.Extensions.Configuration.Json": 
public class Program
{
    public static void Main(string[] args = null)
    {
        var builder = new ConfigurationBuilder()
                    .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json");

        var config = builder.Build();

        var appConfig = new AppOptions();
        config.GetSection("App").Bind(appConfig);

        Console.WriteLine($"Height {appConfig.Window.Height}");
    }
}

ASP.NET Core 1.1 이상에서는 전체 섹션을 대상으로 동작하는 Get<T>를 사용할 수 있습니다. Bind 보다 Get<T>가 더 편리합니다. 다음 코드는 위의 예제에서 Get<T>를 사용하는 방법을 보여줍니다:

var appConfig = config.GetSection("App").Get<AppOptions>();

다음 appsettings.json 파일을 사용할 경우:

{
  "App": {
    "Profile": {
      "Machine": "Rick"
    },
    "Connection": {
      "Value": "connectionstring"
    },
    "Window": {
      "Height": "11",
      "Width": "11"
    }
  }
}

프로그램은 Height 11을 출력합니다.

다음 코드는 구성을 단위 테스트하기 위한 용도로 사용할 수 있습니다:

[Fact]
public void CanBindObjectTree()
{
    var dict = new Dictionary<string, string>
        {
            {"App:Profile:Machine", "Rick"},
            {"App:Connection:Value", "connectionstring"},
            {"App:Window:Height", "11"},
            {"App:Window:Width", "11"}
        };
    var builder = new ConfigurationBuilder();
    builder.AddInMemoryCollection(dict);
    var config = builder.Build();

    var options = new AppOptions();
    config.GetSection("App").Bind(options);

    Assert.Equal("Rick", options.Profile.Machine);
    Assert.Equal(11, options.Window.Height);
    Assert.Equal(11, options.Window.Width);
    Assert.Equal("connectionstring", options.Connection.Value);
}

Entity Framework 사용자 지정 공급자 기본 예제

이번 절에서는 EF를 사용해서 데이터베이스로부터 이름-값 쌍을 읽어 오는 간단한 구성 공급자를 구현해봅니다.

먼저 데이터베이스에 구성 값을 저장하기 위한 ConfigurationValue 엔터티를 정의합니다:

public class ConfigurationValue
{
    public string Id { get; set; }
    public string Value { get; set; }
}

구성된 값을 저장하고 접근하기 위한 ConfigurationContext 클래스를 추가합니다:

public class ConfigurationContext : DbContext
{
    public ConfigurationContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<ConfigurationValue> Values { get; set; }
}

IConfigurationSource 인터페이스를 구현하는 클래스를 작성합니다:

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
    public class EFConfigSource : IConfigurationSource
    {
        private readonly Action<DbContextOptionsBuilder> _optionsAction;

        public EFConfigSource(Action<DbContextOptionsBuilder> optionsAction)
        {
            _optionsAction = optionsAction;
        }

        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new EFConfigProvider(_optionsAction);
        }
    }
}

ConfigurationProvider를 상속받는 사용자 지정 구성 공급자를 생성합니다. 이 구성 공급자는 데이터베이스가 비어있을 경우 데이터베이스를 초기화합니다:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
    public class EFConfigProvider : ConfigurationProvider
    {
        public EFConfigProvider(Action<DbContextOptionsBuilder> optionsAction)
        {
            OptionsAction = optionsAction;
        }

        Action<DbContextOptionsBuilder> OptionsAction { get; }

        // Load config data from EF DB.
        public override void Load()
        {
            var builder = new DbContextOptionsBuilder<ConfigurationContext>();
            OptionsAction(builder);

            using (var dbContext = new ConfigurationContext(builder.Options))
            {
                dbContext.Database.EnsureCreated();
                Data = !dbContext.Values.Any()
                    ? CreateAndSaveDefaultValues(dbContext)
                    : dbContext.Values.ToDictionary(c => c.Id, c => c.Value);
            }
        }

        private static IDictionary<string, string> CreateAndSaveDefaultValues(
            ConfigurationContext dbContext)
        {
            var configValues = new Dictionary<string, string>
                {
                    { "key1", "value_from_ef_1" },
                    { "key2", "value_from_ef_2" }
                };
            dbContext.Values.AddRange(configValues
                .Select(kvp => new ConfigurationValue { Id = kvp.Key, Value = kvp.Value })
                .ToArray());
            dbContext.SaveChanges();
            return configValues;
        }
    }
}

예제를 실행해보면 데이터베이스에서 가져온 값이 ("value_from_ef_1" and "value_from_ef_2") 출력됩니다.

계속해서 이번에는 구성 원본을 추가하기 위한 AddEntityFrameworkConfig 확장 메서드를 추가합니다:

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace CustomConfigurationProvider
{
    public static class EntityFrameworkExtensions
    {
        public static IConfigurationBuilder AddEntityFrameworkConfig(
            this IConfigurationBuilder builder, Action<DbContextOptionsBuilder> setup)
        {
            return builder.Add(new EFConfigSource(setup));
        }
    }
}

다음 코드는 사용자 지정 EFConfigProvider의 사용법을 보여줍니다:

using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using CustomConfigurationProvider;

public static class Program
{
    public static void Main()
    {
        var builder = new ConfigurationBuilder();
        builder.SetBasePath(Directory.GetCurrentDirectory());
        builder.AddJsonFile("appsettings.json");
        var connectionStringConfig = builder.Build();

        var config = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            // Add "appsettings.json" to bootstrap EF config.
            .AddJsonFile("appsettings.json")
            // Add the EF configuration provider, which will override any
            // config made with the JSON provider.
            .AddEntityFrameworkConfig(options =>
                options.UseSqlServer(connectionStringConfig.GetConnectionString("DefaultConnection"))
            )
            .Build();

        Console.WriteLine("key1={0}", config["key1"]);
        Console.WriteLine("key2={0}", config["key2"]);
        Console.WriteLine("key3={0}", config["key3"]);
    }
}

이번 예제에서 JSON 공급자 다음에 사용자 지정 EFConfigProvider를 추가하고 있다는 점에 유의하시기 바랍니다. 이로 인해서 데이터베이스로부터 가져온 모든 설정이 appsettings.json 파일의 설정을 덮어쓰게 됩니다.

다음과 같은 appsettings.json 파일을 사용할 경우:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CustomConfigurationProvider;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "key1": "value_from_json_1",
  "key2": "value_from_json_2",
  "key3": "value_from_json_3"
}

다음과 같은 결과가 출력됩니다:

key1=value_from_ef_1
key2=value_from_ef_2
key3=value_from_json_3

CommandLine 구성 공급자

다음 예제에서는 가장 마지막으로 CommandLine 구성 공급자를 활성화시키고 있습니다:

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;

// Add NuGet  <package id="Microsoft.Extensions.Configuration.Binder"
// Add NuGet  <package id="Microsoft.Extensions.Configuration.CommandLine"
public class Program
{
    public static IConfigurationRoot Configuration { get; set; }

    public static Dictionary<string, string> GetSwitchMappings(
        IReadOnlyDictionary<string, string> configurationStrings)
    {
        return configurationStrings.Select(item =>
            new KeyValuePair<string, string>(
                "-" + item.Key.Substring(item.Key.LastIndexOf(':') + 1), item.Key))
                .ToDictionary(item => item.Key, item => item.Value);
    }

    public static void Main(string[] args = null)
    {
        var dict = new Dictionary<string, string>
            {
                {"Profile:MachineName", "Rick"},
                {"App:MainWindow:Left", "11"}
            };

        var builder = new ConfigurationBuilder();
        builder.AddInMemoryCollection(dict).AddCommandLine(args, GetSwitchMappings(dict));
        Configuration = builder.Build();
        Console.WriteLine($"Hello {Configuration["Profile:MachineName"]}");

        // Set the default value to 80
        var left = Configuration.GetValue<int>("App:MainWindow:Left", 80);
        Console.WriteLine($"Left {left}");
    }
}

구성 설정을 전달하려면 다음 명령을 사용하십시오:

dotnet run /Profile:MachineName=Bob /App:MainWindow:Left=1234

그러면 다음과 같은 결과가 출력됩니다:

Hello Bob
Left 1234

GetSwitchMappings 메서드는 / 대신 -를 사용할 수 있게 해주고 선행하는 하위 키의 접두사를 생략할 수 있게 해줍니다. 예를 들어서 다음과 같은 명령을 사용하면:

dotnet run -MachineName=Bob -Left=7734

다음과 같은 결과가 출력됩니다:

Hello Bob
Left 7734

명령줄 인수에는 반드시 값이 포함되어 있어야합니다 (null일 수도 있습니다). 가령, 다음과 같은 명령은 유효합니다:

dotnet run /Profile:MachineName=

그러나 다음 명령은 예외를 발생시킵니다:

dotnet run /Profile:MachineName

대응하는 스위치 매핑이 존재하지 않는 - 또는 -- 명령줄 스위치 접두사를 지정하면 예외가 발생합니다.

web.config 파일

IIS 또는 IIS-Express에서 응용 프로그램을 호스팅 할 경우 web.config 파일이 필요합니다. web.config 파일의 설정을 통해서 ASP.NET Core 응용 프로그램의 시작에 필요한 IIS의 AspNetCoreModule을 활성화시키고, 그 밖의 IIS 설정 및 모듈을 구성합니다. Visual Studio를 사용할 경우, web.config 파일을 삭제하면 Visual Studio가 새로운 web.config 파일을 생성합니다.

추가 참고사항

  • 의존성 주입(DI, Dependency Injection)은 ConfigureServices 메서드가 호출되기 전까지는 설정되지 않습니다.
  • 구성 시스템은 DI를 인식하지 못합니다.
  • IConfiguration에는 두 가지 특화된 형식이 존재합니다:

    • IConfigurationRoot: 루트 노드에 사용됩니다. 다시 로딩하는 작업을 촉발시킬 수 있습니다.
    • IConfigurationSection: 구성 값 섹션을 나타냅니다. 가령 GetSection 메서드와 GetChildren 메서드는 IConfigurationSection을 반환합니다.

추가 자료