Search code examples
asp.net-corevalidationasp.net-core-mvcasp.net-core-8

ASP.NET Core 8 MVC model validation - [Required] vs nullable type indicator


I'm trying to understand how model validation works as I finalize the design on my website using ASP.NET Core 8 MVC in Visual Studio. I've read several tutorials and posts on here, but the behavior I'm seeing doesn't seem to match what I was expecting.

using System.ComponentModel.DataAnnotations;

namespace mywebsite.Models
{
    public class TestAModel
    {
        //[Required]
        //public int nonnullableint {  get; set; }
        //public int? nullableint {  get; set; }
        //public string? nullablestring { get; set; }
        //public string nonnullablestring { get; set; }
        //public TestCModel? nullablenonrequiredmodel { get; set; }
        public TestCModel nonnullablenonrequiredmodel { get; set; }
        //public TestBModel? nullablerequiredmodel { get; set; }
        //public TestBModel nonnullablerequiredmodel { get; set; }
        //public List<TestBModel>? nullablerequiredmodellist { get; set; }
        //public List<TestBModel> nonnullablerequiredmodellist { get; set; }
        //public List<TestCModel>? nullablenonrequiredmodellist { get; set; }
        //public List<TestCModel> nonnullablenonrequiredmodellist { get; set; }
    }
}
namespace mywebsite.Models
{
    public class TestCModel
    {
        public int? notrequiredint { get; set; }
    }
}

HomeController's Index() action method:

   public IActionResult Index()
   {
       TestAModel model = new TestAModel();

       //model.nonnullablenonrequiredmodel = new TestCModel();
       var validate = TryValidateModel(model);

       if (ModelState.IsValid)
       {
           var variabletest = 1;
       }

       return View();   // breakpoint here to view results
   }

What I've found, using TestCModel as an example, basically is this:

public class TestAModel
{
    //[Required]
    public TestCModel? nonnullablenonrequiredmodel { get; set; }
}

public IActionResult Index()
{
    TestAModel model = new TestAModel();
    //model.nonnullablenonrequiredmodel = new TestCModel();
    var validate = TryValidateModel(model);

    if (ModelState.IsValid)
    {
        var variabletest = 1;
    }

    return View();
}

This results in validation = true.

public class TestAModel
{
    [Required]
    public TestCModel? nonnullablenonrequiredmodel { get; set; }
}

public IActionResult Index()
{
    TestAModel model = new TestAModel();
    //model.nonnullablenonrequiredmodel = new TestCModel();

    var validate = TryValidateModel(model);

    if (ModelState.IsValid)
    {
        var variabletest = 1;
    }

    return View();
}

This results in validation = false.

public class TestAModel
{
    [Required]
    public TestCModel? nonnullablenonrequiredmodel { get; set; }
}

public IActionResult Index()
{
    TestAModel model = new TestAModel();
    model.nonnullablenonrequiredmodel = new TestCModel();

    var validate = TryValidateModel(model);

    if (ModelState.IsValid)
    {
        var variabletest = 1;
    }

    return View();
}

This again results in validation = true.

All of which makes sense to me, except then when I did this:

public class TestAModel
{
    //[Required]
    public TestCModel nonnullablenonrequiredmodel { get; set; }
}

public IActionResult Index()
{
    TestAModel model = new TestAModel();
    //model.nonnullablenonrequiredmodel = new TestCModel();

    var validate = TryValidateModel(model);

    if (ModelState.IsValid) 
    {
        var variabletest = 1;
    }

    return View();
}

I got validation = false!

I don't understand this because aren't classes inherently nullable? I also don't understand this, because it seems to limit the value of the [Required] tag. What's the point of it if something is required simply by leaving it non-nullable? I had the same results with string types as well.

The only thing interesting was int types, because the non-nullable int defaulted to a value = 0, so the way to make it required was [Required] and nullable.

Am I missing something super basic? Please help.


Solution

  • As this document said:

    Beginning with .NET 6, new projects include the <Nullable>enable</Nullable> element in the project file. Once the feature is turned on, existing reference variable declarations become non-nullable reference types.

    This is by design that the non-nullable property must be required with non-null value, otherwise the ModelState will be invalid.

    For int type property, it contains default value 0 although you do not submit it with value so that the validation will always succeed. If you send the value with null, it will fail for model validation.

    For the string type property,the default value is null. That is to say it does not contain non-null value by default, so it will fail to validate. But if you give a non-null value like: public string Name { get; set; } = string.Empty;, it will pass the validation although you do not submit the value for it.