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, enables validation with data annotations, 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 and .NET alike.
✔️ 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;
}
}
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;
}
}
Consumers in this pattern provide the scoped IConfiguration instance of the named section:
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();
The call to .AddMyLibraryService is made on the IServiceCollection type.
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.
Instead, you should use the BindConfiguration extension method. This extension method binds the configuration to the options instance, and also registers a change token source for the configuration section. This allows consumers to use the IOptionsMonitor interface.
Configuration section path parameter
Consumers of your library may want to specify the configuration section path to bind your underlying TOptions type. In this scenario, you define a string parameter in your extension method.
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;
}
}
ValidateOnStart to enforce validation on start rather than in runtime
In the next example, the Microsoft.Extensions.Options.DataAnnotations NuGet package is used to enable data annotation validation. The SupportOptions class is defined as follows:
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; }
}
Imagine that the following JSON appsettings.json file is used:
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;
}
}
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;
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();
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;
}
}
Chains a call to Configure, which specifies default option values that can be overridden from the given userOptions instance
Consumers in this pattern provide an instance of the LibraryOptions class, defining desired property values inline:
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();
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. There are few reasons why you'd want to consider using PostConfigure:
Execution order: You can override any configuration values that were set in the .Configure calls.
Validation: You can validate the default values have been set after all other configurations have been applied.
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;
}
}
Defines an Action<T> parameter configureOptions where T is LibraryOptions
Calls PostConfigure given the configureOptions action
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;
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();
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see our contributor guide.
.NET feedback
.NET is an open source project. Select a link to provide feedback:
Understand and implement dependency injection in an ASP.NET Core app. Use ASP.NET Core's built-in service container to manage dependencies. Register services with the service container.