借助依赖关系注入,在注册服务及其相应的配置时,可以使用选项模式。 选项模式使库(和服务)的使用者能够要求 选项接口 实例(即 TOptions
选项类)。 通过强类型对象使用配置选项有助于确保一致的值表示形式,通过数据注释实现验证,并减轻手动分析字符串值的负担。 库使用者可以使用许多 配置提供程序 。 使用这些提供程序,使用者可以通过多种方式配置库。
作为 .NET 库作者,你将了解有关如何向库使用者正确公开选项模式的一般指南。 有多种方法可以实现相同的目标,还有几个需要考虑的因素。
命名约定
按照约定,负责注册服务的扩展方法命名 Add{Service}
,其中 {Service}
具有有意义的描述性名称。
Add{Service}
扩展方法在 ASP.NET Core 和 .NET 中很常见。
✔️ 请考虑能够将您的服务与其他服务区分开的名称。
❌ 不要使用已是 Microsoft 官方包中 .NET 生态系统的一部分的名称。
✔️ 请考虑将静态类命名为 {Type}Extensions
,其中 {Type}
是您要扩展的类型。
命名空间指南
Microsoft包利用 Microsoft.Extensions.DependencyInjection
命名空间来统一各种服务产品的注册。
✔️ 请考虑一个能够清晰标识您的程序包产品的命名空间。
❌ 请勿将 Microsoft.Extensions.DependencyInjection
命名空间用于非官方Microsoft包。
无参数
如果服务可以使用最小或无显式配置,请考虑使用无参数扩展方法。
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;
}
}
在上述代码中,AddMyLibraryService
执行以下操作:
- 扩展 IServiceCollection 的实例
- 调用OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection),类型参数为
LibraryOptions
- 链接对 Configure 的调用,用来指定默认选项值
IConfiguration
参数
当你创作向使用者公开许多选项的库时,可能需要考虑使用 IConfiguration
参数扩展方法。 应使用IConfiguration
函数将预期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;
}
}
小窍门
该方法 Configure<TOptions>(IServiceCollection, IConfiguration) 是 NuGet 包的 Microsoft.Extensions.Options.ConfigurationExtensions
一部分。
在上述代码中,AddMyLibraryService
执行以下操作:
- 扩展 IServiceCollection 的实例
- 定义 IConfiguration 参数
namedConfigurationSection
- 调用要传递 Configure<TOptions>(IServiceCollection, IConfiguration) 的泛型类型参数和
LibraryOptions
实例的namedConfigurationSection
以进行配置
此模式中的使用者提供已命名部分已限定作用域的 IConfiguration
实例:
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();
对 .AddMyLibraryService
的调用是在 IServiceCollection 类型上进行的。
作为库作者,指定默认值由你决定。
注释
可以将配置绑定到选项实例。 但是,存在名称冲突的风险 - 这将导致错误。 此外,当以这种方式手动绑定时,将选项模式的使用限制为读取一次。 对设置的更改不会重新绑定,因此使用者将无法使用 IOptionsMonitor 接口。
services.AddOptions<LibraryOptions>()
.Configure<IConfiguration>(
(options, configuration) =>
configuration.GetSection("LibraryOptions").Bind(options));
应改用 BindConfiguration 扩展方法。 此扩展方法将配置绑定到选项实例,并注册配置节的更改令牌源。 这允许使用者使用 IOptionsMonitor 接口。
配置节路径参数
库的使用者可能需要指定配置节路径来绑定基础 TOptions
类型。 在此方案中,你将在扩展方法中定义参数 string
。
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;
}
}
在上述代码中,AddMyLibraryService
执行以下操作:
- 扩展 IServiceCollection 的实例
- 定义了一个
string
参数configSectionPath
- 调用:
- 具有 AddOptions 泛型类型参数的
SupportOptions
- 具有给定 BindConfiguration 参数的
configSectionPath
- ValidateDataAnnotations 以启用数据注释验证
- ValidateOnStart 在启动时而不是在运行时强制实施验证
- 具有 AddOptions 泛型类型参数的
在下一个示例中, Microsoft.Extensions.Options.DataAnnotations NuGet 包用于启用数据批注验证。
SupportOptions
类定义如下:
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; }
}
假设使用以下 JSON appsettings.json 文件:
{
"Support": {
"Url": "https://support.example.com",
"Email": "help@support.example.com",
"PhoneNumber": "+1(888)-SUPPORT"
}
}
Action<TOptions>
参数
您的库的使用者可能有兴趣提供一个 lambda 表达式,以生成您的选项类的实例。 在此方案中,你将在扩展方法中定义参数 Action<LibraryOptions>
。
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;
}
}
在上述代码中,AddMyLibraryService
执行以下操作:
- 扩展 IServiceCollection 的实例
- 定义一个 Action<T> 参数
configureOptions
,其中T
为LibraryOptions
- 根据 Configure 操作调用
configureOptions
此模式下的使用者提供一个 Lambda 表达式(或一个符合 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();
选项实例参数
库的使用者可能更倾向于提供一个内联的选项实例。 在此方案中,你将公开一个扩展方法,该方法采用选项对象的 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;
}
}
在上述代码中,AddMyLibraryService
执行以下操作:
- 扩展 IServiceCollection 的实例
- 调用OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection),类型参数为
LibraryOptions
- 链接对 Configure 的调用,指定可从给定
userOptions
实例中替代的默认选项值
此模式中的使用者提供类的 LibraryOptions
实例,以内联方式定义所需的属性值:
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();
配置后
绑定或指定所有配置选项值后,可以使用配置后功能。 公开前面详述的相同 Action<TOptions>
参数 ,可以选择调用 PostConfigure。 发布配置在进行所有 .Configure
调用之后运行。 虽然原因不多,但你仍可以考虑使用PostConfigure
:
-
执行顺序:可以替代调用中
.Configure
设置的任何配置值。 - 验证:可以在应用所有其他配置后验证已设置默认值。
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;
}
}
在上述代码中,AddMyLibraryService
执行以下操作:
- 扩展 IServiceCollection 的实例
- 定义一个 Action<T> 参数
configureOptions
,其中T
为LibraryOptions
- 根据 PostConfigure 操作调用
configureOptions
此模式下的使用者提供一个 Lambda 表达式(或一个符合 Action<LibraryOptions>
参数的委托),就如同在非发布配置场景中,使用者使用 Action<TOptions>
参数提供一样:
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();