DEV Community

Dennis
Dennis

Posted on

Code with intent!

Now that Umbraco has moved to .NET 6 and up, I've been reviewing more pull requests from my coworkers on this new framework and I noticed some struggles with the new null state analysis feature. In this article, I want to briefly explore what this feature is, why one might struggle to deal with it and why it is so important for code quality.

Nullability

In .NET Framework, the value null is an exclusive feature for reference types. In order to enable the use of null for value types, we use a wrapper Nullable<T>. With some syntactic sugar, that turns into T?. It's purely for practical purposes to make types like integers support null.

Moving to .NET 6, we get introduced to a new concept: Nullable reference types. In short: you're now allowed to also add ? behind reference types to indicate that this value might contain null. .NET 6 also introduces a new static analyzer that attempts to identify code that badly handles potential null values.

Struggling to adapt

It's no surprise that experienced developers on .NET Framework feel confused. Why would I use this at all? I already know that reference types can contain null, what's the difference? Looking at my company, I often see one of two things:

  • "This code used to just work, now the compiler says I'm doing things wrong. I'm ignoring / disabling it"
  • "Oh, the compiler says this might be null, I'll add a null check"

And why wouldn't you be like one of those? Especially when converting an old code base to .NET 6, the amount of warnings that you suddenly get, is rather overwhelming. When porting the URL Tracker package to Umbraco 9 and 10, I had over 2000 warnings in my code suddenly. One does not simply adapt to nullable reference types and it may seem more productive to turn the feature off.

It's important though

The importance of this feature is in conveying your intent. Though variables of a reference type can contain null, in some code you expect them to have a non-null value. If you effectively manage your expectations regarding null values, your code becomes significantly more expressive.

When looking at the quotes, what stands out is that this behaviour is led by the compiler as if it's the boss. The compiler is not your boss, it is your friend. It's telling you that your expectations are ambiguous. You should aim to tell the compiler one of these:

  • "I expect that this value will never contain null"
  //  Notice the ! in this line
  string myVar = MyMethod()!.ToLower();
Enter fullscreen mode Exit fullscreen mode
  • "I expect that this value might contain null"
  // Notice the ? in this line
  string? myVar = MyMethod()?.ToLower();
Enter fullscreen mode Exit fullscreen mode
  • "I'll handle the null value"
  string? myPotentialNullVar = MyMethod();
  if (myPotentialNullVar is null) myPotentialNullVar = "My default string";

  // Notice the lack of ? here because we already checked for null in the if statement
  string myVar = myPotentialNullVar.ToLower();
Enter fullscreen mode Exit fullscreen mode

All these examples show clearly whether or not it was intended for myVar to contain null. Managing your expectations automatically improves your code quality. It prevents questions like: "Is this expected behaviour or is this a bug?". It gives you and your team more confidence.

Some practical examples

Let's have a look at some examples that I've come across and see how nullable reference types make your code more expressive.

Method signatures

In .NET Framework, one might write a method like this:

public Thing GetFirstThing()
{
    return _myListOfThings.FirstOrDefault();
}
Enter fullscreen mode Exit fullscreen mode

This however, is ambiguous. Your method promises to return an instance of Thing, but might actually return null. As a consumer of this method, I might get the wrong expectations. In .NET 6, you should change this code to one of these options:

If you expect there to always be an element in the list:

public Thing GetFirstThing()
{
    // There should always be an item in this list
    //   If there isn't, then something is broken, so an exception should be thrown
    return _myListOfThings.First();
}
Enter fullscreen mode Exit fullscreen mode

If you expect that the list might be empty:

// Notice the ? in the return type
public Thing? GetFirstThing()
{
    return _myListOfThings.FirstOrDefault();
}
Enter fullscreen mode Exit fullscreen mode

Now your intentions are well-defined.

It's not always that simple though, so let's have a look at another example:

public static string DefaultIfNullOrWhiteSpace(this string input, string @default)
{
    return !string.IsNullOrWhiteSpace(input) ? input : @default;
}
Enter fullscreen mode Exit fullscreen mode

I like this method a lot, it reduces the amount of code that I write and improves expressiveness. It's ambiguous to the compiler in .NET 6 though, so I'd change the code to this:

public static string? DefaultIfNullOrWhiteSpace(this string? input, string? @default)
{
    return !string.IsNullOrWhiteSpace(input) ? input : @default;
}
Enter fullscreen mode Exit fullscreen mode

However, if I try to use the method, I'm getting some unnecessary warnings:

// WARNING: Converting possible null value to non-nullable type
string myVar = myPotentialNullVar.DefaultIfNullOrWhiteSpace("My default")
Enter fullscreen mode Exit fullscreen mode

I know for sure that the output of the method will never be null, but the compiler is not clever enough to understand that as well, so we have to help the compiler a little bit:

[return: NotNullIfNotNull(nameof(@default))]
public static string? DefaultIfNullOrWhiteSpace(this string? input, string? @default)
{
    return !string.IsNullOrWhiteSpace(input) ? input : @default;
}
Enter fullscreen mode Exit fullscreen mode

The NotNullIfNotNull attribute tells the compiler that the return value can never be null if the parameter @default is not null. This is just one of many, you can read more about these attributes in the microsoft documentation about null state analysis.

Class definitions

In .NET Framework, I might define a POCO like this:

public class MyModel
{
    public string MyProperty { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

That's not good enough for the .NET 6 compiler though: MyProperty is never initialized with a value, so it potentially contains null. You can fix this in one of several ways:

public class MyModel
{
    public MyModel(string myFirstProperty)
    {
        // Method #1: assign the value in the constructor
        MyFirstProperty = myFirstProperty
    }

    public string MyFirstProperty { get; set; }

    // Method #2: assign a default value to the property
    public string MySecondProperty { get; set; } = "My default value";

    // Method #3: make the property nullable
    public string? MyThirdProperty { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Here it's important as well to convey your intent. Don't just make the property nullable because the compiler is complaining. Think about what you expect this value to be and act accordingly.

Now I'm personally still struggling with a few use cases here. Request models in ASP.NET for example are defined as POCOs. I might expect a property on the POCO to always have a value, because it's required. I make sure of that with validation attributes:

public class MyRequestModel
{
    [Required]
    public string MyProperty { get; set; }
    // WARNING Non-nullable field must contain a non-null value when exiting constructor
}
Enter fullscreen mode Exit fullscreen mode

I haven't found a way yet to tell the compiler that this property will never contain null, since it is required and I use model validation. In this case, I also can't assign the property in the constructor. Therefore, I resorted to this workaround:

public class MyRequestModel
{
    [Required]
    public string MyProperty { get; set; } = null!;
}
Enter fullscreen mode Exit fullscreen mode

Albeit not pretty, it does get rid of the warning, while sufficiently showing my intent.

Final thoughts

I love this feature. It's important to me that code is expressive and conveys the intentions of the developer behind it and this feature improves expressiveness a lot. I understand that it may be confusing for experienced .NET Framework developers though. The feature also adds some different challenges, some of which I haven't quite worked out myself.

So should you use this? It depends. If you're lucky enough to start a brand new solution, I'd say you should. If you're converting an old code base to the new framework, then you should probably agree on it with your team. The amount of warnings you might get at the initial conversion may lead to notification fatigue, but if you solve them one at a time, you'll benefit from it in the long term.

So what are your thoughts on nullable reference types and null state analysis? Let me know in a comment!

Thank you for reading! 😊

Top comments (0)