C# 8.0 不可空引用类型和选项模式
C# 8.0 non-nullable reference types and options pattern
Tl;dr: 我想要一个选项 class,它的成员使用不可为 null 的类型,没有默认值。
C# 8.0 引入了Nullable Reference Types.
我发现将可空引用类型与 ASP.Net Options Pattern is rather difficult, incomplete, or that I am missing something. I am experiencing the same issue described in this stack over flow post 一起使用。
- We don't want to make Name nullable as then we need to place traditional null checks everywhere (which is against the purpose of non-nullable reference types)
- We can't create a constructor to enforce the MyOptions class to be created with a non-nullable name value as the Configure method construct the options instance for us
- We can't use the null-forgiving operator trick (public string name { get; set; } = null!;) as then we can't ensure the Name property is set and we can end up with a null in the Name property where this would not be expected (inside the services)
我想要一个选项 class,它的成员使用不可为 null 的类型,没有默认值。 post 中的答案最终使用 nullable无论如何类型(我试图避免)或默认值(我也试图避免)。
关于 options validation 的评论提出了很好的观点并且看起来很有希望,但事实证明 Validate
方法仍然需要一个选项对象来验证,如果你已经有,这就违背了目的将选项对象传递给它。
public ValidateOptionsResult Validate(string name, MyOptions options)
// Pointless if MyOptions options is being passed in here
这是毫无意义的,因为我已经确定对所有不可为 null 的成员强制执行选项 class 并且没有默认值的唯一方法是拥有一个构造函数。以下面的代码示例为例。
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);
}
}
}
因此,如果我需要一个带有class参数的构造函数,那么我可以自己实例化它,例如:
// Configure the JWT options
IConfiguration jwtConfiguration = Configuration.GetSection("Authentication:JwtOptions");
JwtOptions jwtOptions = JwtOptions.FromConfiguration(jwtConfiguration); // This performs validation as well
但是我应该把 jwtOptions
放在哪里? services.Configure<JwtOptions>(jwtOptions);
和变体的 None 只接受一个已经实例化的对象(或者至少 none 我见过)。最后,即使他们这样做了,你也不能使用没有 public 无参数构造函数的依赖注入选项 class。
public JwtService(IOptions<JwtOptions> jwtOptions)
I want an options class that uses non-nullable types for its members with no defaults.
那么不幸的是,Microsoft.Extensions.Options 根本不适合你。 Options 的工作方式是拥有一个包含多个源、操作和验证器的配置管道,它们都使用相同的 options 对象。由于此管道没有明确的开始,并且任何配置源都可以位于管道中的任何位置,因此选项对象的 构造 由框架处理,并且出现在任何调用配置源。
这是绝对必要的,以便 Options 允许它具有的不同类型的用例:您可以从配置中配置选项 (Microsoft.Extensions.Configuration),您可以通过配置操作配置它们,您可以配置它们通过具有额外依赖性的服务等。所有这些都可以 运行 以任何顺序。
因此,由于对象的构造是由框架完成的,因此还需要有默认值来创建选项对象:通常,这些只是类型的 default
值,但您也可以选择不同的值默认通过对象的构造函数。
如果您想在管道之后强制配置特定参数,您可以使用 post-configure 操作来强制配置,或使用选项验证来验证配置的选项。但是由于所有 运行 都在管道中,因此您需要有默认值。
所以基本上,如果您需要具有没有默认值的不可为 null 的属性,那么您不能使用 Options。至少不是开箱即用。如果你想这样做是为了安全地引用你的服务中的选项,那么会有一种不同的方法来解决这个问题:不是注入 IOptions<T>
,而是直接注入一个不可空的选项对象 T
.并通过工厂提供:
services.AddSingleton<MySafeOptions>(sp =>
{
var options = sp.GetService<IOptions<MyUnsafeOptions>>();
return new MySafeOptions(options.Value);
});
services.Configure<MyUnsafeOptions>(Configuration.GetSection("MyOptions"));
另一个基于@poke 答案的选项是将 IConfiguration
传递给您的单例并直接使用 ConfigurationBinder.Bind
。如果你添加了正确的属性,你就不再需要一个选项对象来传递给你的单例。所以像这样的 class:
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)
{
Tl;dr: 我想要一个选项 class,它的成员使用不可为 null 的类型,没有默认值。
C# 8.0 引入了Nullable Reference Types.
我发现将可空引用类型与 ASP.Net Options Pattern is rather difficult, incomplete, or that I am missing something. I am experiencing the same issue described in this stack over flow post 一起使用。
- We don't want to make Name nullable as then we need to place traditional null checks everywhere (which is against the purpose of non-nullable reference types)
- We can't create a constructor to enforce the MyOptions class to be created with a non-nullable name value as the Configure method construct the options instance for us
- We can't use the null-forgiving operator trick (public string name { get; set; } = null!;) as then we can't ensure the Name property is set and we can end up with a null in the Name property where this would not be expected (inside the services)
我想要一个选项 class,它的成员使用不可为 null 的类型,没有默认值。 post 中的答案最终使用 nullable无论如何类型(我试图避免)或默认值(我也试图避免)。
关于 options validation 的评论提出了很好的观点并且看起来很有希望,但事实证明 Validate
方法仍然需要一个选项对象来验证,如果你已经有,这就违背了目的将选项对象传递给它。
public ValidateOptionsResult Validate(string name, MyOptions options)
// Pointless if MyOptions options is being passed in here
这是毫无意义的,因为我已经确定对所有不可为 null 的成员强制执行选项 class 并且没有默认值的唯一方法是拥有一个构造函数。以下面的代码示例为例。
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);
}
}
}
因此,如果我需要一个带有class参数的构造函数,那么我可以自己实例化它,例如:
// Configure the JWT options
IConfiguration jwtConfiguration = Configuration.GetSection("Authentication:JwtOptions");
JwtOptions jwtOptions = JwtOptions.FromConfiguration(jwtConfiguration); // This performs validation as well
但是我应该把 jwtOptions
放在哪里? services.Configure<JwtOptions>(jwtOptions);
和变体的 None 只接受一个已经实例化的对象(或者至少 none 我见过)。最后,即使他们这样做了,你也不能使用没有 public 无参数构造函数的依赖注入选项 class。
public JwtService(IOptions<JwtOptions> jwtOptions)
I want an options class that uses non-nullable types for its members with no defaults.
那么不幸的是,Microsoft.Extensions.Options 根本不适合你。 Options 的工作方式是拥有一个包含多个源、操作和验证器的配置管道,它们都使用相同的 options 对象。由于此管道没有明确的开始,并且任何配置源都可以位于管道中的任何位置,因此选项对象的 构造 由框架处理,并且出现在任何调用配置源。
这是绝对必要的,以便 Options 允许它具有的不同类型的用例:您可以从配置中配置选项 (Microsoft.Extensions.Configuration),您可以通过配置操作配置它们,您可以配置它们通过具有额外依赖性的服务等。所有这些都可以 运行 以任何顺序。
因此,由于对象的构造是由框架完成的,因此还需要有默认值来创建选项对象:通常,这些只是类型的 default
值,但您也可以选择不同的值默认通过对象的构造函数。
如果您想在管道之后强制配置特定参数,您可以使用 post-configure 操作来强制配置,或使用选项验证来验证配置的选项。但是由于所有 运行 都在管道中,因此您需要有默认值。
所以基本上,如果您需要具有没有默认值的不可为 null 的属性,那么您不能使用 Options。至少不是开箱即用。如果你想这样做是为了安全地引用你的服务中的选项,那么会有一种不同的方法来解决这个问题:不是注入 IOptions<T>
,而是直接注入一个不可空的选项对象 T
.并通过工厂提供:
services.AddSingleton<MySafeOptions>(sp =>
{
var options = sp.GetService<IOptions<MyUnsafeOptions>>();
return new MySafeOptions(options.Value);
});
services.Configure<MyUnsafeOptions>(Configuration.GetSection("MyOptions"));
另一个基于@poke 答案的选项是将 IConfiguration
传递给您的单例并直接使用 ConfigurationBinder.Bind
。如果你添加了正确的属性,你就不再需要一个选项对象来传递给你的单例。所以像这样的 class:
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)
{