Durée de lecture : environ 7 minutes

Before C# 8 came in in 2019, the following code compiled just fine :

string something = null;

However since C# 8, and if the <Nullable> option is set to enable in the csproj (which is the case by default since C# 10/.Net 6), a warning appears at build time and the variable will be underlined in blue in Visual Studio or Rider. It shows that the variable was declared as a non-nullable string, meaning affecting null is not a good idea. If you want to have the opportunity to make it nullable, you have to declare it like this :

string? something = null;

That’s what the interrogation mark next to the type does : it means this variable is nullable. This is an explicit declaration and without it the variable is not expected to be null. Hence the warning in the first case.

Now this is not exactly an unknown syntax ; value types have always had it :

int? someInt = null

But not reference types until C# 8. In this article, we will see why nullable reference types are important and also how to handle them.

Dereferencing

Now let’s say we have a nullable reference type. Since C# 8, any time the compiler analyses and finds a potential cause for a NullReferenceException, it sends a warning. For example with the following code :

string? something = SomeCallThatMightReturnNull();
var length = something.Length;

Trying to access a property on a reference type is called dereferencing. It means that when you need to access a property or method or whatever, you are following the reference in order to access the actual object. Doing it on null will obviously cause a crash. So when a variable is possibly null and you try to dereference it, the compiler will show a warning, underlined in your favorite IDE.

But that’s just a few warnings ; there’s a whole list available at Microsoft’s website with examples covering most of them.

But why ?

The point of such warnings is precisely to warn us at build time. We can still act before the problem reaches runtime ! I firmly believe that feedbacks should be fast so that we can fail safely before bugs happen in production. This is exactly what we need here : the infamous NullReferenceException can be detected before it causes a crash. This is pure gold !

Drawbacks of nullables

Overwhelming

When activated on a legacy project, the Nullable option might display quite a lot of warnings. Again, this is a good thing since the point is to avoid exceptions at runtime but somethimes the number of warnings can be quite intimidating and/or introduce a lot of noise at compilation time. While I advocate to fix these warnings as they appear, it might be difficult or unrealistic to fix all of them for a given project. Of course, there are a couple of possibilities to suppress them :

  • Simply disable the option in the csproj : <Nullable>disable</Nullable> Removing the XML node will also have the same effect.
  • Use the ! syntax to tell the compiler “I know what I am doing, this variable is definitely not null”. This can be useful if you have previously checked the variable was not null. Example :
[Test]
public void Do_some_stuff()
{
    string? someString = GetNullableString();

    Assert.That(someString, Is.Not.Null);
    Assert.That(someString!.Length, Is.EqualTo(0));
    Assert.That(someString.EndsWith("123"), Is.True);
}

We know on line 7 that the someString variable is not null, otherwise the code would have stopped executing. However, the compiler does not see that and will show a warning about a possible dereference. So to suppress this warning, we append the exclamation mark after the variable name to tell it “don’t worry, I know it’s not null”. Then the compiler will not bother us about this variable again so we do not need to reuse the exclamation mark afterwards.

The null! case

There is some weird syntax that exists and that does not make much sense to me but let’s talk about it. The following code will compile with no warning :

string someString = null!;

In a nutshell, what this code means is “let’s declare a variable that should not be null but let’s initialize it to null but hey I know what I’m doing and I’m telling you it’s not null”. It seems a bit contradictory does it not ?
I have found there are not many cases where it can be useful ; it comes in handy when testing edge cases however, like when validating the input of a controller in this example :

public record InputPayload(string? NullableString, int SomeInt, string NonNullableString);

[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
    [HttpGet("testNull")]
    public IActionResult TestNull(InputPayload input)
    {
        if (input.NonNullableString == null)
        {
            return BadRequest($"{nameof(input.NonNullableString)} cannot be null");
        }

        return Ok();
    }
}

With the following test code

[Test]
public void Return_BadRequest_when_NonNullableString_is_null()
{
    var controller = new MyController();

    var result = controller.TestNull(new InputPayload(null, 0, null!));

    Assert.That(result, Is.TypeOf<BadRequestObjectResult>());
}

This is a case that can happen because the non-nullability of the NonNullableString property is not checked by the framework and the JSON can definitely pass null. So if we want to validate the input and test it, we can do so by using the null! syntax on line 6 where a simple null will not compile.

Propagation 😱

Of course, one could be tempted to avoid dealing with a null warning by simply propagating it and let the caller take care of it. After all, adding a simple interrogation mark at the end of the return type is easy. But, just like exceptions, I believe that most of the time it’s easier and cleaner to handle null values as soon as we get them. So how do we handle null values gracefully ?

Handling nulls

The first thing to do is ask ourselves where the null comes from. Those questions will then help us handle the null case.

Deserialization

Is the null coming from a deserialization ? Does it mean the field or argument is optional ? If so, depending on how we use it afterwards, we might want to use a better syntax for a possible “empty” or “not applicable” value.
This is where the Null Object Pattern comes at the rescue : deserialize null stuff from your infra or adapter layer and use spacific business-oriented syntax to your business layer. Consider the following code :

// Defined in Business layer
public record Price(decimal Value, Currency Currency);
public record PriceNotFound() : Price(0m, new NoCurrency());
public record Currency(string Name, string Symbol);
public record NoCurrency() : Currency(string.Empty, string.Empty);

// Defined in Infra layer
public record PriceDto(decimal Value, Currency Currency, string RoomName, string RateName);

public class AvailabilityChecker
{
    public Price GetLowestPrice(Guid hotelId, DateOnly startDate, DateOnly endDate, int numberOfGuests)
    {
        var priceDto = GetLowestPriceFromDatabase(hotelId, startDate, endDate, numberOfGuests);
        if (priceDto == null)
        {
            return new PriceNotFound();
        }

        return new Price(priceDto.Value, priceDto.Currency);
    }

    private PriceDto? GetLowestPriceFromDatabase(Guid hotelId, DateOnly startDate, DateOnly endDate, int numberOfGuests)
    {
        // Performs the query to DB ; returns null if no price could be found, e.g. when the hotel is fully booked.
    }
}

The PriceNotFound and NoCurrency objects are key here, they are more explicit than a null value and less error-prone. Worst-case scenario is if nothing is done on the caller side, then PriceNotFound will behave like a normal Price and nothing will crash.
Furthermore, we could imagine querying the database in such a way that the result of the GetLowestPriceFromDatabase() method can detect various cases : the hotel could not be found in database, the hotel is fully booked, the hotel is closed, etc… and return an appropriate type for each case. The business layer could then act depending on the type.

This is similar to (but not exactly the same as) the Result pattern which helps getting rid of exceptions in the control flow. This is out of the scope of this article so you can check out Andrew Lock’s series of articles for more information.

Unnecessary nulls and defensive code

Drop the automatic “OrDefault” suffix. Very often the “non-OrDefault” counterpart is enough.

But what if simply assumed the deserialized value is null but never is ? Or what if we misguided ourselves by being needlessly careful ? For example :

string[] someStrings = ["apple", "orange", "guava", "durian", "apricot"];
string? firstFruit = someStrings.FirstOrDefault();

Why did we use FirstOrDefault() here ? We know the array is not empty, we just defined it on line 1. Of course this is a silly code snippet but calling an “OrDefault” method (i.e. FirstOrDefault, SingleOrDefault…) while there is absolutely no way the default will occur is more common than you think. And it causes unnecessary null management or propagation.

A more subtle (but still very common) way is to use nullable properties in a DTO when querying a database while the column assigned to this DTO is actually non-nullable.

Always go back to the origin of a possible null value and wonder if it’s really is possible. And drop the automatic “OrDefault” suffix, very often the “non-OrDefault” counterpart (First or Single or Last ot whatever) is enough.

Possible new business case(s)

It’s also possible you have found an edge case. Like there’s an call that returns null in some cases and everything is perfectly valid. It’s just that you do not know what to do about them.
Well it’s the perfect time to have a little chat with the business experts ! Tell them what you found and when it can happen ; make sure you understand the specific business conditions when the nulls can happen and ask the experts how the application should behave in such cases.

Maybe it’s not important, maybe there’s no need to handle these cases because the business have determined it is unlikely they will happen, maybe there’s already a contingency plan you did not know about, etc… But the good thing is that you will actually be aware of it.

Taking it up a notch

Now if you really mean business and hate nulls, you can always treat the warnings as errors. The build will fail instead of simply warning you about possible null cases.

There are 2 lines of code :
string[] someStrings = ["apple", "orange", "guava", "durian", "apricot"];
string firstFruit = someStrings.FirstOrDefault();

The last part (someStrings.FirstOrDefault) is underlined in red in Visual Studio.
Entering the danger zone 😁

It may seem a bit overzealous or crazy, especially if you are used to legacy code with little to no null management but I actually have done it quite a few times. The idea is either to start the project with the errors enabled from the get-go, or fix the warnings (if any) and then treat the warnings as errors. Starting with a clean slate is best.

Now on how to it : simply open the csproj (this can’t be done at the solution level unfortunately) where you want to treat the warnings as errors and then add the following line in the main PropertyGroup (right below the Nullable node would be a perfect place) :

<WarningsAsErrors>nullable</WarningsAsErrors>

And that’s it ! All the nullable warnings will now be treated as errors. And if it becomes a pain, you can always remove this behaviour or make define manually which warning(s) you want to turn as errors.

Wrap-up

To me, the real takeaway of this article is this : .Net gives us a strong possibility to manage NullReferenceExceptions before they occur at run time. It’s not foolproof, it’s not perfect but it does a huge job at build time. We should not let this opportunity pass and ignore the warnings.

Software developer since 2000, I try to make the right things right. I usually work with .Net (C#) and Azure, mostly because the ecosystem is really friendly and helps me focus on the right things.
I try to avoid unnecessary (a.k.a. accidental) complexity and in general everything that gets in the way of solving the essential complexity induced by the business needs.
This is why I favor a test-first approach to focus on the problem space and ask questions to business experts before even coding the feature.

Categories: Dev

Guillaume Téchené

Software developer since 2000, I try to make the right things right. I usually work with .Net (C#) and Azure, mostly because the ecosystem is really friendly and helps me focus on the right things. I try to avoid unnecessary (a.k.a. accidental) complexity and in general everything that gets in the way of solving the essential complexity induced by the business needs. This is why I favor a test-first approach to focus on the problem space and ask questions to business experts before even coding the feature.