Turn on Nullability checks by default

01/09/2023

Since C# 8, we have nullable reference types. The word sounds odd, given the fact that reference types are always nullable. The idea is that the default is that your reference types have to be properly initialized. Here are my thoughts after a few years of using them.

Nullable Reference Types (NRT)

The idea is that you have to mark a reference type as nullable explicitly. This is done by adding a ? to the type. For example, string? is a nullable string. The default is that a reference type is not nullable. With C# 8 this feature was only enabled by default if you created a new project, it was not turned on in existing code bases.

The major driver behind this feature is to make it easier to write code that is null safe. The compiler will warn you if you are using a reference type that is not nullable in a way that could lead to a null reference exception. Microsoft saw that the NullReferenceException is the most common exception in .NET applications. So they wanted to make it easier to write code that is null safe. I will not go into much detail about the feature itself, there are plenty of resources out there. Here the official one from Microsoft itself: https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references

What are my thoughts?

In new projects, it is my default choice - and there is a simple reason: It gives you a cleaner API. You have to think about nullability, and you have to make a decision. There is less ambiguity. Have a look at the following method:

ValueTask<TEntity> GetById(int id);

Is the return value nullable? What happens if we can't find the entry by the id? Do we get an exception or just a null and here is where NRTs shine:

ValueTask<TEntity?> GetById(int id); 

Now, we see clearly that the return value is nullable. The same holds true if we generalize the whole thing and look at API design:

public class Person
{
    public string Name { get; set; }
    public string Address { get; set; }
}

From the looks of it, you can't tell what is a mandatory property and what is not. With NRTs you can make it clear:

public class Person
{
    public string Name { get; set; }
    public string? Address { get; set; }
}

The new required keyword helps also out a ton. You can write objects like this, that will not violate any of the NRT rules:

public class Person
{
    public required string Name { get; init; }
}

Obviously, there are also some bigger problems with the feature - everytime then where the framework itself initializes your stuff. Meet Entity Framework and Blazor. In Entity Framework you have DbSet definition like the following:

public class MyDbContext : DbContext
{
    public DbSet<MyDomainObject> MyDomainObjects { get; set; }
}

Without anything this code will throw will warnings that MyDomainObjects might be null and is not initialized. Yes - I mean that is the job of Entity Framework and not mine. So to come around that you have to "trick" the compiler with the Bang operator:

public DbSet<MyDomainObject> MyDomainObjects { get; set; } = default!;

In Blazor you have the same issue:

<MyComponent @ref="MyComponent" />

@code {
    [Parameter]
    public MyObject MyObject { get; set; }

    private MyComponent MyComponent { get; set; }
}

Both (MyObject and MyComponent) are set by the Blazor runtime and not by myself, but both of them will result in compiler warnings/errors (depending on your setting and preference).

Conclusion

For new projects, I would almost all the time use nullable reference types. There are a few exceptions - mainly if you rely on a third-party library you have to use extensively that doesn't have annotations and you get a lot of false positive/false negatives in your code. For legacy applications, I would identify parts where you see NullReferenceExceptions a lot and might partially add the support for NRTs. But often times the time you have to invest to make that "right" is not justified. Imagine refactoring >1 Mio. of code for the sake of having this enabled.

3
An error has occurred. This application may no longer respond until reloaded. Reload x