如果没有公共默认构造函数,ASP.NET Core将使用非默认构造函数。

3

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

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

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的内部服务器错误。
我期望使用私有无参构造函数和私有setters。然后,由于控制器标记了ApiController属性,此模型将自动根据数据注释进行验证,并导致400 Bad Request,因为MyProperty是必需的。
有趣的是-如果我将默认构造函数设置为public,则它将按预期工作,但我不想这样做。
为什么它不使用私有构造函数,如何在不将其标记为public的情况下使用它?
另一个问题是,模型绑定器是否了解如何使用具有参数的构造函数来使用反射?

为什么您假设它首先使用私有构造函数?我的假设是,如果存在公共构造函数,则调用公共构造函数,然后查找私有构造函数。但我建议您不要将此类用于太多事情。也许您需要一个不同的类。 - DavidG
2
你还没有告诉序列化程序(JSON.NET)你想要使用默认构造函数,所以它使用最匹配输入 JSON 的构造函数。 - Panagiotis Kanavos
1
实际上,为什么会有两个构造函数?您是否尝试将同一类用于不同的目的,例如既作为REST有效载荷,又作为其他工作的模型类?这可能会导致很多问题,因为REST API的关注点和限制与域类、ORM DTO或用于与其他服务通信的类非常不同。 - Panagiotis Kanavos
1
“记住,在设计WebAPI时,你的数据模型不是你的对象模型,也不是你的资源模型,更不是你的消息模型。” - Panagiotis Kanavos
1
@YeldarKurmangaliyev,你说得对,我的评论有些不当,可能是因为其他事情让我心情不好。我向你道歉并已将其删除。这个问题和答案实际上非常相关,因为我也不知道JSON.NET会使用参数化构造函数进行反序列化!所以今天我学到了一些东西,我感谢你。 - Ian Kemp
显示剩余6条评论
2个回答

3
感谢Panagiotis Kanavos指出了ASP.NET Core中使用了Json.NET序列化程序。
这使我想到了Json.NET文档中的ConstructorHandling设置

行为原因

文档指定如下:

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

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

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

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

在一个类中使用私有默认构造函数而不是公共参数化构造函数

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反序列化程序将使用明确定义的构造函数。
使用私有默认构造函数而不是公共参数化构造函数(服务)
另一种方法是将JsonSerializerSettings属性的ConstructionHandling更改为使用AllowNonPublicDefaultConstructor:
ConstructionHandling.AllowNonPublicDefaultConstructor:Json.NET会在回退到参数化构造函数之前使用非公共默认构造函数。
以下是如何在Startup.cs中完成此操作:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().AddJsonOptions(o => {
        o.SerializerSettings.ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor;
    });
}

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

请求模型中的参数化构造函数可能会导致代码问题

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

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

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


顺便提一下,拥有参数化模型构造函数的一个原因是可空引用类型。 - JsCoder
2
请求模型中的有参数构造函数可能会是代码异味 - 我非常不同意。对我来说,无参构造函数总是代码异味。这意味着对象可以被构造成无效状态。我不想要没有必要字段的对象,几乎从来没有好的理由。 - cdimitroulas

-1

你想要的不太合逻辑。private 成员,包括构造函数是无法从外部访问的。

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

绑定模型,控制器只有一种方法-调用每个参数都设置为null的公共c-tor。

要使模型绑定成为可能,该类必须具有公共默认构造函数和可公开写入的属性以进行绑定。当发生模型绑定时,使用公共默认构造函数实例化类,然后可以设置属性。

因此,要成功地绑定,您应该:

  1. 具有公共默认c-tor
  2. 为属性具有公共setter。

2
私有构造函数和成员是反序列化的默认实践。反序列化器能够使用它们。 - Yeldar Kurmangaliyev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接