如何在 .NET Core 中使 IOptions 部分成为可选?

Ant*_*ean 5 asp.net-core

考虑一个可选择支持 LDAP 身份验证的示例服务,否则,它会执行类似本地身份验证的操作。当 LDAP 完全配置完毕后,appsettings.json 可能如下所示...

{
  "LdapOptions": {
    "Host": "ldap.example.com",
    "Port": 389
  }
}
Run Code Online (Sandbox Code Playgroud)

有一个选项类。

{
  "LdapOptions": {
    "Host": "ldap.example.com",
    "Port": 389
  }
}
Run Code Online (Sandbox Code Playgroud)

Startup 具有预期的配置调用。

public class LdapOptions
{
    public string Host { get; set; }
    public int Port { get; set; } = 389;
}
Run Code Online (Sandbox Code Playgroud)

当我有完整有效的“LdapOptions”部分时,这非常有用。但是,如果我故意将该部分从我的应用程序设置中删除,那就不太好了。

IOptions<TOptions>即使我将该部分完全排除在我的应用程序设置之外,实例也会解析;如果我完全删除启动配置调用,它甚至可以解决!我得到一个对象,根据属性值,该对象显示为default(TOptions).

service.Configure<LdapOptions>(nameof(LdapOptions));
Run Code Online (Sandbox Code Playgroud)

如果有意省略某个部分,我不想依赖于检查属性。我可以想象这样的场景:对象中的所有属性都有明确的默认值,而这是行不通的。我想要类似 aMaybe<TOptions>HasValue属性,但我会选择 a null

有什么办法可以使选项部分成为可选的吗?


更新:请注意,我还打算验证数据注释......

public AuthenticationService(IOptions<LdapOptions> ldapOptions)
{
    this.ldapOptions = ldapOptions.Value; // never null, sometimes default(LdapOptions)!
}
Run Code Online (Sandbox Code Playgroud)

因此,我真正想要的是当该部分缺失 ( ) 时可选选项有效conf.Exists() == false,然后当该部分部分或完全填写时启动正常验证。

我无法想象任何使用数据注释验证的解决方案取决于创建默认实例的行为(例如,Host 没有正确的默认值,因此默认实例将始终无效)。

Mic*_*erg 5

整个想法IOptions<T>是具有非空默认值,以便您的设置文件不包含数百/数千个部分来配置整个 ASP 管道

因此,不可能将其设置为可选,因为您将得到 null,但您始终可以定义一些“神奇”属性来指示是否已配置:

public class LdapOptions
{
    public bool IsEnabled { get; set; } = false;
    public string Host { get; set; }
    public int Port { get; set; } = 389;
}
Run Code Online (Sandbox Code Playgroud)

和您的应用程序设置文件:

{
  "LdapOptions": {
    "IsEnabled: true,
    "Host": "ldap.example.com",
    "Port": 389
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果您在设置中将“IsEnabled”始终保持为“true”,如果 IsEnabled 为 false,则意味着该部分丢失。

另一种解决方案是使用不同的设计方法,例如将身份验证类型放入设置文件中:

public class LdapOptions
{
    public string AuthType { get; set; } = "Local";
    public string Host { get; set; }
    public int Port { get; set; } = 389;
}
Run Code Online (Sandbox Code Playgroud)

以及您的应用程序设置:

{
  "LdapOptions": {
    "AuthType : "LDAP",
    "Host": "ldap.example.com",
    "Port": 389
  }
}
Run Code Online (Sandbox Code Playgroud)

在我看来,这是一种更干净、更一致的方法

如果你必须有一个基于可用/缺失部分的逻辑,你也可以直接配置它:

var section = conf.GetSection(nameof(LdapOptions));
var optionsBuilder = services.AddOptions<LdapOptions>();

if section.Value != null {
    optionsBuilder.Configure(section).ValidateDataAnnotations();
}
else {
    optionsBuilder.Configure(options => {
       // Set defaults here
       options.Host = "Deafult Host";
    }
}
Run Code Online (Sandbox Code Playgroud)


Ant*_*ean 0

我想避免 Startup 中的 lambda 表达式需要为每个“可选”部分正确复制/粘贴,并且我想非常明确地说明可选性(以一些尴尬的命名为代价)。

启动.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddOption<Optional<LdapOptions>>()
        .ConfigureOptional(conf.GetSection(nameof(LdapOptions)))
        .ValidateOptionalDataAnnotations();
}
Run Code Online (Sandbox Code Playgroud)

可选类型非常简单,但可能需要一个更好的名称(以避免干扰通用 Option/Some/Maybe 模式的其他实现)。我想过只使用null,但这似乎与 Options 坚持无论如何都要返回一些东西相反。

可选.cs

    public class Optional<TOptions> where TOptions : class
    {
        public TOptions Value { get; set; }
        public bool HasValue { get => !(Value is null); }
    }
Run Code Online (Sandbox Code Playgroud)

配置扩展方法考虑了节的存在。

可选扩展.​​cs

    public static class OptionalExtensions
    {
        public static OptionsBuilder<Optional<TOptions>> ConfigureOptional<TOptions>(this OptionsBuilder<Optional<TOptions>> optionsBuilder, IConfigurationSection config) where TOptions : class
        {
            return optionsBuilder.Configure(options =>
            {
                if (config.Exists())
                {
                    options.Value = config.Get<TOptions>();
                }
            });
        }

        public static OptionsBuilder<Optional<TOptions>> ValidateOptionalDataAnnotations<TOptions>(this OptionsBuilder<Optional<TOptions>> optionsBuilder) where TOptions : class
        {
            optionsBuilder.Services.AddSingleton<IValidateOptions<Optional<TOptions>>>(new DataAnnotationValidateOptional<TOptions>(optionsBuilder.Name));
            return optionsBuilder;
        }
    }
Run Code Online (Sandbox Code Playgroud)

验证扩展方法与自定义选项验证器一起使用,该验证器还考虑了缺失部分的工作方式(如注释所示,“缺失可选选项始终有效”)。

DataAnnotationValidateOptional.cs

    public class DataAnnotationValidateOptional<TOptions> : IValidateOptions<Optional<TOptions>> where TOptions : class
    {
        private readonly DataAnnotationValidateOptions<TOptions> innerValidator;

        public DataAnnotationValidateOptional(string name)
        {
            this.innerValidator = new DataAnnotationValidateOptions<TOptions>(name);
        }

        public ValidateOptionsResult Validate(string name, Optional<TOptions> options)
        {
            if (options.Value is null)
            {
                // Missing optional options are always valid.
                return ValidateOptionsResult.Success;
            }

            return this.innerValidator.Validate(name, options.Value);
        }
    }
Run Code Online (Sandbox Code Playgroud)

现在,在任何需要使用可选选项(例如登录控制器)的地方,您都可以执行以下操作...

LdapLoginController.cs

[ApiController]
[Route("/api/login/ldap")]
public class LdapLoginController : ControllerBase
{
    private readonly Optional<LdapOptions> ldapOptions;

    public LdapLoginController(IOptionsSnapshot<Optional<LdapOptions>> ldapOptions)
    {
        // data annotations should trigger here and possibly throw an OptionsValidationException
        this.ldapOptions = ldapOptions.Value;
    }

    [HttpPost]
    public void Post(...)
    {
        if (!ldapOptions.Value.HasValue)
        {
            // a missing section is valid, but indicates that this option was not configured; I figure that relates to a 501 Not Implemented
            return StatusCode((int)HttpStatusCode.NotImplemented);
        }

        // else we can proceed with valid options
    }
}
Run Code Online (Sandbox Code Playgroud)