0

The following code in C# will throw an InvalidOperationException because it's not allowed to access Value when it is not set.

int? a = null;
Console.Writeline(a.Value);

I have the following code in AspNet.Core:

public class Request
{
    [Range(10, 20]
    public int? Field1 {get; set;}

    [Range(10, 20]
    public MyStruct Field2 {get; set;}
}

public struct MyStruct
{
    public int Value => throw new Exception();
}

When model validation happens framework just throws an exception because it tries to read all the properties of MyStruct and it obviously cannot read Value. But if I have only nullable field, validation works just fine even though Value there throws an exception as well.

Is there some magic that is just hardcoded to not do that for nullable or is there some way I can have the same behaviour in my code? I.e. I want the validation to not throw an exception for my class.

I have a suspicion is that this is either a hardcoded check or it's some syntactic sugar that makes Nullable struct possible to assign and compare to null.

Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207

1 Answers1

1

If you take a look at the reference source for the RangeAttribute, you'll see this at the beginning of the IsValid method:

// Automatically pass if value is null or empty. RequiredAttribute should be used to assert a value is not empty.
if (value == null) {
    return true;
}

So this is by design. As the comment suggests, if you have a nullable type and want to confirm there is a value, you should use the RequiredAttribute.

public class Request
{
    [Required]
    [Range(10, 20]
    public int? Field1 {get; set;}

    [Range(10, 20]
    public MyStruct Field2 {get; set;}
}

With Nullable<T> the rules are a bit different for how it's properties work.

Properties on the Nullable<T> type work even if the instance is null, which would normally throw on other types.

int? val = null;
Console.WriteLine(val.HasValue); // No NullReferenceException
Console.WriteLine(val == null); // True
Jonathon Chase
  • 9,396
  • 21
  • 39
  • Well, Value does in fact throw an exception if it is null, and for some reason it does not throw. It is not the attribute itself actually that does that, but the validation logic that goes through all fields of the validateable object – Ilya Chernomordik Nov 07 '19 at 15:38
  • @IlyaChernomordik The `Value` property is never actually called by the validator. `Nullable`'s equality checks if it `HasValue`, and if it doesn't, it returns the result of `other == null`. If it's comparing to null, it returns true. – Jonathon Chase Nov 07 '19 at 15:42
  • Yes, that's what I suspected, can you point me to where in Nullable it does happen? It seems it is harwired into the compiler itself – Ilya Chernomordik Nov 07 '19 at 15:43
  • @IlyaChernomordik [Nullable's Equals Overload](https://referencesource.microsoft.com/#mscorlib/system/nullable.cs,64) – Jonathon Chase Nov 07 '19 at 15:44
  • I tried to check == operator overload instead, not sure actually that it will work with other types, but let me check – Ilya Chernomordik Nov 07 '19 at 15:53
  • Unfortunately Equals does not get called for my struct. I assume this is because nullable has a special compiler trick for == operator and comparison to null – Ilya Chernomordik Nov 07 '19 at 15:55
  • @IlyaChernomordik Does the actual implementation of your struct have validation attributes on it's properties as well? Is there some kind of implicit conversion occuring? I wouldn't expect the RangeAttribute to actually call the Value property of the struct when validating. – Jonathon Chase Nov 07 '19 at 16:04
  • No, nothing like that. No attributes, no implicit conversion (though I tried with it as well). It is not the Range attribute itself, this is just an example, I have my own attribute that derives from ValidationAttribute, but it actually is never called, because the error happens in some framework code that decides to scan through all properties for some reason. And for some other reason it does not do that for Nullable – Ilya Chernomordik Nov 07 '19 at 16:07