简体   繁体   中英

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

I am writing ASP.NET Core 2.2.0 application hosted on Service Fabric.

I have a class which represents a request and I have declared two constructors: public for my own usage and private for serializers:

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; }
}

Then, I created an API controller:

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

And I test it by calling with null value with Fiddler:

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

The problem I encounter is that my public constructor is called with myProperty equal to null , which causes ArgumentNullException to be thrown and results into 500 Internal Server Error.

What I expected is that it will use private parameterless constructor and private setters. Then, since the controller is marked with ApiController attribute, this model will be validated automatically against data annotations and will result into 400 Bad Request, because MyProperty is required.

What is interesting - if I make the default constructor public, then it works as expected, but I wouldn't want to do this.

Why doesn't it use private constructor and how can I make it use it without marking it as public?

Another question is does model binder understand how to use constructor with parameters using reflection?

Thanks to Panagiotis Kanavos for pointing out that Json.NET serializer is used in ASP.NET Core.
This led me to ConstructorHandling setting in the Json.NET documentation .

Reason of the behaviour

The documentation specifies the following:

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.

That is Json.NET searches for the constructors in the following order:

  • Public default constructor
  • Public parameterized constructor
  • Private default constructor

And this is the reason why parameterized constructor is preferred over private default constructor.

Use private default ctor instead of public parameterized ctor (one class)

JsonConstructorAttribute can be used to explicitly specify constructor to Json.NET deserializer:

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; }
}

Now Json.NET deserializer will use the explicitly specified constructor.

Use private default ctor instead of public parameterized ctor (service)

Another way is to change ConstructionHandling of JsonSerializerSettings property to use AllowNonPublicDefaultConstructor :

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

This is how it can be done in the Startup.cs :

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

This will apply this logic to all models and the deserializer will always prefer private default constructor over public parameterized constructors for all models.

Parameterized constructor in request model can be a code smell

In this particular example, the code has been presented to reproduce the problem.

In real code, parameterized or multiple constructors can imply that you are using classes for multiple purposes, ie domain model and request model. This can eventually lead to problems with reusing or supporting this code.

DTOs with public default constructor and no logic should be used for requests to avoid these problems.

What you want is not logical. private members, including constructor are not 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 , controller has only one way - call public c-tor with every argument set to 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.

So to bind successfully you should:

  1. Have public default c-tor
  2. Have public setters for properties.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM