Skip to content

Conversation

@roji
Copy link
Member

@roji roji commented Jul 19, 2020

By default, nullability warnings/errors won't appear - use this build hack to temporarily target .NET Core and generate warnings.

@roji roji requested review from ajcvickers and stephentoub July 19, 2020 23:30
/// <param name="validatingAttribute">The attribute that triggered this exception</param>
/// <param name="value">The value that caused the validating attribute to trigger the exception</param>
public ValidationException(string errorMessage, ValidationAttribute validatingAttribute, object value)
public ValidationException(string? errorMessage, ValidationAttribute? validatingAttribute, object? value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was tempted to suggest trying to make ValidationAttribute not nullable here since I don't think we'd have any uses ourselves where it would be null, but with it being a public constructor and with no references to it other than the property setter, this is the right call.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed... I'm generally trying to be as conservative as possible - if there's a public code path which results in something being nullable (and in the type being usable), I tend to mark it nullable, even if it doesn't make a lot of sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to consider: for arguments we can always relax the constraint and allow null in vNext (and user have an option to pass it anyway with !), if we mark it as nullable then constraining it will be a breaking change for vNext (for return values relaxation would be making it non-nullable)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@krwq agreed about the general logic, but do you feel that going non-nullable is the right choice for this specific case? For these cases where we're annotating legacy APIs with a very long history, there's non-negligible chance of users using these APIs with null, and if the implementation supports that correctly, my tendency is to annotate the actual, supported state of affairs (at least that's the guidance I've received).

Another point is that an overly-relaxed annotation of a parameter as nullable only has the consequence of not flagging a warning in user code which passes null; again, as long as the API handles that correctly that may not be a big concern.

@ghost
Copy link

ghost commented Jul 20, 2020

Tagging subscribers to this area: @ajcvickers
Notify danmosemsft if you want to be subscribed.


// TODO: Enable after System.ComponentModel.TypeDescriptionProvider is annotated
#nullable disable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't put a comment there but in here:
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)

should object be nullable (I'm not familiar with the API just shape and name of the API suggest like there should be no issue with providing a type but not providing instance and possibly also reverse)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am also not familiar with this API, so am waiting for it to be annotated by someone who hopefully does :)


// TODO: Enable after System.ComponentModel.TypeDescriptionProvider is annotated
#nullable disable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should AssociatedMetadataType be nullable? I'm seeing null check in GetAttributes()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely seems so - I left AssociatedMetadataTypeTypeDescriptor and AssociatedMetadataTypeTypeDescriptionProvider unannotated (and disabled) because of the dependency on System.ComponentModel.TypeDescriptionProvider. The idea is to return to these soon once that's done.

public bool ConvertValueInInvariantCulture { get; set; }

private Func<object, object> Conversion { get; set; }
private Func<object, object>? Conversion { get; set; }
Copy link
Member

@krwq krwq Jul 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be Func<object?, object?>? (with NotNullWhenNotNull or similar)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conversion function isn't user-provided - it's generated internally in SetupConversion based on user-provided minimum and maximum values, and except for the int/double special cases (where there's no null anyway), it will NRE on null inputs (it calls GetType in inputs to see if it needs to actual perform conversions). It's "protected" against this in IsValid, where value is checked for null explicitly, and invocation is skipped entirely - so the implementation never seems to pass null to the function. So it seems right for the conversion parameter to be non-nullable.

However, it's technically possible (though probably weird) for the user-provided conversion function to return null, am changing that.

Re NotNullWhenNotNull, I don't see anything explicit showing a relationship between the nullability of the input and the output of the conversion function - the user could conceivably decide to convert empty strings to null, for example, for the purpose of this attribute.

Let me know what you think of the above.

// Lazy load the dictionary. It's fine if this method executes multiple times in stress scenarios.
// If the method throws (indicating that the input params are invalid) this property will throw
// every time it's accessed.
public IDictionary<string, object> ControlParameters =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc: @GrabYourPitchforks for GetHashCode below

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anything in particular we need to do here?

private string _errorMessageResourceName;
private Type _errorMessageResourceType;
private string? _errorMessage;
private Func<string>? _errorMessageResourceAccessor;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might make sense to just mark it as non-nullable and = null! to suppress warnings

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible, but as it's late-initialized in at least some scenarios (via SetupResourceAccessor, where there's an actual null check) it makes a bit more sense to me to annotate it as nullable... Let me know if you feel otherwise.

@roji roji requested a review from krwq July 27, 2020 04:52
Copy link
Member

@jeffhandley jeffhandley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a review comment where you had a code comment suggesting something might be a big, but it was by design. Theoretically a big exists there, but it's intentional and maybe could use a different comment to reflect that.

Other than updating that comment appropriately, approved.

Thanks!!

@roji roji force-pushed the DataAnnotationsNullability branch 2 times, most recently from fc0934a to 55fbb76 Compare July 28, 2020 09:15
@roji roji force-pushed the DataAnnotationsNullability branch from 55fbb76 to 54f4d30 Compare July 28, 2020 09:29
@roji
Copy link
Member Author

roji commented Jul 28, 2020

Remaining build failures appear to be unrelated MacOS infrastructure-related, merging.

@roji roji merged commit a9009db into dotnet:master Jul 28, 2020
Jacksondr5 pushed a commit to Jacksondr5/runtime that referenced this pull request Aug 10, 2020
@karelz karelz added this to the 5.0.0 milestone Aug 18, 2020
@pentp
Copy link
Contributor

pentp commented Sep 18, 2020

ValidationAttribute.IsValid should allow null returns because ValidationResult.Success is null (and correctly annotated).

@stephentoub
Copy link
Member

@jeffhandley?

@roji
Copy link
Member Author

roji commented Sep 18, 2020

This is a discrepancy between ref and src that has already been fixed in master (#41731), but apparently not yet in release/5.0.0-rc2... am not sure what the backporting status of this... /cc @buyaa-n @jeffhandley

@jeffhandley
Copy link
Member

The RC2 port PR (#41845) was already merged. I'm checking to see if we somehow missed some commits in the port.

@jeffhandley
Copy link
Member

The port PR has everything that #41731 had, and I see the ref assembly source contains ValidationResult? IsValid.

https://github.com/dotnet/runtime/blob/release/5.0-rc2/src/libraries/System.ComponentModel.Annotations/ref/System.ComponentModel.Annotations.cs#L295

    public abstract partial class ValidationAttribute : System.Attribute
    {
        protected ValidationAttribute() { }
        protected ValidationAttribute(System.Func<string> errorMessageAccessor) { }
        protected ValidationAttribute(string errorMessage) { }
        public string? ErrorMessage { get { throw null; } set { } }
        public string? ErrorMessageResourceName { get { throw null; } set { } }
        public System.Type? ErrorMessageResourceType { get { throw null; } set { } }
        protected string ErrorMessageString { get { throw null; } }
        public virtual bool RequiresValidationContext { get { throw null; } }
        public virtual string FormatErrorMessage(string name) { throw null; }
        public System.ComponentModel.DataAnnotations.ValidationResult? GetValidationResult(object? value, System.ComponentModel.DataAnnotations.ValidationContext validationContext) { throw null; }
        public virtual bool IsValid(object? value) { throw null; }
        protected virtual System.ComponentModel.DataAnnotations.ValidationResult? IsValid(object? value, System.ComponentModel.DataAnnotations.ValidationContext validationContext) { throw null; }
        public void Validate(object? value, System.ComponentModel.DataAnnotations.ValidationContext validationContext) { }
        public void Validate(object? value, string name) { }
    }

@pentp Can you let me know where you were seeing that ValidationResult.IsValid wasn't allowing null?

@pentp
Copy link
Contributor

pentp commented Sep 18, 2020

I was testing out one of our applications on RC1, so that's why I noticed it. Good to know that it's already fixed in RC2.

@jeffhandley
Copy link
Member

@pentp Thank you! And thanks for reporting it. RC2 builds are available here if you're interested in verifying that you see the expected behavior with RC2.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants