面向 .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 执行以下操作:

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;
    }
}

在上述代码中,AddMyLibraryService 执行以下操作:

此模式中的使用者提供已命名部分已限定作用域的 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 执行以下操作:

在以下示例中,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 执行以下操作:

此模式下的使用者提供一个 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 执行以下操作:

此模式下的使用者提供 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 执行以下操作:

此模式下的使用者提供一个 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();

另请参阅