Quản lý cấu hình trong ASP.NET Core với IConfiguration và Options Pattern

Trong ASP.NET Core, việc quản lý cấu hình là một phần cực kỳ quan trọng để ứng dụng hoạt động ổn định và linh hoạt. Trong bài này, chúng ta sẽ tìm hiểu cách truy cập, binding và tổ chức cấu hình bằng IConfiguration, Options Pattern, cũng như các công cụ nâng cao như IOptionsMonitor hay Custom Configuration Providers.

Cách tiếp cận cơ bản với IConfiguration

Cài đặt package: Microsoft.Extensions.Configuration

IConfiguration là interface để truy cập cấu hình từ nhiều nguồn khác nhau như appsettings.json, enviroment variables hoặc command-line arguments.

Cách sử dụng

// Lấy giá trị trực tiếp với key cụ thể
var value = _configuration.GetValue<bool>("section1:section2:key");

// Lấy qua section
var section = _configuration.GetSection("section1:section2");
var value = section.GetValue<bool>("key");

Hạn chế của IConfiguration:

  • Cần gõ lại key và section nhiều lần, dễ bị lỗi typo.
  • Không có kiểm tra tại thời điểm biên dịch.
  • Code dễ bị vỡ nếu cấu trúc JSON thay đổi.

Binding Configuration - Ánh xạ vào đối tượng

Để khắc phục hạn chế của IConfiguration, ta có thể ánh xạ cấu hình vào các đối tượng strongly-typed

public class FeatureOptions
{
    public bool EnableGreeting { get; set; }
    public string GreetingContent { get; set; }
}

var featureOptions = new FeatureOptions();
// Ánh xạ cấu hình từ section "Features:HomeEndpoint" vào đối tượng
_configuration.Bind("Features:HomeEndpoint", featureOptions);

Ưu điểm:

  • Giảm lỗi khi thao tác trực tiếp với key.
  • Dễ quản lý và tái sử dụng.

Options Pattern

Options Pattern là phương pháp sử dụng các lớp strongly-typed để quản lý cấu hình. Nó được khuyến nghị cho các ứng dụng ASP.NET Core, đặc biệt khi sử dụng Dependency Injection.

Điều kiện cần thiết:

  • Lớp cấu hình phải là lớp cụ thể (concrete class).
  • Lớp cấu hình phải có constructor không tham số (parameterless constructor).
  • Các thuộc tính của lớp phải có setter công khai (public setter).

Cài đặt package: Microsoft.Extensions.Options

Tạo lớp cấu hình

public class FeatureConfiguration
{
    public bool EnableGreeting { get; set; }
    public string GreetingContent { get; set; }
}

Đăng ký với DI Container

builder.Services.Configure<FeatureConfiguration>(
    _configuration.GetSection("Features:HomeEndpoint")
);

Sử dụng trong Controller

[ApiController]
[Route("/api/[controller]")]
public class HomeController : ControllerBase
{
    private readonly FeatureConfiguration _featureConfiguration;

    // Inject IOptions<T> để lấy giá trị cấu hình
    public HomeController(IOptions<FeatureConfiguration> options)
    {
        _featureConfiguration = options.Value;
    }
}

Ưu điểm của Options Pattern:

  • Tổ chức cấu hình theo module rõ ràng.
  • Strongly-typed, giảm lỗi runtime.
  • Hỗ trợ tốt cho DI, dễ tái sử dụng trong nhiều module.

Tổng kết các cách tiếp cận

Phương pháp Ưu điểm Hạn chế
IConfiguration - Đơn giản, dễ sử dụng - Code lặp lại
- Dễ vỡ khi thay đổi cấu hình
Binding Configuration - Truy cập dữ liệu dễ dàng hơn với đối tượng mạnh kiểu - Phức tạp hơn khi cấu hình nhiều lớp
- Dễ quản lý và ít lỗi sai hơn
Options Pattern - Mạnh kiểu, hỗ trợ tốt cho DI - Yêu cầu cài đặt thêm package
- Tổ chức tốt cấu hình, dễ tái sử dụng - Cấu hình phức tạp hơn

Lưu ý: Options Pattern phù hợp khi cấu hình lớn hoặc cần tái sử dụng trong nhiều module khác nhau.


IOptions, IOptionsSnapshot, và IOptionsMonitor

Tính năng IOptions IOptionsSnapshot IOptionsMonitor
Hỗ trợ reload cấu hình ❌ Không hỗ trợ ✔ Hỗ trợ ✔ Hỗ trợ
Đăng ký trong DI container Singleton Scoped Singleton
Thời điểm giá trị được tải Khi sử dụng lần đầu tiên Mỗi request Ngay lập tức
Sử dụng trong mọi lifetime ✔ Có thể dùng ❌ Không dùng được trong singleton ✔ Có thể dùng
Hỗ trợ named options ❌ Không hỗ trợ ✔ Hỗ trợ ✔ Hỗ trợ

IOptions

  • Đơn giản và hiệu quả khi cấu hình không cần thay đổi sau khi ứng dụng khởi chạy.
  • Sử dụng tốt cho cấu hình tĩnh hoặc khi các giá trị không cần reload.

IOptionsSnapshot

  • Phù hợp với ứng dụng web, nơi mỗi request cần tải lại cấu hình.
  • Hạn chế: không dùng được trong các service có lifetime singleton.

IOptionsMonitor

  • Lựa chọn linh hoạt nhất, phù hợp khi cần reload cấu hình ngay lập tức trong mọi lifetime.
  • Hỗ trợ đăng ký callback để xử lý khi cấu hình thay đổi.

Ví dụ sử dụng IOptionsMonitor

public class SomeController
{
    private readonly IOptionsMonitor<FeatureConfiguration> _options;
    private FeatureConfiguration _featureConfiguration;

    public SomeController(IOptionsMonitor<FeatureConfiguration> options)
    {
        _options = options;

        // Lấy giá trị hiện tại
        _featureConfiguration = _options.CurrentValue;

        // Đăng ký callback khi cấu hình thay đổi
        _options.OnChange(
            config =>
            {
                _featureConfiguration = config;

                // Ghi log khi cấu hình thay đổi
                Console.WriteLine("Configuration changed!");
            }
        );
    }
}

Named Options

Sử dụng cùng một lớp Options với các phần cấu hình khác nhau.

builder.Services.Configure<MyConfiguration>(
    "OptionName", 
    _configuration.GetSection("Features:HomeEndpoint")
);

public SomeController(IOptionsMonitor<MyConfiguration> options)
{
    var config = options.Get("OptionName");
}

Options Validation

Data Annotations

Sử dụng các chú thích dữ liệu (data annotations) để xác thực các thuộc tính của lớp cấu hình.

public class FeatureConfiguration
{
    [Required] // Đảm bảo giá trị không null
    public bool EnableGreeting { get; set; }

    [Required] // Đảm bảo giá trị không null
    public string GreetingContent { get; set; }
}

builder.Services.AddOptions<FeatureConfiguration>()
    .Bind(builder.Configuration.GetSection("SectionString"))
    .ValidateDataAnnotations() // Xác thực theo các chú thích dữ liệu
    .ValidateOnStart(); // Xác thực khi Option được sử dụng lần đầu

Advanced Validation

Ngoài data annotations, bạn cũng có thể xác thực với logic tùy chỉnh.

builder.Services.AddOptions<FeatureConfiguration>()
    .Bind(builder.Configuration.GetSection("SectionString"))
    .Validate(c => 
    {
        if (c.EnableGreeting && string.IsNullOrEmpty(c.GreetingContent))
        {
            return false; // Nếu EnableGreeting là true mà GreetingContent rỗng, xác thực thất bại
        }
        return true;
    }, "Some error messages.")
    .ValidateOnStart(); // Xác thực khi Option được sử dụng lần đầu

Tạo lớp xác thực tùy chỉnh để kiểm tra điều kiện phức tạp hơn.

public class FeatureConfigurationValidator : IValidateOptions<FeatureConfiguration>
{
    public ValidateOptionsResult Validate(string name, FeatureConfiguration options)
    {
        if (options.EnableGreeting && string.IsNullOrEmpty(options.GreetingContent))
        {
            return ValidateOptionsResult.Fail("Greeting content is required.");
        }

        return ValidateOptionsResult.Success;
    }
}

builder.Services.AddOptions<FeatureConfiguration>("OptionName")
    .Bind(builder.Configuration.GetSection("SectionString"))
    .ValidateOnStart();

builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton<IValidateOptions<FeatureConfiguration>, FeatureConfigurationValidator>()
);

Named Options Validation

Đối với các tùy chọn có tên (named options), bạn có thể xác thực dựa trên tên của từng cấu hình.

public class FeatureConfigurationValidator : IValidateOptions<FeatureConfiguration>
{
    public ValidateOptionsResult Validate(string name, FeatureConfiguration options)
    {
        switch (name)
        {
            case "Option1":
                if (options.EnableGreeting && string.IsNullOrEmpty(options.GreetingContent))
                {
                    return ValidateOptionsResult.Fail("Greeting content is required.");
                }
                break;
            case "Option2":
                if (options.EnableGreeting && string.IsNullOrEmpty(options.GreetingContent))
                {
                    return ValidateOptionsResult.Fail("Greeting content is required.");
                }
                break;
            default:
                return ValidateOptionsResult.Skip; // Nếu không thuộc "Option1" hay "Option2", bỏ qua
        }

        return ValidateOptionsResult.Success;
    }
}

Sử dụng Interface

Khi bạn sử dụng các interface với IOptions, bạn có thể tách biệt các lớp cấu hình và dịch vụ của mình, tạo ra sự linh hoạt trong việc quản lý cấu hình và dễ dàng tiêm phụ thuộc.

public class SomeServiceConfiguration : ISomeServiceConfiguration
{
    // Các thuộc tính ở đây
}

public class SomeController
{
    private readonly ISomeServiceConfiguration _configuration;

    public SomeController(ISomeServiceConfiguration configuration)
    {
        _configuration = configuration;
    }
}

builder.Services.Configure<SomeServiceConfiguration>(_configuration.GetSection("SectionString"));

builder.Services.AddSingleton<ISomeServiceConfiguration>(sp => 
{
    return sp.GetRequiredService<IOptions<SomeServiceConfiguration>>().Value;
}); 

Unit Testing

Khi viết kiểm thử cho các lớp sử dụng IOptions, bạn có thể sử dụng các công cụ như Options.Create() và thư viện mock như Moq.

Options.Create()

Options.Create() là cách nhanh chóng để tạo ra một đối tượng IOptions trong kiểm thử.

var options = Options.Create(new FeatureConfiguration
{
    EnableGreeting = true,
    GreetingContent = "Hello"
});

Moq

Sử dụng thư viện Moq để tạo mock cho IOptions. Điều này cho phép bạn giả lập đối tượng IOptions trong các bài kiểm thử.

var mockOptions = new Mock<IOptions<FeatureConfiguration>>();
mockOptions.SetupGet(x => x.Value).Returns(new FeatureConfiguration
{
    EnableGreeting = true,
    GreetingContent = "Hello"
});

Configuration Providers

Configuration Providers là các nguồn thông tin cấu hình mà ứng dụng có thể sử dụng để cấu hình các dịch vụ của mình. Các nguồn cấu hình bao gồm:

  • JSON (Ví dụ: appsettings.json)
  • Environment Variables (Biến môi trường)
  • Command Line Arguments (Các tham số dòng lệnh)
  • Configuration Secrets (Bí mật cấu hình)
    • User Secrets (Dành cho phát triển)
    • Azure Key Vault (Dành cho sản xuất)
  • Cloud Services (Dịch vụ đám mây như AWS Parameter Store)
  • Custom Configuration Providers (Các nhà cung cấp cấu hình tùy chỉnh)

Khi host của ứng dụng được xây dựng, cấu hình ban đầu của ứng dụng được tải từ các biến môi trường. Trong quá trình xây dựng host, ứng dụng sẽ tải cấu hình.

Các nguồn cấu hình được thêm vào theo thứ tự. Các nguồn cấu hình sau sẽ ghi đè các giá trị của các mục cấu hình đã được thêm bởi các nguồn trước đó.


JSON Configuration

var configBuilder = new ConfigurationBuilder();
configBuilder.SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

IConfiguration config = configBuilder.Build();

builder.ConfigureAppConfiguration(
    (hostingContext, config) =>
    {
        var env = hostingContext.HostingEnvironment;

        config
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile(
                $"appsettings.{env.EnvironmentName}.json",
                optional: true,
                reloadOnChange: true
            );

        if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
        {
            var assembly = Assembly.Load(new AssemblyName(env.ApplicationName));
            if (assembly != null)
            {
                config.AddUserSecrets(assembly, optional: true);
            }
        }

        config.AddEnvironmentVariables();

        if (args != null)
        {
            config.AddCommandLine(args);
        }
    }
);

Environment Variables

// Lệnh để thiết lập biến môi trường
[Environment]::SetEnvironmentVariable("AAA__MyKey", "MyValue", "User") 

dotnet run --AAA:MyKey "MyValue"
dotnet run AAA:MyKey="MyValue"

// launchSettings.json

"profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5239",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "AAA__MyKey": "MyValue"
      }
    }
}

User Secrets

dotnet user-secrets set "Section:Key" "Value"
dotnet user-secrets list

Secure Secrets trong môi trường Production

if (builder.Environment.IsProduction())
{
    builder.Services.AddAzureKeyVault(
        new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net"),
        new DefaultAzureCredential()
    );
}

AWS Parameter Store

Cài đặt gói Amazon.Extensions.Configuration.SystemsManager để sử dụng AWS Parameter Store.

builder.Configuration.AddSystemsManager(
    configure =>
    {
        configure.Path = "";
        configure.ReloadAfter = TimeSpan.FromMinutes(5);
        configure.Optional = true;
    }
);

Custom the Order of Configuration Providers

builder.ConfigureAppConfiguration(
    (hostingContext, config) =>
    {
        config.Sources.Clear();

        var env = hostingContext.HostingEnvironment;

        config.AddEnvironmentVariables("ASPNETCORE_");

        config
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile(
                $"appsettings.{env.EnvironmentName}.json",
                optional: true,
                reloadOnChange: true
            );

        if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
        {
            var assembly = Assembly.Load(new AssemblyName(env.ApplicationName));
            if (assembly != null)
            {
                config.AddUserSecrets(assembly, optional: true);
            }
        }

        if (args != null)
        {
            config.AddCommandLine(args);
        }

        config.AddEnvironmentVariables();
    }
);

Custom Configuration Providers

Nếu bạn muốn sử dụng một nhà cung cấp cấu hình tùy chỉnh, bạn có thể tạo ra một ConfigurationProvider để lấy cấu hình từ cơ sở dữ liệu hoặc các nguồn dữ liệu khác.

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

    public override void Load()
    {
        var builder = new DbContextOptionsBuilder<AppDbContext>();
        OptionsAction(builder);

        using (var dbContext = new AppDbContext(builder.Options))
        {
            dbContext.Database.EnsureCreated();

            Data = dbContext.ConfigurationEntries().Any()
                ? dbContext
                  .ConfigurationEntries()
                  .ToDictionary(e => e.Key, e => e.Value, StringComparer.OrdinalIgnoreCase)
                : new Dictionary<string, string>();
        }
    }
}

public class EntityFrameworkConfigurationSource : IConfigurationSource
{
    private readonly Action<DbContextOptionsBuilder> _optionsAction;

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

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

public static class EntityFrameworkConfigurationExtensions
{
    public static IConfigurationBuilder AddEntityFrameworkConfiguration(
        this IConfigurationBuilder builder,
        Action<DbContextOptionsBuilder> optionsAction
    )
    {
        return builder.Add(new EntityFrameworkConfigurationSource(optionsAction));
    }
}

Debugging Configuration

// Lấy thông tin cấu hình từ builder
var debugView = builder.Configuration.GetDebugView();

  • builder.Configuration là đối tượng chứa tất cả các cấu hình của ứng dụng.
  • Phương thức GetDebugView() trả về một chuỗi mô tả cấu hình của ứng dụng dưới dạng văn bản, rất hữu ích trong việc gỡ lỗi.
  • Bạn có thể sử dụng debugView để kiểm tra tất cả các giá trị cấu hình hiện tại của ứng dụng, giúp bạn dễ dàng nhận diện các vấn đề hoặc xác minh các thiết lập cấu hình.

Bình luận

Gợi ý

0%