1

When using DataAnnotations to validate models for incoming controller requests, non-nullable reference types are implicitly treated as required unless you declare them as nullable (i.e. string? instead of string).

public class MyExampleModel
{
    // Cannot be null! Equivalent to having [Required(AllowEmptyStrings = true)] on it
    public string PropertyName { get; set; } 

    // Allowed to be null! No validation errors occur.
    public string? OtherProp { get; set; } 
}

This behavior results in an expected validation error of The PropertyName field is required when hitting the endpoint via Postman / etc.

However, when using the Validator class in a unit testing scenario, this implicit check is not correctly reported when passing null for the string PropertyName property.

using System.ComponentModel.DataAnnotations;
using FluentAssertions;
using Xunit;

namespace MyNamespace.Tests;

public class TestingValidationOfMyExampleModel
{
    [Fact]
    public void ShouldHaveErrorWhenPropertyNameIsNull()
    {
        var model = new MyExampleModel(); // model.PropertyName is null.

        var validationResults = new List<ValidationResult>();
        var validationContext = new ValidationContext(model, null, null);

        // No errors returned! validationResults remains empty.
        Validator.TryValidateObject(model, validationContext, validationResults, true);

        validationResults.Should().HaveCount(1); // Fails!
    }
}

Is there some way to configure the static System.ComponentModel.DataAnnotations.Validator class so that it also makes this implicit check?

Doctor Blue
  • 3,769
  • 6
  • 40
  • 63

1 Answers1

2

The DataAnnotations.Validator will validate exclusively based on the applied data annotation attributes. It's has been around for a very long time, and it's heavily used by various frameworks/tools (e.f. EF, Winforms, MVC). Modifying the behavior would break everything.

On the other hand, the ASP.NET Core team decided to add support for NRT in the MVC's model binding and validation middleware. They did it by just adding the Required attributes to the non-nullable reference types. You can find the implementation here DataAnnotationsMetadataProvider. Here is the section

...
if (addInferredRequiredAttribute)
{
    // Since this behavior specifically relates to non-null-ness, we will use the non-default
    // option to tolerate empty/whitespace strings. empty/whitespace INPUT will still result in
    // a validation error by default because we convert empty/whitespace strings to null
    // unless you say otherwise.
    requiredAttribute = new RequiredAttribute()
    {
        AllowEmptyStrings = true,
    };
    attributes.Add(requiredAttribute);
}
...

You can opt-out of this behavior while registering the controllers

builder.Services.AddControllers(options => options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true);

The ability to opt out makes it OK to have such inferring in MVC.

Fati Iseni
  • 311
  • 2
  • 5
  • Thanks for the response! I'd found out as much myself, but unfortunately my application requires it be left on. With it being on, and with me also wanting to isolate this behavior in unit testing, it looks like I'll have to simulate the inferred required type myself. Not the end of the world, but at least this confirms that the validator itself isn't magically adding it somehow. – Doctor Blue Dec 10 '22 at 17:04
  • 1
    I'd suggest you always add `Required` attributes explicitly to all non-nullable properties. It clutters the code, but it's more verbose and that would solve your issue in unit tests. – Fati Iseni Dec 10 '22 at 21:54
  • Accepted! Yeah there's a couple paths folks can take here: Either disable the inferring / explicitly use `[Required]`, or simulate the inferring in one's own unit tested (what I ultimately did). – Doctor Blue Dec 12 '22 at 14:41