面向 .NET 库创建者的选项模式指南
借助依赖关系注入,在注册服务及其相应的配置时,可以使用选项模式。 通过选项模式,库(和服务)的使用者可以要求提供选项接口的实例,其中 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 的实例
- 调用类型参数为
LibraryOptions
的 OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection) - 链接对 Configure 的调用,用来指定默认选项值
IConfiguration
参数
在创建向使用者公开许多选项的库时,可能需要考虑要求使用 IConfiguration
参数扩展方法。 应使用 IConfiguration.GetSection 函数,将预期的 IConfiguration
实例的作用域限定为此配置的已命名部分。
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) 方法是 Microsoft.Extensions.Options.ConfigurationExtensions
NuGet 包的一部分。
在上述代码中,AddMyLibraryService
执行以下操作:
- 扩展 IServiceCollection 的实例
- 定义 IConfiguration 参数
namedConfigurationSection
- 调用要传递
LibraryOptions
的泛型类型参数和namedConfigurationSection
实例的 Configure<TOptions>(IServiceCollection, IConfiguration) 以进行配置
此模式中的使用者提供已命名部分已限定作用域的 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
- 调用:
- 具有
SupportOptions
泛型类型参数的 AddOptions - 具有给定
configSectionPath
参数的 BindConfiguration - ValidateDataAnnotations 以启用数据注释验证
- ValidateOnStart 在启动时而不是运行时强制验证
- 具有
在以下示例中,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
- 根据
configureOptions
操作调用 Configure
此模式下的使用者提供一个 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 的实例
- 调用类型参数为
LibraryOptions
的 OptionsServiceCollectionExtensions.AddOptions<TOptions>(IServiceCollection) - 链接对 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
- 根据
configureOptions
操作调用 PostConfigure
此模式下的使用者提供一个 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();