.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函数将预期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;
    }
}

在上述代码中,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();

另请参阅