ASP.NET 如果未找到 public 默认构造函数,则核心正在使用非默认构造函数

ASP.NET Core is using non-default constructor if no public default constructor found

我正在编写 ASP.NET Service Fabric 上托管的核心 2.2.0 应用程序。

我有一个 class 代表一个请求,我已经声明了两个构造函数:public 供我自己使用,私有的供序列化程序使用:

public class MyClass
{
    private MyClass() // for serializer
    {
    }

    public MyClass(string myProperty) // for myself
    {
        MyProperty = myProperty ?? throw new ArgumentNullException(nameof(myProperty));
    }

    [Required]
    public string MyProperty { get; private set; }
}

然后,我创建了一个 API 控制器:

[ApiController]
public class MyController
{
    [HttpPut]
    public async Task<IActionResult> Save([FromBody] MyClass model)
    {
        throw new NotImplementedException("Doesn't matter in this example");
    }
}

我通过使用 Fiddler 调用 null 值来测试它:

PUT /MyController (Content-Type: application/json)
{
    "MyProperty": null
}

我遇到的问题是我的public构造函数被调用时myProperty等于null,这导致抛出ArgumentNullException并导致 500 Internal Server Error。

我所期望的是它将使用私有无参数构造函数和私有设置器。然后,由于控制器标有 ApiController 属性,该模型将根据数据注释自动验证并导致 400 Bad Request,因为 MyProperty 是必需的。

有趣的是 - 如果我创建默认构造函数 public,那么它会按预期工作,但我不想这样做。

为什么它不使用私有构造函数,我如何在不将其标记为 public 的情况下使用它?

另一个问题是模型绑定器是否理解如何使用反射使用带参数的构造函数?

你想要的不符合逻辑。 private 成员,包括构造函数不是 accessible from outside

If a class has one or more private constructors and no public constructors, other classes (except nested classes) cannot create instances of this class.

To bind model,控制器只有一种方法 - 调用 public c-tor 并将每个参数设置为 null

To make model binding possible, the class must have a public default constructor and public writable properties to bind. When model binding occurs, the class is instantiated using the public default constructor, then the properties can be set.

所以要成功绑定你应该:

  1. 默认 public c-tor
  2. 有 public 个属性设置器。

感谢 Panagiotis Kanavos 指出 Json.NET 序列化程序用于 ASP.NET Core。
这导致我 ConstructorHandling setting in the Json.NET documentation.

行为原因

文档指定了以下内容:

ConstructionHandling.Default. First attempt to use the public default constructor, then fall back to a single parameterized constructor, then to the non-public default constructor.

即Json.NET按以下顺序搜索构造函数:

  • Public 默认构造函数
  • Public参数化构造函数
  • 私有默认构造函数

这就是参数化构造函数优于私有默认构造函数的原因。

使用私有默认构造函数而不是 public 参数化构造函数(一个 class)

JsonConstructorAttribute 可用于显式指定 Json.NET 反序列化器的构造函数:

using Newtonsoft.Json;

public class MyClass
{
    [JsonConstructor]
    private MyClass() // for serializer
    {
    }

    public MyClass(string myProperty) // for myself
    {
        MyProperty = myProperty ?? throw new ArgumentNullException(nameof(myProperty));
    }

    [Required]
    public string MyProperty { get; private set; }
}

现在 Json.NET 反序列化器将使用显式指定的构造函数。

使用私有默认构造函数而不是 public 参数化构造函数(服务)

另一种方法是将 ConstructionHandling of JsonSerializerSettings 属性 更改为使用 AllowNonPublicDefaultConstructor:

ConstructionHandling.AllowNonPublicDefaultConstructor: Json.NET will use a non-public default constructor before falling back to a parameterized constructor.

Startup.cs:

中可以这样做
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddJsonOptions(o => {
        o.SerializerSettings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
    });
}

这会将此逻辑应用于所有模型,反序列化器将始终优先选择私有默认构造函数,而不是所有模型的 public 参数化构造函数。

请求模型中的参数化构造函数可能是一种代码味道

在此特定示例中,提供了重现问题的代码。

在实际代码中,参数化或多个构造函数可能意味着您正在将 classes 用于多种目的,即域模型和请求模型。这最终会导致重用或支持此代码的问题。

具有 public 默认构造函数的 DTO 并且不应将逻辑用于请求以避免这些问题。