Bagikan melalui


Pola opsi di .NET

Pola opsi menggunakan kelas untuk menyediakan akses yang sangat diketik ke grup pengaturan terkait. Saat pengaturan konfigurasi diisolasi oleh skenario ke dalam kelas terpisah, aplikasi mematuhi dua prinsip rekayasa perangkat lunak penting:

Opsi juga menyediakan mekanisme untuk memvalidasi data konfigurasi. Untuk informasi selengkapnya, lihat bagian Validasi opsi.

Mengikat konfigurasi hierarkis

Cara yang lebih disukai untuk membaca nilai konfigurasi terkait adalah menggunakan pola opsi. Pola opsi dimungkinkan IOptions<TOptions> melalui antarmuka, di mana parameter TOptions jenis generik dibatasi ke class. Nantinya IOptions<TOptions> dapat disediakan melalui injeksi dependensi. Untuk informasi selengkapnya, lihat Injeksi dependensi.

Misalnya, untuk membaca nilai konfigurasi yang disorot dari file appsettings.json :

{
    "SecretKey": "Secret key value",
    "TransientFaultHandlingOptions": {
        "Enabled": true,
        "AutoRetryDelay": "00:00:07"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    }
}

Buat kelas TransientFaultHandlingOptions berikut:

public sealed class TransientFaultHandlingOptions
{
    public bool Enabled { get; set; }
    public TimeSpan AutoRetryDelay { get; set; }
}

Saat menggunakan pola opsi, kelas opsi:

  • Harus non-abstrak dengan konstruktor tanpa parameter publik
  • Berisi properti baca-tulis publik untuk mengikat (bidang tidak terikat)

Kode berikut adalah bagian dari file C# Program.cs dan:

  • Memanggil ConfigurationBinder.Bind untuk mengikat kelas TransientFaultHandlingOptions ke bagian "TransientFaultHandlingOptions".
  • Menampilkan data konfigurasi .
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Configuration.Sources.Clear();

IHostEnvironment env = builder.Environment;

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

TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
    .Bind(options);

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

// <Output>
// Sample output:

Dalam kode sebelumnya, file konfigurasi JSON memiliki bagian yang "TransientFaultHandlingOptions" terikat ke TransientFaultHandlingOptions instans. Ini menghidrasi properti objek C# dengan nilai yang sesuai dari konfigurasi.

ConfigurationBinder.Get<T> mengikat dan mengembalikan jenis yang ditentukan. ConfigurationBinder.Get<T> mungkin lebih nyaman daripada menggunakan ConfigurationBinder.Bind. Contoh kode berikut menunjukkan cara menggunakan ConfigurationBinder.Get<T> dengan kelas TransientFaultHandlingOptions:

var options =
    builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
        .Get<TransientFaultHandlingOptions>();

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

Dalam kode sebelumnya, ConfigurationBinder.Get<T> digunakan untuk memperoleh instans TransientFaultHandlingOptions objek dengan nilai propertinya yang diisi dari konfigurasi yang mendasarinya.

Penting

Kelas ConfigurationBinder mengekspos beberapa API, seperti .Bind(object instance) dan .Get<T>() yang tidak dibatasi ke class. Saat menggunakan salah satu antarmuka Opsi, Anda harus mematuhi batasan kelas opsi yang disebutkan di atas.

Pendekatan alternatif saat menggunakan pola opsi adalah mengikat bagian "TransientFaultHandlingOptions" dan menambahkannya ke kontainer layanan injeksi dependensi. Dalam kode berikut, TransientFaultHandlingOptions ditambahkan ke kontainer layanan dengan Configure dan terikat ke konfigurasi:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure<TransientFaultHandlingOptions>(
    builder.Configuration.GetSection(
        key: nameof(TransientFaultHandlingOptions)));

Dalam builder contoh sebelumnya adalah instans .HostApplicationBuilder

Tip

Parameter key adalah nama bagian konfigurasi untuk dicari. Ini tidak harus cocok dengan nama jenis yang mewakilinya. Misalnya, Anda dapat memiliki bagian bernama "FaultHandling" dan dapat diwakili oleh TransientFaultHandlingOptions kelas. Dalam hal ini, Anda akan meneruskan "FaultHandling" ke fungsi sebagai gantinya GetSection . Operator nameof digunakan sebagai kenyamanan ketika bagian bernama cocok dengan jenis yang sesuai dengannya.

Dengan menggunakan kode sebelumnya, kode berikut membaca opsi posisi:

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

Dalam kode sebelumnya, perubahan pada file konfigurasi JSON setelah aplikasi dimulai tidak dibaca. Untuk membaca perubahan setelah aplikasi dimulai, gunakan IOptionsSnapshot atau IOptionsMonitor untuk memantau perubahan saat terjadi, dan bereaksi sesuai.

Antarmuka opsi

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

IOptionsFactory<TOptions> bertanggung jawab untuk membuat instans opsi baru. Ini memiliki satu Create metode. Implementasi default mengambil semua yang terdaftar IConfigureOptions<TOptions> dan IPostConfigureOptions<TOptions> dan menjalankan semua konfigurasi terlebih dahulu, diikuti oleh pasca-konfigurasi. Ini membedakan antara IConfigureNamedOptions<TOptions> dan IConfigureOptions<TOptions> dan hanya memanggil antarmuka yang sesuai.

IOptionsMonitorCache<TOptions> digunakan oleh IOptionsMonitor<TOptions> untuk menyimpan TOptions instans. IOptionsMonitorCache<TOptions> Instans opsi yang tidak valid di monitor sehingga nilai dikomputasi ulang (TryRemove). Nilai dapat diperkenalkan secara manual dengan TryAdd. Metode Clear ini digunakan ketika semua instans bernama harus dibuat ulang sesuai permintaan.

IOptionsChangeTokenSource<TOptions> digunakan untuk mengambil IChangeToken yang melacak perubahan pada instans yang mendasar TOptions . Untuk informasi selengkapnya tentang primitif token perubahan, lihat Mengubah pemberitahuan.

Manfaat antarmuka opsi

Menggunakan jenis pembungkus generik memberi Anda kemampuan untuk memisahkan masa pakai opsi dari kontainer DI. Antarmuka IOptions<TOptions>.Value menyediakan lapisan abstraksi, termasuk batasan generik, pada jenis opsi Anda. Ini memberikan manfaat berikut:

  • Evaluasi T instans konfigurasi ditangguhkan untuk mengakses IOptions<TOptions>.Value, daripada ketika disuntikkan. Ini penting karena Anda dapat menggunakan T opsi dari berbagai tempat dan memilih semantik seumur hidup tanpa mengubah apa pun tentang T.
  • Saat mendaftarkan opsi jenis T, Anda tidak perlu mendaftarkan T jenis secara eksplisit. Ini adalah kenyamanan ketika Anda menulis pustaka dengan default sederhana, dan Anda tidak ingin memaksa pemanggil untuk mendaftarkan opsi ke dalam kontainer DI dengan masa pakai tertentu.
  • Dari perspektif API, api memungkinkan batasan pada jenis T (dalam hal ini, T dibatasi ke jenis referensi).

Menggunakan IOptionsSnapshot untuk membaca data yang diperbarui

Saat Anda menggunakan IOptionsSnapshot<TOptions>, opsi dihitung sekali per permintaan saat diakses dan di-cache selama masa pakai permintaan. Perubahan pada konfigurasi dibaca setelah aplikasi dimulai saat menggunakan penyedia konfigurasi yang mendukung pembacaan nilai konfigurasi yang diperbarui.

Perbedaan antara IOptionsMonitor dan IOptionsSnapshot adalah bahwa:

  • IOptionsMonitor adalah layanan singleton yang mengambil nilai opsi saat ini kapan saja, yang sangat berguna dalam dependensi singleton.
  • IOptionsSnapshot adalah layanan terlingkup dan menyediakan rekam jepret opsi pada saat IOptionsSnapshot<T> objek dibangun. Rekam jepret opsi dirancang untuk digunakan dengan dependensi sementara dan terlingkup.

Kode berikut menggunakan IOptionsSnapshot<TOptions>.

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ScopedService(IOptionsSnapshot<TransientFaultHandlingOptions> options)
{
    private readonly TransientFaultHandlingOptions _options = options.Value;

    public void DisplayValues()
    {
        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
    }
}

Kode berikut mendaftarkan instans konfigurasi yang TransientFaultHandlingOptions mengikat terhadap:

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

Dalam kode sebelumnya, Configure<TOptions> metode ini digunakan untuk mendaftarkan instans konfigurasi yang TOptions akan mengikat, dan memperbarui opsi saat konfigurasi berubah.

IOptionsMonitor

Untuk menggunakan pemantau opsi, objek opsi dikonfigurasi dengan cara yang sama dari bagian konfigurasi.

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

Contoh berikut menggunakan IOptionsMonitor<TOptions>:

using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
    public void DisplayValues()
    {
        TransientFaultHandlingOptions options = monitor.CurrentValue;

        Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
        Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
    }
}

Dalam kode sebelumnya, perubahan pada file konfigurasi JSON setelah aplikasi dimulai dibaca.

Tip

Beberapa sistem file, seperti kontainer Docker dan berbagi jaringan, mungkin tidak mengirim pemberitahuan perubahan dengan andal. Saat menggunakan IOptionsMonitor<TOptions> antarmuka di lingkungan ini, atur DOTNET_USE_POLLING_FILE_WATCHER variabel lingkungan ke 1 atau true untuk melakukan polling sistem file untuk perubahan. Interval di mana perubahan dijajaki adalah setiap empat detik dan tidak dapat dikonfigurasi.

Untuk informasi selengkapnya tentang kontainer Docker, lihat Membuat kontainer aplikasi .NET.

Dukungan opsi bernama menggunakan IConfigureNamedOptions

Opsi bernama:

  • Berguna saat beberapa bagian konfigurasi mengikat properti yang sama.
  • Peka huruf besar/kecil.

Pertimbangkan file appsettings.json berikut:

{
  "Features": {
    "Personalize": {
      "Enabled": true,
      "ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
    },
    "WeatherStation": {
      "Enabled": true,
      "ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
    }
  }
}

Daripada membuat dua kelas untuk mengikat Features:Personalize dan Features:WeatherStation, kelas berikut digunakan untuk setiap bagian:

public class Features
{
    public const string Personalize = nameof(Personalize);
    public const string WeatherStation = nameof(WeatherStation);

    public bool Enabled { get; set; }
    public string ApiKey { get; set; }
}

Kode berikut mengonfigurasi opsi bernama:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<Features>(
    Features.Personalize,
    builder.Configuration.GetSection("Features:Personalize"));

builder.Services.Configure<Features>(
    Features.WeatherStation,
    builder.Configuration.GetSection("Features:WeatherStation"));

Kode berikut menampilkan opsi bernama:

public sealed class Service
{
    private readonly Features _personalizeFeature;
    private readonly Features _weatherStationFeature;

    public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
    {
        _personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
        _weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
    }
}

Semua opsi diberi nama instans. IConfigureOptions<TOptions> instans diperlakukan sebagai menargetkan Options.DefaultName instans, yaitu string.Empty. IConfigureNamedOptions<TOptions> juga mengimplementasikan IConfigureOptions<TOptions>. Implementasi default dari IOptionsFactory<TOptions> memiliki logika untuk menggunakan masing-masing dengan tepat. null Opsi bernama digunakan untuk menargetkan semua instans bernama alih-alih instans bernama tertentu. ConfigureAll dan PostConfigureAll gunakan konvensi ini.

OptionsBuilder API

OptionsBuilder<TOptions> digunakan untuk mengonfigurasi TOptions instans. OptionsBuilder menyederhanakan pembuatan opsi bernama karena hanya satu parameter ke panggilan awal AddOptions<TOptions>(string optionsName) alih-alih muncul di semua panggilan berikutnya. Validasi opsi dan ConfigureOptions kelebihan beban yang menerima dependensi layanan hanya tersedia melalui OptionsBuilder.

OptionsBuilder digunakan di bagian Validasi opsi.

Menggunakan layanan DI untuk mengonfigurasi opsi

Layanan dapat diakses dari injeksi dependensi sambil mengonfigurasi opsi dengan dua cara:

  • Teruskan delegasi konfigurasi untuk Mengonfigurasi pada OptionsBuilder<TOptions>. OptionsBuilder<TOptions> menyediakan kelebihan beban Konfigurasi yang memungkinkan penggunaan hingga lima layanan untuk mengonfigurasi opsi:

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
    
  • Buat jenis yang mengimplementasikan IConfigureOptions<TOptions> atau IConfigureNamedOptions<TOptions> dan mendaftarkan jenis sebagai layanan.

Sebaiknya teruskan delegasi konfigurasi ke Konfigurasi, karena membuat layanan lebih kompleks. Membuat jenis setara dengan apa yang dilakukan kerangka kerja saat memanggil Konfigurasikan. Memanggil Konfigurasi mendaftarkan generik IConfigureNamedOptions<TOptions>sementara , yang memiliki konstruktor yang menerima jenis layanan generik yang ditentukan.

Validasi opsi

Validasi opsi memungkinkan nilai opsi divalidasi.

Pertimbangkan file appsettings.json berikut:

{
  "MyCustomSettingsSection": {
    "SiteTitle": "Amazing docs from Awesome people!",
    "Scale": 10,
    "VerbosityLevel": 32
  }
}

Kelas berikut mengikat ke bagian "MyCustomSettingsSection" konfigurasi dan menerapkan beberapa DataAnnotations aturan:

using System.ComponentModel.DataAnnotations;

namespace ConsoleJson.Example;

public sealed class SettingsOptions
{
    public const string ConfigurationSectionName = "MyCustomSettingsSection";

    [Required]
    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public required string SiteTitle { get; set; }

    [Required]
    [Range(0, 1_000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public required int Scale { get; set; }

    [Required]
    public required int VerbosityLevel { get; set; }
}

Di kelas sebelumnya SettingsOptions , ConfigurationSectionName properti berisi nama bagian konfigurasi yang akan diikat. Dalam skenario ini, objek opsi menyediakan nama bagian konfigurasinya.

Tip

Nama bagian konfigurasi tidak bergantung pada objek konfigurasi yang mengikatnya. Dengan kata lain, bagian konfigurasi bernama "FooBarOptions" dapat terikat ke objek opsi bernama ZedOptions. Meskipun mungkin umum untuk memberi nama yang sama, itu tidak perlu dan benar-benar dapat menyebabkan konflik nama.

Kode berikut:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations();

Metode ValidateDataAnnotations ekstensi ditentukan dalam paket NuGet Microsoft.Extensions.Options.DataAnnotations .

Kode berikut menampilkan nilai konfigurasi atau melaporkan kesalahan validasi:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

public sealed class ValidationService
{
    private readonly ILogger<ValidationService> _logger;
    private readonly IOptions<SettingsOptions> _config;

    public ValidationService(
        ILogger<ValidationService> logger,
        IOptions<SettingsOptions> config)
    {
        _config = config;
        _logger = logger;

        try
        {
            SettingsOptions options = _config.Value;
        }
        catch (OptionsValidationException ex)
        {
            foreach (string failure in ex.Failures)
            {
                _logger.LogError("Validation error: {FailureMessage}", failure);
            }
        }
    }
}

Kode berikut menerapkan aturan validasi yang lebih kompleks menggunakan delegasi:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

Validasi terjadi pada waktu proses, tetapi Anda dapat mengonfigurasinya agar terjadi saat startup dengan menautkan panggilan ke ValidateOnStart:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.")
    .ValidateOnStart();

Dimulai dengan .NET 8, Anda dapat menggunakan API alternatif, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String), yang memungkinkan validasi pada awal untuk jenis opsi tertentu:

builder.Services
    .AddOptionsWithValidateOnStart<SettingsOptions>()
    .Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

IValidateOptions untuk validasi kompleks

Kelas berikut mengimplementasikan IValidateOptions<TOptions>:

using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace ConsoleJson.Example;

sealed partial class ValidateSettingsOptions(
    IConfiguration config)
    : IValidateOptions<SettingsOptions>
{
    public SettingsOptions? Settings { get; private set; } =
        config.GetSection(SettingsOptions.ConfigurationSectionName)
              .Get<SettingsOptions>();

    public ValidateOptionsResult Validate(string? name, SettingsOptions options)
    {
        StringBuilder? failure = null;
    
        if (!ValidationRegex().IsMatch(options.SiteTitle))
        {
            (failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
        }

        if (options.Scale is < 0 or > 1_000)
        {
            (failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
        }

        if (Settings is { Scale: 0 } && Settings.VerbosityLevel <= Settings.Scale)
        {
            (failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
        }

        return failure is not null
            ? ValidateOptionsResult.Fail(failure.ToString())
            : ValidateOptionsResult.Success;
    }

    [GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
    private static partial Regex ValidationRegex();
}

IValidateOptions memungkinkan pemindahan kode validasi ke dalam kelas.

Catatan

Contoh kode ini bergantung pada paket NuGet Microsoft.Extensions.Configuration.Json .

Menggunakan kode sebelumnya, validasi diaktifkan saat mengonfigurasi layanan dengan kode berikut:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<SettingsOptions>(
    builder.Configuration.GetSection(
        SettingsOptions.ConfigurationSectionName));

builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton
        <IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());

Opsi pasca-konfigurasi

Atur pasca-konfigurasi dengan IPostConfigureOptions<TOptions>. Pasca-konfigurasi berjalan setelah semua IConfigureOptions<TOptions> konfigurasi terjadi, dan dapat berguna dalam skenario saat Anda perlu mengambil alih konfigurasi:

builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

PostConfigure tersedia untuk pasca-konfigurasi opsi bernama:

builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Gunakan PostConfigureAll untuk mengonfigurasi semua instans konfigurasi pasca-konfigurasi:

builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Lihat juga