代码改变世界

[转]使用IConfigureNamedOptions和ConfigureAll配置命名选项

2019-10-11 18:44  音乐让我说  阅读(828)  评论(0编辑  收藏  举报

这是我一篇关于在ASP.NET Core 2.x中使用多个强类型设置实例的后续文章在文章的结尾,我介绍了命名选项的概念,选项已添加到ASP.NET Core 2.0中。在本文中,我将详细介绍如何配置命名选项。我将特别关注:

快速回顾命名选项

在上一篇文章中,我深入研究了名为options旨在解决的方案。命名选项提供了一个解决方案,您希望拥有多个强类型设置类的实例,每个实例都可以从DI容器中解析。

在上一篇文章中,我使用了一种场景,其中您想要使用WebHooks向Slack发送消息的任意数量的设置。例如,假设您具有以下强类型的设置对象:

public class SlackApiSettings  
{
    public string WebhookUrl { get; set; }
    public string DisplayName { get; set; }
}

以及以下配置(存储在appsettings.json中),该配置将加载到IConfigurationapp上对象中Startup

{
  "SlackApi": {
    "DevChannel" : {
      "WebhookUrl": "https://hooks.slack.com/T1/B1/111111",
      "DisplayName": "c0mp4ny 5l4ck b07"
    },
    "GeneralChannel" : {
      "WebhookUrl": "https://hooks.slack.com/T2/B2/222222",
      "DisplayName": "Company Slack Bot"
    },
    "PublicChannel" : {
      "WebhookUrl": "https://hooks.slack.com/T3/B3/333333",
      "DisplayName": "Professional Looking name"
    }
  }

您可以使用以下命令将每个单独的通道绑定到新SlackApiSettings实例Startup.ConfigureServices()

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<SlackApiSettings>("Dev", Configuration.GetSection("SlackApi:DevChannel")); 
    services.Configure<SlackApiSettings>("General", Configuration.GetSection("SlackApi:GeneralChannel")); 
    services.Configure<SlackApiSettings>("Public", Configuration.GetSection("SlackApi:PublicChannel"));
} 

每个实例都有一个唯一的名称(方法的第一个参数Configure()),以及一个要绑定的配置节。您可以使用IOptionsSnapshot<>接口方法及其Get(name)方法访问以下设置

public class SlackNotificationService
{
    public SlackNotificationService(IOptionsSnapshot<SlackApiSettings> options)
    {
        // fetch the settings for each channel
        SlackApiSettings devSettings = options.Get("Dev");
        SlackApiSettings generalSettings = options.Get("General");
        SlackApiSettings publicSettings = options.Get("Public");
    }
}

值得记住的是,在IOptionsSnapshot<T> 选项被请求时(每次请求一次)重新绑定选项这与IOptions在应用程序的生命周期内一次绑定选项不同由于通常使用来公开命名选项IOptionsSnapshot<T>,因此每个请求都它们绑定一次。

命名选项与默认选项实例

您可以在同一应用程序中使用命名选项和默认选项,它们不会干扰。Configure()不指定名称的调用将以默认选项为目标,例如:

public void ConfigureServices(IServiceCollection services)
{
    // Configure named options
    services.Configure<SlackApiSettings>("Dev", Configuration.GetSection("SlackApi:DevChannel")); 
    services.Configure<SlackApiSettings>("Public", Configuration.GetSection("SlackApi:PublicChannel"));

    // Configure the default "unnamed" options
    services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi:GeneralChannel")); 
} 

您可以使用Value属性来检索默认选项IOptions<T>IOptionsSnapshot<T>

public class SlackNotificationService
{
    public SlackNotificationService(IOptionsSnapshot<SlackApiSettings> options)
    {
        // fetch the settings for each channel
        SlackApiSettings devSettings = options.Get("Dev");
        SlackApiSettings publicSettings = options.Get("Public");

        // fetch the default unnamed options
        SlackApiSettings defaultSettings = options.Value;
    }
}

即使您没有在应用程序中显式使用命名选项,Options框架本身也会在后台使用命名选项。当您调用Configure<T>(section)扩展方法(不提供名称)时,框架将使用Options.DefaultName默认名称在后台调用扩展方法命名版本

public static IServiceCollection Configure<TOptions>(
    this IServiceCollection services, IConfiguration config) 
    where TOptions : class
{
    return services.Configure<TOptions>(Options.Options.DefaultName, config);
}

Options.DefaultName设置为string.Empty,因此以下两行具有相同的效果-它们配置默认选项对象:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi:GeneralChannel")); 
    // Using string.Empty as the named options type 
    services.Configure<SlackApiSettings>(string.Empty, Configuration.GetSection("SlackApi:GeneralChannel")); 
}

对于此帖子,这是要记住的重要事项-默认选项只是命名为具有特定名称的选项string.Empty

对于本文的其余部分,我将展示一些配置命名选项的方法,特别是与默认的“未命名”选项相比。

使用以下命令将服务注入命名选项 IConfigureNamedOptions<T>

配置选项时,通常需要外部服务。以前写过关于如何使用IConfigureOptions<T>配置选项时,访问服务在上一篇文章中,我讨论了将这些服务注册为范围服务时需要注意的一些问题。

在所有这些文章中,我描述了如何使用来配置默认选项IConfigureOptions<T>您还可以实现一个等效接口来配置命名选项IConfigureNamedOptions<T>

public interface IConfigureNamedOptions<in TOptions> : IConfigureOptions<TOptions> where TOptions : class
{
    void Configure(string name, TOptions options);
}

此接口有两种方法:

  • Configure(name, options) -由接口直接实现
  • Configure(options)-由IConfigureOptions<T>(继承)实现

在实现接口时,重要的是要了解Configure(name, options)将为应用程序中实例化的选项对象的每个实例调用该接口T这包括所有命名选项,包括默认选项。由您决定在运行时当前正在配置哪个实例。

实施IConfigureNamedOptions<T>特定命名的选项实例

我认为最容易理解的方法IConfigureNamedOptions<T>是举一个例子。让我们考虑基于我之前描述的Slack WebHooks场景的情况您的应用必须调用多个WebHook URL,这些URL在appsettings.json中配置,并绑定到的单独命名实例SlackApiSettings此外,您还有一个默认选项实例。这些都按照我之前所述进行配置:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<SlackApiSettings>("Dev", Configuration.GetSection("SlackApi:DevChannel")); 
    services.Configure<SlackApiSettings>("Public", Configuration.GetSection("SlackApi:PublicChannel"));
    services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi:GeneralChannel")); 
} 

现在想象一下,命名实例的WebHook URL "Public"事先未知,因此无法将其添加到appsettings.json中相反,您可以使用一个单独的服务PublicSlackDetailsService来查找URL:

public interface PublicSlackDetailsService
{
    public string GetPublicWebhookUrl() => return "/some/url";
}

请注意,该GetPublicWebhookUrl()方法是同步的,而不是async在配置对象时,将在DI容器内进行选项配置,因此,执行异步操作(如调用远程端点)不是一个好地方。如果发现需要此功能,请考虑使用其他模式(例如,工厂对象)代替“选项”。

PublicSlackDetailsService服务已在ConfigureServices()以下位置注册为Singleton

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<PublicSlackDetailsService>();
} 

重要提示:如果您需要使用范围服务来配置命名选项,请参阅我的上一篇文章

通过实施IConfigureNamedOptions<T>,您可以使用以下服务配置特定的命名选项实例("Public"PublicSlackDetailsService

public class ConfigurePublicSlackApiSettings: IConfigureNamedOptions<SlackApiSettings>
{
    // inject the PublicSlackDetailsService directly
    private readonly PublicSlackDetailsService _service;
    public ConfigurePublicSlackApiSettings(PublicSlackDetailsService service)
    {
        _service = service;
    }

    // Configure the named instance
    public void Configure(string name, SlackApiSettings options)
    {
        // Only configure the options if this is the correct instance
        if (name == "Public")
        {
            options.WebhookUrl = _service.GetPublicWebhookUrl();
        }
    }

    // This won't be called, but is required for the interface
    public void Configure(SlackApiSettings options) => Configure(Options.DefaultName, options);
}

限制ConfigurePublicSlackApiSettings只配置"Public"命名实例很容易name传递参数进行简单检查Configure(name, options)可以避免同时配置其他命名实例(例如"Dev")或默认实例(name将是string.Empty)。

还要注意的另一件事是,Configure(options)方法(IConfigureOptions接口要求Configure(name, options)使用名称委托给该方法Options.DefaultName从技术上讲,这实际上不是必需的:用于创建选项(OptionsFactory的选项基础结构总是Configure(name, options)在其可用时优先调用但是,所示示例应视为最佳实践。

最后要做的是将ConfigurePublicSlackApiSettings注册到DI容器中ConfigureServices()

public void ConfigureServices(IServiceCollection services)
{
    // Configure the options objects using appsettings.json
    services.Configure<SlackApiSettings>("Dev", Configuration.GetSection("SlackApi:DevChannel")); 
    services.Configure<SlackApiSettings>("Public", Configuration.GetSection("SlackApi:PublicChannel"));
    services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi:GeneralChannel")); 

    // Add required service
    services.AddSingleton<PublicSlackDetailsService>();

    // Add named options configuration AFTER other configuration
    services.AddSingleton<IConfigureOptions<SlackApiSettings>, ConfigurePublicSlackApiSettings>);
}

重要提示:请注意,您必须为注册IConfigureOptions<T>实例,不是一个IConfigureNamedOptions<T>实例!同样,与所有选项配置一样,顺序很重要。

每当您请求SlackApiSettingsusing 的实例时IOptionsSnapshot<T>.Get(),该ConfigurePublicSlackApiSettings.Configure(name, options)方法都会执行。"Public"实例将WebhookUrl更新属性,所有其他命名选项将被忽略。

现在,我之前说过,默认选项实例只是具有特殊名称的命名选项实例string.Empty我也说,IConfigureNamedOptions<T>被调用的所有命名的设置。这包括使用来请求默认选项实例时IOptions<T>.ValueConfigurePublicSlackApiSettings处理此作为name传递到Configure(name, options)string.Empty如此我们的代码优雅地忽略它,同其他任何命名选项。

使用以下命令配置所有选项对象 ConfigureAll<T>

到目前为止,在最近的帖子中,我已经展示了如何:

我没有显示的一件事是如何一次配置所有选项:命名选项和默认选项。如果要绑定到配置部分或使用Action<>,则最简单的方法是使用ConfigureAll()扩展方法

public void ConfigureServices(IServiceCollection services)
{
    // Configure ALL options instances, both named and default
    services.ConfigureAll<SlackApiSettings>(Configuration.GetSection("SlackApi:GeneralChannel")); 
    services.ConfigureAll<SlackApiSettings>(options => options.DisplayName = "Unknown"); 

    // Override values for named options
    services.Configure<SlackApiSettings>("Dev", Configuration.GetSection("SlackApi:DevChannel")); 
    services.Configure<SlackApiSettings>("Public", Configuration.GetSection("SlackApi:PublicChannel"));

    // Override values for default options 
    services.Configure<SlackApiSettings>(() => options.DisplayName = "default");
}

在此示例中,我们将请求的每个选项对象绑定"SlackApi:GeneralChannel"配置部分,并将设置DisplayName"Unknown"然后,根据所请求的选项实例的名称,可能会执行另一个配置步骤:

  • 如果请求了默认实例(使用IOptions<T>.ValueIOptionsSnapshot<T>.Value,则将DisplayName其设置为"default"
  • 如果"Dev"请求命名实例,则该实例将绑定到"SlackApi:DevChannel"配置部分
  • 如果"Public"请求命名实例,则该实例将绑定到"SlackApi:PublicChannel"配置部分
  • 如果请求任何其他命名实例,则不会进行进一步的配置。

这就提出了另一个重要的观点:

您可以请求尚未显式注册的命名选项实例。

当您可以使用简单选项Action<>或绑定到配置部分时,以这种方式配置所有选项很方便,但是如果需要使用类似服务,该PublicSlackDetailsService怎么办?在这种情况下,您将返回到实施IConfigureNamedOptions<T>

配置所有选项实例时使用注入的服务

为了简单起见,我们将扩展前面描述的场景。而不是使用的PublicSlackDetailsService设置WebhookUrl只对"Public"命名方案的实例,我们想象,我们需要设置值,每个命名的选项的实例,包括默认选项。幸运的是,我们需要做的就是if()从先前的实现中删除该语句,并且我们已经到了很多:

public class ConfigureAllSlackApiSettings: IConfigureNamedOptions<SlackApiSettings>
{
    // inject the PublicSlackDetailsService directly
    private readonly PublicSlackDetailsService _service;
    public ConfigurePublicSlackApiSettings(PublicSlackDetailsService service)
    {
        _service = service;
    }

    // Configure all instances
    public void Configure(string name, SlackApiSettings options)
    {
        // we don't care which instance it is, just set the URL!
        options.WebhookUrl = _service.GetPublicWebhookUrl();
    }

    // This won't be called, but is required for the interface
    public void Configure(SlackApiSettings options) => Configure(Options.DefaultName, options);
}

剩下的就是ConfigureAllSlackApiSettings在默认容器中注册请记住,顺序对于选项配置很重要:如果要ConfigureAllSlackApiSettings在其他配置之前运行,则应该Configure()ConfigureServices;中的其他方法之前显示否则应该出现在它们之后:

public void ConfigureServices(IServiceCollection services)
{
    // Configure ALL options instances, both named and default
    services.ConfigureAll<SlackApiSettings>(options => options.DisplayName = "Unknown"); 

    // Override values for named options
    services.Configure<SlackApiSettings>("Dev", Configuration.GetSection("SlackApi:DevChannel")); 
    services.Configure<SlackApiSettings>("Public", Configuration.GetSection("SlackApi:PublicChannel"));

    // Add ALL options configuration AFTER other configuration (in this case)
    services.AddSingleton<IConfigureOptions<SlackApiSettings>, ConfigureAllSlackApiSettings>);
}

随着Configure<T>()ConfigureAll<T>()IConfigureOptions<T>,和IConfigureNamedOptions<T>你有一个广泛的配置无论是默认选项,并在您的应用程序命名选项工具。IConfigureNamedOptions<T>特别灵活-将配置应用于所有选项实例,子集或特定命名实例很容易。

但是,一如既往,最好选择最简单的方法来完成工作。不需要命名选项吗?不要使用它们。需要绑定到配置部分吗?只需使用Configure<T>()KISS制定规则,但很高兴知道您是否需要灵活性。

摘要

在本文中,我描述了默认选项对象如何是命名选项的特例,其名称为string.Empty我展示了如何通过实现来配置需要其他注入服务的选项IConfigureNamedOptions<T>,以及如何限制也应用了哪些选项。

我还展示了如何使用ConfigureAll<T>()扩展方法将配置应用于所有选项,包括命名实例和默认实例最后,我展示了IConfigureNamedOptions<T>在需要访问其他服务进行配置时如何使用相同的方法

如果要实现IConfigureNamedOptions<T>,请务必考虑所使用服务的生命周期。特别是,您需要采取额外的步骤来使用范围服务,如我在上一篇文章中所述

 

 

转发自:https://andrewlock.net/configuring-named-options-using-iconfigurenamedoptions-and-configureall/