Conseils sur les modèles d’options pour les auteurs de bibliothèque .NET
Avec l’aide de l’injection de dépendances, l’inscription de vos services et leurs configurations correspondantes peuvent utiliser le modèle d’options. Le modèle d’options permet aux consommateurs de votre bibliothèque (et de vos services) de demander des instances d’interfaces d’options où TOptions
correspond à votre classe d’options. L’utilisation d’options de configuration par le biais d’objets fortement typés permet de garantir une représentation cohérente de la valeur, d’activer la validation avec des annotations de données et d’éviter la charge de travail que représente l’analyse manuelle de valeurs de chaîne. Il existe de nombreux fournisseurs de configuration que les consommateurs de votre bibliothèque peuvent utiliser. Ces fournisseurs permettent aux consommateurs de configurer votre bibliothèque de plusieurs façons.
En tant qu’auteur de bibliothèque .NET, vous allez découvrir des conseils généraux sur la manière d’exposer correctement le modèle d’options aux consommateurs de votre bibliothèque. Il existe différentes façons de parvenir au même résultat, et plusieurs considérations à prendre en compte.
Conventions d’affectation de noms
Par convention, les méthodes d’extension responsables de l’inscription des services sont nommées Add{Service}
, où {Service}
correspond à un nom explicite et descriptif. Les méthodes d’extension Add{Service}
sont courantes dans ASP.NET Core et .NET.
✔️ PRIVILÉGIEZ les noms qui lèvent l’ambiguité entre votre service et d’autres offres.
❌ N’utilisez PAS de noms qui font déjà partie de l’écosystème .NET à partir de packages Microsoft officiels.
✔️ ENVISAGEZ de nommer les classes statiques qui exposent les méthodes d’extension en tant que {Type}Extensions
, où {Type}
correspond au type que vous étendez.
Conseils relatifs à l’espace de noms
Les packages Microsoft utilisent l’espace de noms Microsoft.Extensions.DependencyInjection
pour unifier l’inscription de différentes offres de service.
✔️ PRIVILÉGIEZ un espace de noms qui identifie clairement votre offre de package.
❌ N’utilisez PAS l’espace de noms Microsoft.Extensions.DependencyInjection
pour les packages Microsoft non officiels.
Sans paramètre
Si votre service peut fonctionner avec une configuration minimale ou sans configuration explicite, envisagez une méthode d’extension sans paramètre.
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;
}
}
Dans le code précédent, AddMyLibraryService
:
- Étend une instance de IServiceCollection
- Appelle OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection) avec le paramètre de type
LibraryOptions
- Chaîne un appel à Configure, qui spécifie les valeurs des options par défaut
Paramètre IConfiguration
Lorsque vous créez une bibliothèque qui expose de nombreuses options aux consommateurs, vous pouvez envisager d’exiger une méthode d’extension de paramètre IConfiguration
. L’instance attendue IConfiguration
doit être étendue à une section nommée de la configuration à l’aide de la fonction 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;
}
}
Conseil
La méthode Configure<TOptions>(IServiceCollection, IConfiguration) fait partie du package NuGet Microsoft.Extensions.Options.ConfigurationExtensions
.
Dans le code précédent, AddMyLibraryService
:
- Étend une instance de IServiceCollection
- Définit un paramètre IConfiguration
namedConfigurationSection
- Appelle Configure<TOptions>(IServiceCollection, IConfiguration) en transmettant le paramètre de type générique
LibraryOptions
et l’instancenamedConfigurationSection
à configurer
Les consommateurs de ce modèle fournissent l’instance délimitée IConfiguration
de la section nommée :
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();
L’appel à .AddMyLibraryService
est effectué sur le type IServiceCollection.
En tant qu’auteur de bibliothèque, la spécification des valeurs par défaut vous incombe.
Notes
Il est possible de lier la configuration à une instance d’options. Toutefois, il existe un risque de collisions de noms susceptible de provoquer des erreurs. En outre, lors d’une telle liaison manuelle, vous limitez la consommation de votre modèle d’options en lecture seule. Les modifications apportées aux paramètres ne seront pas une nouvelle fois liées, car de tels consommateurs ne pourront pas utiliser l’interface IOptionsMonitor.
services.AddOptions<LibraryOptions>()
.Configure<IConfiguration>(
(options, configuration) =>
configuration.GetSection("LibraryOptions").Bind(options));
Vous devez plutôt utiliser la méthode d’extension BindConfiguration. Cette méthode d’extension lie la configuration à l’instance d’options et inscrit également une source de jeton de modification pour la section de configuration. Les consommateurs peuvent ainsi utiliser l’interface IOptionsMonitor.
Paramètre de chemin d’accès à la section de configuration
Les consommateurs de votre bibliothèque peuvent spécifier le chemin de section de configuration pour lier votre type sous-jacent TOptions
. Dans ce scénario, vous définissez un paramètre string
dans votre méthode d’extension.
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;
}
}
Dans le code précédent, AddMyLibraryService
:
- Étend une instance de IServiceCollection
- Définit un paramètre
string
configSectionPath
- Appelle :
- AddOptions avec le paramètre de type générique
SupportOptions
- BindConfiguration avec le paramètre donné
configSectionPath
- ValidateDataAnnotations pour activer la validation des annotations de données
- ValidateOnStart pour appliquer la validation au démarrage plutôt qu’au runtime
- AddOptions avec le paramètre de type générique
Dans l’exemple suivant, le package NuGet Microsoft.Extensions.Options.DataAnnotations est utilisé pour activer la validation des annotations de données. La classe SupportOptions
est définie comme suit :
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; }
}
Imaginez que le fichier JSON appsettings.json suivant est utilisé :
{
"Support": {
"Url": "https://support.example.com",
"Email": "help@support.example.com",
"PhoneNumber": "+1(888)-SUPPORT"
}
}
Paramètre Action<TOptions>
Les consommateurs de votre bibliothèque peuvent être intéressés par la mise à disposition d’une expression lambda qui génère une instance de votre classe d’options. Dans ce scénario, vous définissez un paramètre Action<LibraryOptions>
dans votre méthode d’extension.
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;
}
}
Dans le code précédent, AddMyLibraryService
:
- Étend une instance de IServiceCollection
- Définit un paramètre Action<T>
configureOptions
, oùT
correspond àLibraryOptions
- Appelle Configure compte tenu de l’action
configureOptions
Les consommateurs de ce modèle fournissent une expression lambda (ou un délégué qui satisfait le paramètre 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();
Paramètre d’instance d’options
Les consommateurs de votre bibliothèque peuvent préférer fournir une instance d’options inlined. Dans ce scénario, vous exposez une méthode d’extension qui utilise une instance de votre objet d’options, 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;
}
}
Dans le code précédent, AddMyLibraryService
:
- Étend une instance de IServiceCollection
- Appelle OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection) avec le paramètre de type
LibraryOptions
- Chaîne un appel à Configure, qui spécifie les valeurs d’options par défaut qui peuvent être substituées à partir de l’instance donnée
userOptions
Les consommateurs de ce modèle fournissent une instance de la classe LibraryOptions
, en définissant les valeurs de propriété souhaitées inlined :
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();
Après la configuration
Une fois toutes les valeurs d’options de configuration liées ou spécifiées, la fonctionnalité post-configuration est disponible. En exposant le même paramètre Action<TOptions>
détaillé précédemment, vous pouvez choisir d’appeler PostConfigure. La post-configuration s’exécute après tous les appels .Configure
. Il existe quelques raisons pour lesquelles vous souhaitez envisager d’utiliser PostConfigure
:
- Ordre d’exécution : vous pouvez remplacer toutes les valeurs de configuration qui ont été définies dans les
.Configure
appels. - Validation : vous pouvez vérifier que les valeurs par défaut ont été définies une fois que toutes les autres configurations ont été appliquées.
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;
}
}
Dans le code précédent, AddMyLibraryService
:
- Étend une instance de IServiceCollection
- Définit un paramètre Action<T>
configureOptions
, oùT
correspond àLibraryOptions
- Appelle PostConfigure compte tenu de l’action
configureOptions
Les consommateurs de ce modèle fournissent une expression lambda (ou un délégué qui satisfait le paramètre Action<LibraryOptions>
), comme ils le feraient avec le paramètre Action<TOptions>
dans un scénario autre que pos-configuration :
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();