Modèle d’options dans .NET

Le modèle d’options utilise des classes pour fournir un accès fortement typé aux groupes de paramètres associés. Quand les paramètres de configuration sont isolés par scénario dans des classes distinctes, l’application est conforme à deux principes d’ingénierie logicielle importants :

Ces options fournissent également un mécanisme de validation des données de configuration. Pour plus d'informations, reportez-vous à la section Validation des options.

Lier une configuration hiérarchique

La meilleure méthode pour lire les valeurs de configuration associées consiste à utiliser le modèle d’options. Le modèle d’options est possible via l’interface IOptions<TOptions>, où le paramètre TOptions de type générique est limité à un class. Le IOptions<TOptions> peut être fourni ultérieurement via l’injection de dépendances. Pour plus d’informations, consultez Injection de dépendances dans .NET.

Par exemple, pour lire les valeurs de configuration mises en surbrillance à partir d’un fichier appsettings.json :

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

Créez la classe TransientFaultHandlingOptions suivante :

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

Lorsque vous utilisez le modèle d’options, une classe d’options :

  • elle doit être non abstraite avec un constructeur public sans paramètre
  • Contenir des propriétés publiques en lecture-écriture à lier (les champs ne sont pas liés)

Le code suivant fait partie du fichier C# Program.cs et :

  • Appelle ConfigurationBinder.Bind pour lier la classe TransientFaultHandlingOptions à la section "TransientFaultHandlingOptions".
  • Affiche les données de configuration.
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:

Dans le code précédent, le fichier de configuration JSON a sa section "TransientFaultHandlingOptions" liée à l’instance TransientFaultHandlingOptions. Cela hydrate les propriétés des objets C# avec les valeurs correspondantes de la configuration.

ConfigurationBinder.Get<T> lie et retourne le type spécifié. Il peut être plus pratique d’utiliser ConfigurationBinder.Get<T> que ConfigurationBinder.Bind. Le code suivant illustre la classe ConfigurationBinder.Get<T> avec la classe TransientFaultHandlingOptions :

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

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

Dans le code précédent, le ConfigurationBinder.Get<T> est utilisé pour acquérir une instance de l’objet TransientFaultHandlingOptions avec ses valeurs de propriété remplies à partir de la configuration sous-jacente.

Important

La classe ConfigurationBinder expose plusieurs API, telles que .Bind(object instance) et .Get<T>() qui ne sont pas limitées à class. Lorsque vous utilisez l’une des Options d’interface, vous devez respecter les contraintes de classe d’options mentionnées ci-dessus.

Une autre approche lors de l’utilisation du modèle d’options consiste à lier la section "TransientFaultHandlingOptions" et à l’ajouter au conteneur de service d’injection de dépendances. Dans le code suivant, TransientFaultHandlingOptions est ajouté au conteneur de service avec Configure et lié à la configuration :

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

Dans l’exemple précédent, builder représente une instance de HostApplicationBuilder.

Conseil

Le paramètre key est le nom de la section de configuration à rechercher. Il n’est pas obligé de correspondre au nom du type qui le représente. Par exemple, vous pouvez avoir une section nommée "FaultHandling" et elle peut être représentée par la classe TransientFaultHandlingOptions. Dans cette instance, vous passez "FaultHandling" à la fonction GetSection à la place. L’opérateur nameof est utilisé à des fins pratiques lorsque la section nommée correspond au type auquel elle correspond.

À l’aide du code précédent, le code suivant lit les options de position :

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}");
    }
}

Dans le code précédent, les modifications apportées au fichier de configuration JSON après le démarrage de l’application ne sont pas lues. Pour lire les modifications après le démarrage de l’application, utilisez IOptionsSnapshot ou IOptionsMonitor pour surveiller les modifications à mesure qu’elles se produisent et réagir en conséquence.

Interfaces d’options

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

IOptionsFactory<TOptions> est chargée de créer les instances d’options. Elle dispose d’une seule méthode Create. L’implémentation par défaut prend toutes les IConfigureOptions<TOptions> et IPostConfigureOptions<TOptions> inscrites et exécute toutes les configurations, puis les post-configurations. Elle fait la distinction entre IConfigureNamedOptions<TOptions> et IConfigureOptions<TOptions> et n’appelle que l’interface appropriée.

IOptionsMonitorCache<TOptions> est utilisée par IOptionsMonitor<TOptions> pour mettre en cache les instances TOptions. IOptionsMonitorCache<TOptions> invalide les instances des options dans le moniteur afin que la valeur soit recalculée (TryRemove). Les valeurs peuvent aussi être introduites manuellement avec TryAdd. La méthode Clear est utilisée quand toutes les instances nommées doivent être recréées à la demande.

IOptionsChangeTokenSource<TOptions> s’utilise pour extraire le IChangeToken de suivi des modifications apportées à l’instance TOptions sous-jacente. Pour plus d’informations sur les primitives de jeton de modification, consultez Notifications de modification.

Avantages des interfaces d’options

L’utilisation d’un type de wrapper générique vous permet de dissocier la durée de vie de l’option du conteneur d’injection de dépendances (DI). L’interface IOptions<TOptions>.Value fournit une couche d’abstraction, y compris des contraintes génériques, sur votre type d’options. Vous bénéficiez ainsi des avantages suivants :

  • L’évaluation de l’instance de configuration T est différée à l’accès à IOptions<TOptions>.Value, plutôt qu’au moment de son injection. C’est important, car vous pouvez utiliser l’option T à partir de différents emplacements et choisir la sémantique de durée de vie sans modifier quoi que ce soit sur T.
  • Lors de l’inscription d’options de type T, vous n’avez pas besoin d’inscrire explicitement le type T. Il s’agit d’une commodité lorsque vous créez une bibliothèque avec des valeurs par défaut simples et que vous ne voulez pas forcer l’appelant à inscrire des options dans le conteneur d’intégration avec une durée de vie spécifique.
  • Du point de vue de l’API, elle autorise des contraintes sur le type T (dans ce cas, T est limité à un type de référence).

Utiliser IOptionsSnapshot pour lire des données mises à jour

Lorsque vous utilisez IOptionsSnapshot<TOptions>, les options sont calculées une fois par requête quand le système y accède et sont mises en cache pour toute la durée de vie de la requête. Les modifications apportées à la configuration sont lues après le démarrage de l’application lors de l’utilisation de fournisseurs de configuration qui prennent en charge la lecture des valeurs de configuration mises à jour.

La différence entre IOptionsMonitor et IOptionsSnapshot est que :

  • IOptionsMonitor est un service singleton qui récupère les valeurs d’option actuelles à tout moment, ce qui est particulièrement utile dans les dépendances Singleton.
  • IOptionsSnapshot est un service délimité et fournit un instantané des options au moment où l’objet IOptionsSnapshot<T> est construit. Les instantanés d’options sont conçus pour être utilisés avec des dépendances temporaires et étendues.

Le code suivant utilise 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}");
    }
}

Le code suivant inscrit une instance de configuration qui TransientFaultHandlingOptions se lie à :

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

Dans le code précédent, la méthode Configure<TOptions> est utilisée pour inscrire une instance de configuration à laquelle TOptions va se lier, et met à jour les options quand la configuration change.

IOptionsMonitor

Le type IOptionsMonitor prend en charge les notifications de changement et permet de réaliser des scénarios dans lesquels votre application peut avoir besoin de répondre aux changements de source de configuration de manière dynamique. Cette fonction est utile lorsque vous devez réagir à des modifications des données de configuration après le démarrage de l'application Les notifications de changement ne sont prises en charge que pour les fournisseurs de configuration basés sur un système de fichiers, tels que les suivants  :

Pour utiliser le moniteur d’options, les objets options sont paramétrés de la même façon à partir d’une section de configuration.

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

L'exemple suivant utilise 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}");
    }
}

Dans le code précédent, les modifications apportées au fichier de configuration JSON après le démarrage de l’application sont lues.

Conseil

Certains systèmes de fichiers, comme les conteneurs Docker et les partages réseau, peuvent ne pas envoyer de manière fiable les notifications de modifications. Lorsque vous utilisez l’interface IOptionsMonitor<TOptions> dans ces environnements, définissez la variable d’environnement DOTNET_USE_POLLING_FILE_WATCHER sur 1 ou true pour interroger le système de fichiers pour les modifications. L’intervalle auquel les modifications sont interrogées est toutes les quatre secondes et n’est pas configurable.

Pour plus d’informations sur les conteneurs Docker, consultez Conteneuriser une application .NET.

Prise en charge des options nommées à l’aide de IConfigureNamedOptions

Options nommées :

  • Sont utiles lorsque plusieurs sections de configuration se lient aux mêmes propriétés.
  • Sensibles à la casse.

Considérez le fichier appsettings.json suivant :

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

Au lieu de créer deux classes à lier Features:Personalize et Features:WeatherStation, la classe suivante est utilisée pour chaque section :

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; }
}

Le code ci-dessous configure les options nommées :

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"));

Le code suivant affiche les options nommées :

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);
    }
}

Toutes les options sont des instances nommées. Les instances IConfigureOptions<TOptions> sont traitées comme ciblant l’instance Options.DefaultName, qui est string.Empty. En outre, IConfigureNamedOptions<TOptions> implémente IConfigureOptions<TOptions>. L’implémentation par défaut de IOptionsFactory<TOptions> possède une logique qui utilise chaque élément de manière appropriée. L’option nommée null est utilisée pour cibler toutes les instances nommées au lieu d’une instance nommée spécifique. ConfigureAll et PostConfigureAll utilisent cette convention.

API OptionsBuilder

OptionsBuilder<TOptions> permet de configurer des instances TOptions. OptionsBuilder simplifie la création d’options nommées. En effet, il est le seul paramètre de l’appel AddOptions<TOptions>(string optionsName) initial et n’apparaît pas dans les appels ultérieurs. La validation des options et les surcharges ConfigureOptions qui acceptent des dépendances de service sont uniquement disponibles avec OptionsBuilder.

OptionsBuilder est utilisé dans la section Validation des options.

Utiliser les services d’injection de dépendances (DI) pour configurer des options

Lorsque vous configurez des options, vous pouvez utiliser l’injection de dépendances pour accéder aux services inscrits, et les utiliser pour configurer des options. Ceci est utile lorsque vous devez accéder à des services pour configurer des options. Les service sont accessibles à partir de l’injection de dépendances pendant la configuration des options de deux manières différentes :

  • Transmettez un délégué de configuration à Configure sur OptionsBuilder<TOptions>. OptionsBuilder<TOptions> fournit des surcharges de Configure qui permettent d’utiliser jusqu’à cinq services pour configurer des options :

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
    
  • Créer un type qui implémente IConfigureOptions<TOptions> ou IConfigureNamedOptions<TOptions> et inscrit le type en tant que service.

Nous vous recommandons de transmettre un délégué de configuration à Configure, car il est plus complexe de créer un service. La création d’un type équivaut à ce que fait l’infrastructure lors de l’appel de Configure. L’appel de Configure a pour effet d’inscrire une instance générique temporaire de IConfigureNamedOptions<TOptions>, dont l’un des constructeurs accepte les types de service génériques spécifiés.

Validation des options

La validation des options permet de valider les valeurs d’option.

Considérez le fichier appsettings.json suivant :

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

La classe suivante est liée à la section de configuration "MyCustomSettingsSection" et applique quelques règles DataAnnotations :

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; }
}

Dans la classe SettingsOptions précédente, la propriété ConfigurationSectionName contient le nom de la section de configuration à lier. Dans ce scénario, l’objet d’options fournit le nom de sa section de configuration.

Conseil

Le nom de la section de configuration est indépendant de l’objet de configuration auquel il est lié. En d’autres termes, une section de configuration nommée "FooBarOptions" peut être liée à un objet d’options nommé ZedOptions. Bien qu’il soit courant de les nommer de la même façon, cela n’est pas nécessaire et peut effectivement provoquer des conflits de noms.

Le code suivant :

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

La méthode d'extension ValidateDataAnnotations est définie dans le package NuGet Microsoft.Extensions.Options.DataAnnotations.

Le code suivant affiche les valeurs de configuration ou signale les erreurs de validation :

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);
            }
        }
    }
}

Le code suivant applique une règle de validation plus complexe à l’aide d’un délégué :

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.");

La validation se produit au moment de l’exécution, mais vous pouvez la configurer au démarrage en chaînant plutôt un appel à 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();

À compter de .NET 8, vous pouvez utiliser une autre API, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String), qui active la validation au démarrage pour un type d’options spécifique :

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 pour la validation complexe

La classe suivante implémente 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 permet de déplacer le code de validation dans une classe.

Notes

Cet exemple de code s’appuie sur le package NuGet Microsoft.Extensions.Configuration.Json.

À l’aide du code précédent, la validation est activée lors de la configuration des services avec le code suivant :

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>());

Options de post-configuration

Définissez la post-configuration avec IPostConfigureOptions<TOptions>. Après l’exécution de la configuration, une fois la configuration IConfigureOptions<TOptions> effectuée, elle peut être utile dans les scénarios où vous devez remplacer la configuration :

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

PostConfigure permet de post-configurer les options nommées :

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

Utilisez PostConfigureAll pour post-configurer toutes les instances de configuration :

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

Voir aussi