C# 8.0 不可为空的引用类型和选项模式

Posted

技术标签:

【中文标题】C# 8.0 不可为空的引用类型和选项模式【英文标题】:C# 8.0 non-nullable reference types and options pattern 【发布时间】:2020-08-08 18:30:26 【问题描述】:

Tl;dr:我想要一个选项类,它对其成员使用不可为空的类型,没有默认值。

C# 8.0 引入Nullable Reference Types.

我发现在 ASP.Net Options Pattern 中使用可为空的引用类型相当困难、不完整,或者我遗漏了一些东西。我遇到了stack over flow post 中描述的相同问题。

    我们不想让 Name 可以为空,因为我们需要在任何地方进行传统的空检查(这违背了不可为空引用类型的目的) 我们无法创建构造函数来强制使用不可为空的名称值创建 MyOptions 类,因为 Configure 方法为我们构造选项实例 我们不能使用 null-forgiving 运算符技巧 (public string name get; set; = null!;),因为这样我们就不能确保 Name 属性已设置并且我们最终会在不期望出现这种情况的 Name 属性(在服务内部)

我想要一个对其成员使用不可为空类型且没有默认值的选项类。 该帖子中的答案最终还是使用可空类型(我试图避免)或默认值(我也试图避免)。

关于 options validation 的 cmets 提出了很好的观点并且看起来很有希望,但事实证明 Validate 方法仍然需要一个选项对象来验证,如果您已经必须将选项对象传递给它。

public ValidateOptionsResult Validate(string name, MyOptions options)
 // Pointless if MyOptions options is being passed in here

这是没有意义的,因为我已经确定强制使用所有不可为空的成员且没有默认值的选项类的唯一方法是拥有一个构造函数。以下面的代码示例为例。

namespace SenderServer.Options

    using System;
    using Microsoft.Extensions.Configuration;

    /// <summary>
    /// Configuration options for json web tokens.
    /// </summary>
    public class JwtOptions
    
        /// <summary>
        /// The secret used for signing the tokens.
        /// </summary>
        public String Secret  get; 

        /// <summary>
        /// The length of time in minutes tokens should last for.
        /// </summary>
        public Int32 TokenExpirationInMinutes  get; 

        /// <summary>
        /// Configuration options for json web tokens.
        /// </summary>
        /// <param name="secret"> The secret used for signing the tokens.</param>
        /// <param name="tokenExpirationInMinutes">The length of time in minutes tokens should last for.</param>
        public JwtOptions(String secret, Int32 tokenExpirationInMinutes)
        
            Secret = secret;
            TokenExpirationInMinutes = tokenExpirationInMinutes;
        

        /// <summary>
        /// Create a JwtOptions instance from a configuration section.
        /// </summary>
        /// <param name="jwtConfiguration">The configuration section.</param>
        /// <returns>A validated JwtOptions instance.</returns>
        public static JwtOptions FromConfiguration(IConfiguration jwtConfiguration)
        
            // Validate the secret
            String? secret = jwtConfiguration[nameof(Secret)];
            if (secret == null)
            
                throw new ArgumentNullException(nameof(Secret));
            

            // Validate the expiration length
            if (!Int32.TryParse(jwtConfiguration[nameof(TokenExpirationInMinutes)], out Int32 tokenExpirationInMinutes))
            
                throw new ArgumentNullException(nameof(TokenExpirationInMinutes));
            

            if (tokenExpirationInMinutes < 0)
            
                throw new ArgumentOutOfRangeException(nameof(TokenExpirationInMinutes));
            

            return new JwtOptions(secret, tokenExpirationInMinutes);
        
    

所以如果我需要一个带有类参数的构造函数,那么我可以自己实例化它:

// Configure the JWT options
IConfiguration jwtConfiguration = Configuration.GetSection("Authentication:JwtOptions");
JwtOptions jwtOptions = JwtOptions.FromConfiguration(jwtConfiguration); // This performs validation as well

但是我应该把jwtOptions 放在哪里呢? services.Configure&lt;JwtOptions&gt;(jwtOptions); 和变体中没有一个只接收一个已经实例化的对象(或者至少没有我见过的)。最后,即使他们这样做了,您也不能使用没有公共无参数构造函数的依赖注入选项类。

public JwtService(IOptions<JwtOptions> jwtOptions)

【问题讨论】:

您不能在FromConfiguration 方法中手动执行验证,然后立即调用services.Configure&lt;JwtOptions&gt;(jwtConfiguration) 吗?无论如何,内置选项验证并不急切 (github.com/dotnet/extensions/issues/459)。 【参考方案1】:

我想要一个选项类,它对其成员使用不可为空的类型,没有默认值。

那么不幸的是,Microsoft.Extensions.Options 根本不适合您。 Options 的工作方式是拥有一个由多个源、操作和验证器组成的配置管道,它们都使用相同的选项对象。由于该管道没有明确的开始,并且任何配置源都可以位于管道中的任何位置,因此选项对象的 构造 由框架处理,并且位于任何配置源之前调用。

这对于 Options 允许其具有的不同类型的用例是绝对必要的:您可以从配置 (Microsoft.Extensions.Configuration) 配置选项,您可以通过配置操作配置它们,您可以通过服务配置它们有额外的依赖等。所有这些都可以按任何顺序运行。

因此,由于对象的构造是由框架进行的,因此还需要创建选项对象的默认值:通常,这些只是类型的 default 值,但您也可以通过对象的构造函数。

如果您想强制在管道之后配置特定参数,您可以使用配置后操作来强制配置,或使用选项验证来验证配置的选项。但是由于这一切都在管道中运行,因此您需要有默认值。

因此,基本上,如果您需要没有默认值的不可为空的属性,那么您不能使用选项。至少不是开箱即用。如果您想这样做是为了安全地引用服务中的选项,那么将有一种不同的方法来解决这个问题:而不是注入 IOptions&lt;T&gt;,而是直接注入一个不可为空的选项对象 T。并通过工厂提供:

services.AddSingleton<MySafeOptions>(sp =>

    var options = sp.GetService<IOptions<MyUnsafeOptions>>();
    return new MySafeOptions(options.Value);
);
services.Configure<MyUnsafeOptions>(Configuration.GetSection("MyOptions"));

【讨论】:

【参考方案2】:

基于@poke 的答案构建的另一个选项是将IConfiguration 传递给您的单身人士并直接使用ConfigurationBinder.Bind。如果您添加正确的属性,您不再需要将选项对象传递给您的单例。所以有了这样的课程:

public class JwtConfiguration

    public JwtConfiguration(IConfiguration configuration)
    
        ConfigurationBinder.Bind(configuration, this);

        // ensure the fields are not null so that the attributes are not
        // a lie
        _ = this.Secret ?? throw new ArgumentException(
            $"nameof(this.Secret) required",
            nameof(configuration));
        _ = this.TokenExpirationInMinutes ?? throw new ArgumentException(
            $"nameof(this.TokenExpirationInMinutes) required",
            nameof(configuration));
    

    [DisallowNull]
    [NotNull]
    public string? Secret  get; set; 

    [DisallowNull]
    [NotNull]
    public int32? TokenExpirationInMinutes  get; set; 

然后将其连接在一起:

            .ConfigureServices(
                (context, services) => services
                    .AddSingleton<JwtConfiguration>(
                        (service) => new JwtConfiguration(
                            context.Configuration.GetSection("JwtConfig")))
                    .AddSingleton<JwtService, JwtService>());

然后消费:

    public class JwtService
    
        public JwtService(JwtConfiguration configuration)
        

【讨论】:

以上是关于C# 8.0 不可为空的引用类型和选项模式的主要内容,如果未能解决你的问题,请参考以下文章

可空引用类型和选项模式

C# 8.0 可空(Nullable)给ASP.NET Core带来的坑

C# 8.0 正式发布:Visual Studio 2019 支持所有新功能

C# 8.0 和可为空引用类型

c#静态类中的8个可为空的引用类型

.NET 使用可为空的引用类型实现 IEnumerator