【问题标题】:ASP.NET Core is using non-default constructor if no public default constructor found如果未找到公共默认构造函数,则 ASP.NET Core 使用非默认构造函数
【发布时间】:2019-05-10 20:59:19
【问题描述】:

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

我有一个代表请求的类,并且我声明了两个构造函数:我自己使用的公共和序列化程序的私有:

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
}

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

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

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

为什么它不使用私有构造函数,我怎样才能让它使用它而不将它标记为公共?

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

【问题讨论】:

  • 你为什么假设它首先使用私有构造函数?我的假设是,如果它存在,它会调用一个公共的,然后寻找一个私有的构造函数。但是,我建议您将此类用于太多事情。也许你需要一个不同的班级。
  • 您没有告诉序列化程序 (JSON.NET) 您希望它使用默认构造函数,因此它使用与输入 JSON 最匹配的构造函数
  • 其实为什么会有两个构造函数呢?您是否尝试将同一类用于不同目的,即既作为 REST 有效负载又作为模型类用于其他工作?这可能会导致很多问题,因为 REST API 的关注点和限制与域类、ORM DTO 或用于与之通信的类的关注点和限制非常不同其他服务。
  • @YeldarKurmangaliyev 你对我的评论是对的,由于一些无关的事情我心情不好,我道歉并删除了它。这个问题和答案实际上非常相关,因为我也不知道 JSON.NET 将使用参数化构造函数进行反序列化!所以我今天学到了一些东西,我很感谢你。

标签: c# asp.net-core data-annotations model-binding


【解决方案1】:

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

行为原因

文档指定以下内容:

ConstructionHandling.Default.首先尝试使用公共默认构造函数,然后回退到单个参数化构造函数,然后使用非公共默认构造函数。

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

  • 公共默认构造函数
  • 公共参数化构造函数
  • 私有默认构造函数

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

使用私有默认ctor而不是公共参数化ctor(一个类)

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 反序列化器将使用明确指定的构造函数。

使用私有默认ctor而不是公共参数化ctor(服务)

另一种方法是将JsonSerializerSettingsConstructionHandling 属性更改为使用AllowNonPublicDefaultConstructor

ConstructionHandling.AllowNonPublicDefaultConstructor: Json.NET 将在回退到参数化构造函数之前使用非公共默认构造函数。

这是在Startup.cs中可以做到的:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddJsonOptions(o => {
        o.SerializerSettings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
    });
}

这会将这个逻辑应用于所有模型,并且反序列化器将始终更喜欢私有默认构造函数而不是所有模型的公共参数化构造函数。

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

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

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

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

【讨论】:

  • 顺便说一句,拥有参数化模型构造函数的一个原因是 Nullable ref types
  • “请求模型中的参数化构造函数可能是代码异味” - 我完全不同意。无参数构造函数对我来说总是一种代码味道。这意味着可以将对象构造为无效状态。我不想要没有必要字段的对象,这几乎没有充分的理由。
【解决方案2】:

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

如果一个类有一个或多个私有构造函数而没有公共构造函数,则其他类(嵌套类除外)无法创建该类的实例。

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

为了使模型绑定成为可能,类必须有一个公共的默认构造函数和公共的可写属性才能绑定。当模型绑定发生时,使用公共默认构造函数实例化类,然后可以设置属性。

所以要成功绑定你应该:

  1. 拥有公共默认 c-tor
  2. 拥有属性的公共设置器。

【讨论】:

  • 私有构造函数和成员是反序列化的默认做法。反序列化程序可以使用它们。
猜你喜欢
  • 2010-10-30
  • 2017-02-28
  • 1970-01-01
  • 2015-10-09
  • 2016-07-23
  • 1970-01-01
  • 2023-03-20
  • 1970-01-01
  • 2014-05-15
相关资源
最近更新 更多