Is it possible to inform the C# compiler that another property will be non-null based on another property?
Asked Answered
M

1

5

Supposing I have a class like this:

public class BridgeFormModel
{
    [Required]
    [Display( Name = "What is your name?" )]
    public String? Name { get; set; }

    [Required]
    [Display( Name = "What is your quest?" )]
    public String? Quest { get; set; }

    [Required]
    [Display( Name = "What is your favourite colour?" )]
    public String? FaveColour { get; set; }

    [BindNever]
    public Boolean IsValid =>
        !String.IsNullOrWhiteSpace( this.Name ) && 
        !String.IsNullOrWhiteSpace( this.Quest ) && 
        !String.IsNullOrWhiteSpace( this.FaveColour );
}

Currently the C# 8.0 and C# 9.0 compiler will not infer that Name is not-null when it knows IsValid is true:

public IActionResult CrossTheBridge( BridgeFormModel form )
{
    if( form.IsValid )
    {
        if( form.FaveColour.Equals( "Blue" ) ) // Warning: `form.FaveColour` may be null here
        {
            Console.WriteLine( "Right, off you go" )
        }
    }
}

So we have to either assert form.FaveColour! - or do this instead:

public IActionResult CrossTheBridge( BridgeFormModel form )
{
    if( form.IsValid && form.FaveColour != null && form.Name != null && form.Quest != null )
    {
        if( form.FaveColour.Equals( "Blue" ) )
        {
            Console.WriteLine( "Right, off you go" )
        }
    }
}

We have the [NullWhen] and [NotNullWhen] attributes, but those only apply to method parameters, not other properties on the same object instance.

This wouldn't be an issue if C# still supported code-contracts, but alas, here we are... is there any way to inform the C# compiler of nullability (and other state-invariants?) based on a property?

I'd like to be able to do something like this:

public class BridgeFormModel
{
    [NotNullWhenPropertyIsTrue( nameof(IsValid) )]
    [Required]
    [Display( Name = "What is your name?" )]
    public String? Name { get; set; }

    [NotNullWhenPropertyIsTrue( nameof(IsValid) )]
    [Required]
    [Display( Name = "What is your quest?" )]
    public String? Quest { get; set; }

    [NotNullWhenPropertyIsTrue( nameof(IsValid) )]
    [Required]
    [Display( Name = "What is your favourite colour?" )]
    public String? FaveColour { get; set; }

    [BindNever]
    public Boolean IsValid =>
        !String.IsNullOrWhiteSpace( this.Name ) && 
        !String.IsNullOrWhiteSpace( this.Quest ) && 
        !String.IsNullOrWhiteSpace( this.FaveColour );
}

As NotNullWhenPropertyIsTrue is not a real attribute, I'm wondering if there's some way to write a Roslyn extension or analyser that can implement the necessary logic - or provide null-safety assertions to Roslyn.

Merissa answered 20/3, 2021 at 21:53 Comment(8)
Does it work when you have this.Name != null as well in the IsValid property to make it easier for the compiler to see the null check (and not hide behind string.IsNullOrWhiteSpace()?Hanshansard
can you use is? if( form.FaceColour is string colour && colour.Equals( "Blue" )) { Console.WriteLine(colour); }Knisley
@Hanshansard Yes, that works, but my point is that it shouldn't be necessary to do that.Merissa
@GetOffMyLawn Yes, and I demonstrated that in my question already - my point is that it shouldn't be necessary.Merissa
@Merissa I do not see you demonstrating that in your question....Knisley
form is { IsValid: true, FaceColour: "Blue" } would work too since it's all constants... Though I admit it's not what's being asked, I just like how it looksVittorio
@Vittorio When using the is { Prop: value } syntax, how do you specify an IComparer? or is it always bound to Object.Equals(Object) - or does it use IEquatable<T> if available?Merissa
@Merissa unfortunately you don't, which makes switching on string constants semi-problematic for example. If they ever introduce active patterns (user defined is essentially) then it would become possible to specify a Comparer. But for now... You can'tVittorio
C
10

You may look at MemberNotNullWhen attribute, which was introduced in C# 9 and .NET 5 and write something like this:

[MemberNotNullWhen(true, nameof(Name), nameof(Quest), nameof(FaveColour))]
public Boolean IsValid =>
        !String.IsNullOrWhiteSpace( this.Name ) && 
        !String.IsNullOrWhiteSpace( this.Quest ) && 
        !String.IsNullOrWhiteSpace( this.FaveColour );

Design notes and additional details can be found in the dotnet runtime issue #31877

Cephalonia answered 20/3, 2021 at 22:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.