Options pattern guidance for .NET library authors
With the help of dependency injection, registering your services and their corresponding configurations can make use of the options pattern. The options pattern enables consumers of your library (and your services) to require instances of options interfaces where TOptions is your options class. Consuming configuration options through strongly-typed objects helps to ensure consistent value representation, and removes the burden of manually parsing string values. There are many configuration providers for consumers of your library to use. With these providers, consumers can configure your library in many ways.
As a .NET library author, you'll learn general guidance on how to correctly expose the options pattern to consumers of your library. There are various ways to achieve the same thing, and several considerations to make.
Naming conventions
By convention, extension methods responsible for registering services are named Add{Service}, where {Service} is a meaningful and descriptive name. Add{Service} extension methods are commonplace in ASP.NET Core.
✔️ CONSIDER names that disambiguate your service from other offerings.
❌ DO NOT use names that are already part of the .NET ecosystem from official Microsoft packages.
✔️ CONSIDER naming static classes that expose extension methods as {Type}Extensions, where {Type} is the type that you're extending.
Namespace guidance
Microsoft packages make use of the Microsoft.Extensions.DependencyInjection namespace to unify the registration of various service offerings.
✔️ CONSIDER a namespace that clearly identifies your package offering.
❌ DO NOT use the Microsoft.Extensions.DependencyInjection namespace for non-official Microsoft packages.
Parameterless
If your service can work with minimal or no explicit configuration, consider a parameterless extension method.
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;
}
}
In the preceding code, the AddMyLibraryService:
- Extends an instance of IServiceCollection
- Calls OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection) with the type parameter of
LibraryOptions - Chains a call to Configure, which specifies the default option values
IConfiguration parameter
When you author a library that exposes many options to consumers, you may want to consider requiring an IConfiguration parameter extension method. The expected IConfiguration instance should be scoped to a named section of the configuration by using the IConfiguration.GetSection function.
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;
}
}
Tip
The Configure<TOptions>(IServiceCollection, IConfiguration) method is part of the Microsoft.Extensions.Options.ConfigurationExtensions NuGet package.
In the preceding code, the AddMyLibraryService:
- Extends an instance of IServiceCollection
- Defines an IConfiguration parameter
namedConfigurationSection - Calls Configure<TOptions>(IServiceCollection, IConfiguration) passing the generic type parameter of
LibraryOptionsand thenamedConfigurationSectioninstance to configure
Consumers in this pattern provide the scoped IConfiguration instance of the named section:
using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Options.ConfigParam;
class Program
{
static async Task Main(string[] args)
{
using IHost host = CreateHostBuilder(args).Build();
// Application code should start here.
await host.RunAsync();
}
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddMyLibraryService(
context.Configuration.GetSection("LibraryOptions"));
});
}
The call to .AddMyLibraryService is made in the ConfigureServices method. The same is true when using a Startup class, the addition of services being registered occurs in ConfigureServices.
As the library author, specifying default values is up to you.
Note
It is possible to bind configuration to an options instance. However, there is a risk of name collisions - which will cause errors. Additionally, when manually binding in this way, you limit the consumption of your options pattern to read-once. Changes to settings will not be re-bound, as such consumers will not be able to use the IOptionsMonitor interface.
services.AddOptions<LibraryOptions>()
.Configure<IConfiguration>(
(options, configuration) =>
configuration.GetSection("LibraryOptions").Bind(options));
Action<TOptions> parameter
Consumers of your library may be interested in providing a lambda expression that yields an instance of your options class. In this scenario, you define an Action<LibraryOptions> parameter in your extension method.
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;
}
}
In the preceding code, the AddMyLibraryService:
- Extends an instance of IServiceCollection
- Defines an Action<T> parameter
configureOptionswhereTisLibraryOptions - Calls Configure given the
configureOptionsaction
Consumers in this pattern provide a lambda expression (or a delegate that satisfies the Action<LibraryOptions> parameter):
using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Options.Action;
class Program
{
static async Task Main(string[] args)
{
using IHost host = CreateHostBuilder(args).Build();
// Application code should start here.
await host.RunAsync();
}
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddMyLibraryService(options =>
{
// User defined option values
// options.SomePropertyValue = ...
});
});
}
Options instance parameter
Consumers of your library might prefer to provide an inlined options instance. In this scenario, you expose an extension method that takes an instance of your options object, 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;
}
}
In the preceding code, the AddMyLibraryService:
- Extends an instance of IServiceCollection
- Calls OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection) with the type parameter of
LibraryOptions - Chains a call to Configure, which specifies default option values that can be overridden from the given
userOptionsinstance
Consumers in this pattern provide an instance of the LibraryOptions class, defining desired property values inline:
using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Options.Object;
class Program
{
static async Task Main(string[] args)
{
using IHost host = CreateHostBuilder(args).Build();
// Application code should start here.
await host.RunAsync();
}
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddMyLibraryService(new LibraryOptions
{
// Specify option values
// SomePropertyValue = ...
});
});
}
Post configuration
After all configuration option values are bound or specified, post configuration functionality is available. Exposing the same Action<TOptions> parameter detailed earlier, you could choose to call PostConfigure. Post configure runs after all .Configure calls.
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;
}
}
In the preceding code, the AddMyLibraryService:
- Extends an instance of IServiceCollection
- Defines an Action<T> parameter
configureOptionswhereTisLibraryOptions - Calls PostConfigure given the
configureOptionsaction
Consumers in this pattern provide a lambda expression (or a delegate that satisfies the Action<LibraryOptions> parameter), just as they would with the Action<TOptions> parameter in a non-post configuration scenario:
using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Options.PostConfig;
class Program
{
static async Task Main(string[] args)
{
using IHost host = CreateHostBuilder(args).Build();
// Application code should start here.
await host.RunAsync();
}
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddMyLibraryService(options =>
{
// Specify option values
// options.SomePropertyValue = ...
});
});
}
See also
Povratne informacije
Pošalјite i prikažite povratne informacije za