Avoiding Null Reference Exceptions for Umbraco developers

The humble null reference has been described as the "billion-dollar mistake" by its inventor, and if you've spent time developing software then you're probably already aware why. This one particular error has a habit of plaguing our codebases, causing surprise production issues, and only actually causing errors far away from where the real problem is, making tracking these problems down harder still.

It doesn’t have to be this way! A few versions ago, C# introduced “nullable types” for primitives, like ints and bools, which allow us to represent a value which can be not present in a more structured way. This idea was originally borrowed from functional languages, like F# or Haskell, where there is no concept of a “not present value” at all!

Unfortunately, the Nullable<T> type in C# is only available for value types, which means we can’t use it for almost everything interesting. Thankfully, other people have written C# types which provide similar behaviour, but for any type, as well as providing a suite of functions or methods to interact with this data inspired by the same functional paradigm that the concepts originally came from.

Most people who work with Umbraco aren’t already experts at functional programming, however! Thankfully, you don’t need to be, and you might be surprised at how many of the concepts are already familiar to you, thanks to C#’s habits of taking the best parts of other languages—did you know that LINQ was another Haskell-inspired addition?

There’s a wide variety of libraries you can use for these purposes, including the heavyweight Language-Ext and more targeted Optional. For these examples, we’ll be using Optional, but these are common concepts in many languages and so everything we’re discussing is achievable in many different ways.

So let’s take a look at the common problems caused by null references, and how we can solve them.

Problem #1 - Can this return null?

The simplest problem with null references is also one of the easiest to solve. Because any reference type can hold a null value, it can be very difficult to know whether it’s possible for a method to return null without checking the source code. If you don’t have the source code immediately available to you, then things are even harder. Option types solve this by letting you mark whether something definitely returns a value, or whether it only might return a value. In a codebase which always uses optional types, you can know that a function

T Foo();

will always give you back an instance of T, while

Option<T> Foo();

may not be able to return a value. By encoding it in the type system like this, you can always know when and where your values could be null, and so avoid the necessity of null checks which will never fire, and the inverse case of forgetting null checks where they are actually necessary.


There are then several ways to turn values into optional values, depending on which is the most convenient. Turning a value you know you have into a present optional value is easy—definitelyPresent.Some()—, as is turning a value which may be null into an optional value which may not be present—maybePresent.SomeNotNull(). If you know the value is not present, then Optional.None<T> will provide you a correctly typed optional value which is not present.

This looks a lot like null by another name, though. What’s the difference? The most important part is the things you can’t do with an optional value! When you have a null reference, you can do anything with it that you could do with a non-null reference, except that it’ll actually fail at runtime—if you’re lucky! With an optional value, you can only interact with it via a small set of methods, and each of those methods has very well defined semantics for what it does when there is a value, what it does when there isn’t a value, and how each method interacts with every other method.

None of those methods let you do what you can do with null references. There are no methods which work fine when the value is present, but fail somewhere unexpected down the line when it isn’t. There are methods which work when the value is present, and fail immediately if they are not, but these are obviously marked by the inclusion of “Unsafe” in their name, and always fail at the point of attempting to use them, unlike null references, which will fail whenever they happen to be accessed.

Let’s take a look at some of the available methods!

Problem #2 - Did this return null?

With the above change, we now know whether a method can return null. We still need to be able to deal with that eventuality, however. It’s no use knowing that something can return null if we then don’t actually handle it. Thankfully, this has already been thought about.

There are many ways to interact with an Option<T>, and the vast majority all automatically respect whether there’s a value present. Every available method and function is either always safe to use, or obviously flags up that they’re unsafe.

The simplest way is to ask the Option<T> to give you either the value it contains, or a default you provide.

option.ValueOr(defaultValue);

This has the advantage of forcing you to consider what should happen if a value isn’t present. This can be the most appropriate technique to use if you’re retrieving a setting from config, or some piece of content which isn’t mandatory. If the methods which retrieve these things return an optional value, then you know that regardless of who calls them, be it you in five minutes or a fresh new junior developer in eighteen months time, simply won’t be able to forget that the value may not be present.

Problem #3 - Changes in behaviour

With the above change we can easily guarantee default values for our optional parameters, but it’s very often the case that if we do have the value, we need to do some additional work with it. Traditionally, this relies on a developer remembering every stage at which something can be null, and then wrapping the affected areas in null checks. This is manual and failure-prone, as we all know!

With optional types, we can make it impossible to forget what could be not present and where, while still being able to operate on them. Many of the available methods on an Option<T> are dedicated to this purpose. For example, it provides Map, which is analogous to Select in a LINQ statement, as a function which will apply to the value if it is present, or be skipped if it is not. For more advanced usages, FlatMap is analogous to a LINQ SelectMany, in that it behaves the same as Map, but flattens the response—which is to say, if you have an inner function which itself returns an optional type, Map will not flatten and will return an Option<Option<T>>, while FlatMap will flatten, and will return an Option<T>.

For example, the snippet

int? nodeId = GetNodeIdFromConfig();
string relevantContent;
if (nodeId.HasValue) 
{
  var content = IPublishedContentIfItIsPresent(nodeId.Value);
  relevantContent = content.Value("relevantContent");
}
//use relevantContent

is probably recognisable, at least in structure, to most of us. There are several issues with this code, however.

  1. When it’s done, it isn’t clear whether relevantContent actually contains anything.
  2. It still contains the potential for a Null Reference Exception, as content may also be not present!
  3. It isn’t safe against future changes, as the flow of data needs to be considered and understood to avoid writing code which assumes values are present when they are not.

Using optional types, we can write a safe and maintainable version of the above. If our two functions instead returned Option<T>, the above issues would not be possible for any developer, no matter how junior, to create.

var relevantContent = GetNodeIdFromConfig()
  .FlatMap(IPublishedContentIfItIsPresent)
  .FlatMap(c => c.Value("relevantContent").SomeNotNull())
//use relevantContent

This snippet implements the same logic as the above, but has no potential for null reference errors, and neither is it easy for those errors to be introduced during maintenance. Further, the handling for not present values, which is exposed as null-checking boilerplate in the above example, is transparently handled here.

Problem #4 - Using the data

At some point, we have to stop transforming the data and start doing something with it, however. Thankfully, we have some powerful tools for this, too. The most familiar workflow will use ValueOr as described above, but we can often do better than that.

The Match, MatchSome, and MatchNone set of methods allow us to run code based on the state of our value. Match takes two parameters, one function for if the value is present, and one for if it is not, and is allowed to return a value, making it useful both for triggering behaviour based on our values and as a super-powered ValueOr. MatchSome and MatchNone both take one function, and will either run it or not, depending on whether the value is present or not. As it isn’t guaranteed that those methods will execute any code, they can’t return anything, and so are only useful for the code they run, but this is often very useful. For example:

relevantContent.Match(c => MakeWidgetForContent(c), () => MakeNoContentWidget());

This code lets us add the content we may have read out in the previous step to wherever we want to put it, but only if it’s present. If it is not present, we’ll instead make a default widget. It’s very similar to doing the same with null checks and if statements, except it’s not possible to forget, and all of the boilerplate code is hidden from us behind methods which mean things.

Problem #5 - Rendering a page

Ideally, your view holds no logic, and so when it comes time to render a page, there are no values which may not be present. As we can use ValueOr to provide default values, this is often easy to achieve, but sometimes it is not, and we are forced to put at least a little bit of logic into our templates. Thankfully, we can still use optional types here too! It’s a little more awkward to do in Razor than it is plain C#, but the same techniques still work.

Option one is to take advantage of the fact that an optional type can easily be modelled as an IEnumerable with either zero or one values. This allows us to very easily unwrap an optional value in a safe manner. Our view can thus contain code like:

@foreach(var content in relevantContent.ToEnumerable()){
{
  <div> @(content.ToString()) </div>
}

With a structure like that, it is easy to solve the most common problem in a view, rendering out some section if the data is present, and not otherwise. If it is necessary to perform special behaviour if the value is not present, we have several other options. Firstly, the
HasValue property will let us determine if the optional value is empty (and the Exists and Contains methods exist to let us ask more specific questions about what it does and does not contain), making it easy to write code like:

@if (!relevantContent.HasValue){
{
  //display the not-present UI
}

Alternatively, if your view is decomposed into partial views or helper functions, we can take advantage of the more powerful Match method, which allows us to provide two functions, one for the case where the value is present, and one for the case where it is not. This allows us to write code which maintains the iron-clad guarantees we benefit from in backend code, like:

@relevantContent.Match(
  presentValue => Html.Partial("Widget", presentValue),
  () => Html.Partial("NoWidget"))

Conclusion

This is a very brief overview of the capabilities of optional types, but I hope it shows how they aren’t an academic persuit, but are something which can be actively useful to real world developers creating real world projects, today. There’s plenty of capabilities we haven’t touched on here, but I think that this is a good place to start for real usage.

About the Author

Annabelle Heaford, Senior Developer

Belle is one of the super brains at Gibe with a 1st in Computer Science. Belle's been programming software, websites and games since age 12. As one of our C# and .NET developers she is a hard-core problem solver. Annabelle is also an Umbraco Certified Professional.