Goodbye, NullReferenceException: Option and LINQ
20 Mar 2023 #tutorial #csharpIn the previous post of this series, we covered three C# operators to simplify null checks and C# 8.0 Nullable References to signal when things can be null
. In this post, let’s learn a more “functional” approach to removing null
and how to use it to avoid null
when working with LINQ XOrDefault
methods.
1. Use Option: A More Functional Approach to Nulls
Functional languages like F# or Haskell use a different approach for null
and optional values. Instead of null
, they use an Option
or Maybe
type.
With the Option
type, we have a “box” that might have a value or not. It’s the same concept of nullable ints, for example. I bet you have already used them. Let’s see an example,
int? maybeAnInt = null;
var hasValue = maybeAnInt.HasValue;
// false
var dangerousInt = maybeAnInt.Value;
// ^^^^^
// Nullable object must have a value.
var safeInt = maybeAnInt.GetValueOrDefault();
// 0
With nullable ints, we have variable that either holds an interger or null
. They have the HasValue
and Value
properties, and the GetValueOrDefault()
method to access their inner value.
We can extend the concept of a box with possibly a value to reference types with the Option
type. We can wrap our reference types like Option<int>
or Option<Movie>
.
The Option Type
The Option
type has two subtypes: Some
and None
. Some
represents a box with a value inside it, and None
, an empty box.
The Option
has two basic methods:
- One method to put something into a box. Often we call it
Unit
. For this, we can use the constructor ofSome
andNone
. - One method to open the box, transform its value and return a new one with the result. Let’s call this method
Map
.
Let’s use the Optional library, a robust option type for C#, to see how to use the Some
, None
, and Map()
,
using Optional;
Option<int> someInt = Option.Some(42);
Option<int> none = Option.None<int>();
var doubleOfSomeInt = someInt.Map(value => value * 2)
.ValueOr(-1);
// 84
var doubleOfNone = none.Map(value => value * 2)
.ValueOr(-1);
// -1
We created two optional ints: someInt
and none
. Then, we used Map()
to double their values. Then, to retrieve the value of each optional, we used ValueOr()
with a default value.
For someInt
, Map()
returned another optional with the double of 42 and ValueOr()
returned the same result. And for none
, Map()
returned None
and ValueOr()
returned -1.
How to Flatten Nested Options
Now, let’s rewrite the HttpContext
example from previous posts,
public class HttpContext
{
public static Option<HttpContext> Current;
// ^^^^^
public HttpContext()
{
}
public Option<Request> Request { get; set; }
// ^^^^^
}
public record Request(Option<string> ApplicationPath);
// ^^^^^
Notice that instead of appending ?
to type declarations like what we did with in the past post when we covered C# 8.0 Nullable References, we wrapped them around Option
.
This time, Current
is a box with Request
as another box inside. And Request
has the ApplicationPath
as another box.
Now, let’s retrieve the ApplicationPath
,
var path = HttpContext.Current
.FlatMap(current => current.Request)
// ^^^^^
.FlatMap(request => request.ApplicationPath)
// ^^^^^
.ValueOr("/some-default-path-here");
// ^^^^
// Or
//.Match((path) => path , () => "/some-default-path-here");
// This isn't the real HttpContext class...
// We're writing some dummy declarations to prove a point
public class HttpContext
{
public static Option<HttpContext> Current;
public HttpContext()
{
}
public Option<Request> Request { get; set; }
}
public record Request(Option<string> ApplicationPath);
To get the ApplicationPath
value, we had to open all boxes that contain it. For that, we used the FlatMap()
method. It grabs the value in the box, transforms it, and returns another box. With FlatMap()
, we can flatten two nested boxes.
Notice we didn’t do any transformation with FlatMap()
. We only retrieved the inner value of Option
, which was already another Option
.
This is how we read ApplicationPath
:
- With
FlatMap()
, we opened theCurrent
box and grabbed theRequest
box in it. - Then, we used
FlatMap()
again to openRequest
and grab theApplicationPath
. - Finally, with
ValueOr()
, we took out the value insideApplicationPath
if it had any. Otherwise, if theApplicationPath
was empty, it returned a default value of our choice.
“This is the way!” Sorry, this is the “functional” way! We can think of nullable ints like ints being wrapped around a Nullable
box with more compact syntax and some helper methods.
2. Option and LINQ XOrDefault methods
Another source of NullReferenceException is when we don’t check the result of the FirstOrDefault
, LastOrDefault
, and SingleOrDefault
methods. These methods return null
when the source collection has reference types, and there are no matching elements. In fact, this is one of the most common mistakes when working with LINQ.
There are some alternatives to prevent the NullReferenceException when working with XOrDefault
methods.
.NET 6.0 released some new LINQ methods and overloads. With .NET 6.0, we can use a second parameter with the XOrDefault
methods to pass a default value of our choice. Also, we can use the DefaultIfEmpty method instead of filtering collections with FirstOrDefault
.
Use Optional’s XOrNone
But, let’s combine the XOrDefault
methods with the Option
type. We can make the XOrDefault
methods return an Option<T>
instead of null
.
The Optional library has FirstOrNone()
, LastOrNone()
and SingleOrNone()
instead of the usual XOrDefault
methods.
This time, let’s use FirstOrNone()
instead of FirstOrDefault()
,
using Optional.Collections;
// ^^^^^
var movies = new List<Movie>
{
new Movie("Shrek", 2001, 3.95f),
new Movie("Inside Out", 2015, 4.1f),
new Movie("Ratatouille", 2007, 4f),
new Movie("Toy Story", 1995, 4.1f),
new Movie("Cloudy with a Chance of Meatballs", 2009, 3.75f)
};
var theBestOfAll = new Movie("My Neighbor Totoro", 1988, 5);
// With .NET FirstOrDefault()
var theBest = movies.FirstOrDefault(
movie => movie.Rating == 5.0,
theBestOfAll);
// ^^^^^
// With Optional's FirstOrNone()
var theBestAgain = movies.FirstOrNone(movie => movie.Rating == 5.0)
// ^^^^^
.ValueOr(theBestOfAll);
// ^^^^^
Console.WriteLine(theBestAgain.Name);
record Movie(string Name, int ReleaseYear, float Rating);
By using the XOrNone
methods, we’re forced to check if they return something before trying to use their result.
Voilà! That’s the functional way of doing null
, with the Option or Maybe type. Here we used the Optional library, but there’s also another library I like: Optuple. It uses the tuple (bool HasValue, T Value)
to represent the Some
and None
subtypes.
Even though we used a library to bring the Option type, we can implement our own Option
type and its methods. It’s not that difficult. We need an abstract class with two child classes and a couple of extension methods to make it work.
Don’t miss the other posts in this series, what the NullReferenceException is and when it’s thrown, nullable operators and references, and separate optional state into separate objects.
If you want to learn more about LINQ, check my Quick Guide to LINQ to start learning about it.
Happy coding!