--- title: "Nullable reference types and designing with intent" description: "Null reference exceptions can be a thing of the path, but with great power comes great responsibility." pubDate: 2019-11-05 heroImage: "/images/blog/compiler_complaint.png" tags: [dotnet] --- It's almost 5 o'clock, you've just deployed the latest API build into your test environment for other teams to start consuming and integrating into their applications, and the weekend is right around the corner. Then, the Slack messages begin. > Steve from that one team: Hey, you guys just released a new build, right? Looks like we're getting 500s calling your > team's API. Naturally, we check the logs in the test environment using our favorite application insight tool (we happen to use [Splunk](https://www.splunk.com/) in my company), and aimlessly attempt to find any sign of failure, praying to the higher powers that may be to just be the client that has the issue. Then, it happens: ``` ERROR: NullReferenceException at (35,16) in Program.cs ``` Ugh... of course, and only on a Friday. The previous scenario is something we developers are all too familiar with, and our mortal enemy, the `NullReferenceException` has been besting even our most experienced code slingers for over half of a decade. Since its inception in the early 1960s, `null` has been a staple of computer science, software engineering, and expression of application intent in nearly every facet of building applications. We've built million dollar software systems based on the idea, and even worse, have caused billions (yes, with a "b") of dollars of damage in the form of irrecoverable business data. Unfortunately, one could argue that `null` is here to stay, deeply rooted in many of the world's most complex software systems that power entire economies, and there's no plans to re-engineer its original design intent. With the rise of object-oriented programming, the `null` pointer has possibly been one of the most common issues in our software applications. Take for example the following: ```csharp var myObject= new MyObject { Foo = "Bar" }; // Assigning another object to the same reference that myObject points to var anotherObject = myObject; anotherObject.Foo = "Not Bar"; Console.WriteLine(myObject.Foo); // Prints "Not Bar" ``` Nothing out of the ordinary here, as we software engineers see this kind of stuff all over our codebases. Sharing references between objects (one could make the argument) forms the core of object-oriented programming. Pointers, pieces of our stack allocated memory that "point" to our reference values in memory, are easy to pass around, manipulate, and conveniently dereference and go about our merry way. Pointer references, although great as they may be, present the issue that has plagued nearly every software application at one point, or another: the null pointer. Take for example the extension of our code from above: ```csharp anotherObject.Foo = null; Console.WriteLine(myObject.Foo.Length); ``` What happens now? `anotherObject` assigns our `Foo` property to an absent value, all while we attempt to dereference that same property value and retrieve the length on the next line. Run the program, and watch the catastrophe in action: ``` Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object. ``` _Hello darkness, my old friend..._ the infamous `NullReferenceException`. _Queue the obvious rhetorical question_ - is there anything we can do to prevent this behavior? Enter C# 8.0 and [nullable reference types](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references), our `NullReferenceException` saving grace. As a professional .NET Core amateur, I'll do what I do best and explore this shiny new feature of C# in all its glory. Let's spin up a simple console app reflecting the previous example code to start things off: ``` > dotnet new console -n NullableReferencesExample ``` Now, in our `Program.cs` file, let's add the following: ```csharp using System; namespace NullableReferencesExample { class Program { static void Main(string[] args) { var myObject= new MyObject { Foo = "Bar" }; var anotherObject = myObject; anotherObject.Foo = "Not Bar"; Console.WriteLine(myObject.Foo); anotherObject.Foo = null; Console.WriteLine(myObject.Foo.Length); } } internal class MyObject { public string Foo { get; set; } } } ``` Go ahead and run a quick `dotnet restore`, and let's build our project with a `dotnet build`: ``` Microsoft (R) Build Engine version 16.3.0+0f4c62fea for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restore completed in 22.78 ms for /path/to//NullableReferencesExample/NullableReferencesExample.csproj. NullableReferencesExample -> /path/to//NullableReferencesExample/bin/Debug/netcoreapp3.0/NullableReferencesExample.dll Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:02.44 ``` Our build completes just fine, with no signs of terror ahead even though we've knowingly written an inevitable disaster within our code. If we run this application with a `dotnet run`, we get exactly what we expect. Let's capture the build warning ahead of time by bringing in C# 8.0 and the nullable context to our project scope by adding the following to our `PropertyGroup` section of our `NullableReferencesExample.csproj`: ```xml Exe netcoreapp3.0 enable 8.0 ``` Notice that we add the `enable` property; with this tag, we've now enabled nullable references throughout our _entire_ project. In layman's terms, now every where in our project that a reference type exists _without_ being declared as nullable, the compiler will assume that **we cannot assign those values as `null`**. This is huge. If you've ever programmed in a language like Rust, you've seen this concept firsthand. Briefly for those that haven't, the concept of `null` does not _really_ exist in Rust, which is one of the reasons (along with _many_ others) an army of developers have adopted it as one of the most loved languages, according to the last [Stack Overflow developer survey](https://insights.stackoverflow.com/survey/2019). By introducing this nullable context for all reference types in our code, the compiler will implement _strict_ rules anytime we do not initialize a non-nullable reference type, dereference a nullable reference type without checking for null, etc. Let's rebuild our project to see this in action with a quick `dotnet build`: ``` Microsoft (R) Build Engine version 16.3.0+0f4c62fea for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restore completed in 21.09 ms for /path/to/NullableReferencesExample/NullableReferencesExample.csproj. Program.cs(26,23): warning CS8618: Non-nullable property 'Foo' is uninitialized. Consider declaring the property as nullable. [/path/to/NullableReferencesExample/NullableReferencesExample.csproj] Program.cs(19,33): warning CS8625: Cannot convert null literal to non-nullable reference type. [/path/to/NullableReferencesExample/NullableReferencesExample.csproj] NullableReferencesExample -> /path/to//NullableReferencesExample/bin/Debug/netcoreapp3.0/NullableReferencesExample.dll Build succeeded. Program.cs(26,23): warning CS8618: Non-nullable property 'Foo' is uninitialized. Consider declaring the property as nullable. [/path/to/NullableReferencesExample/NullableReferencesExample.csproj] Program.cs(19,33): warning CS8625: Cannot convert null literal to non-nullable reference type. [/path/to/NullableReferencesExample/NullableReferencesExample.csproj] 2 Warning(s) 0 Error(s) Time Elapsed 00:00:02.61 ``` Low and behold, the compiler warnings we hoped for (the good kind, at least) alert us that we have not initialized a non-nullable reference type (our `Foo` property in our `MyObject` class) and that we've assigned our `Foo` property to `null` (from the compiler's perspective, we shouldn't be doing that). In our new world of nullable reference types, we have to specify to the compiler when our reference types have the possibility of being null, and where exactly we might make those assignments. Now, coming from a language like Rust, nullable reference types are not to say there is no longer a concept of `null` in C#; should we opt in for the nullable context, we must tell the compiler what types have the ability to be null. By designing our code with this idea in mind, coupled with the compiler assisting us with its strict compile time reference checking, we gain an added layer of code security and the possibility of eliminating any chance of a `NullReferenceException` (not to say we'll _never_ get one, we simply greatly reduce the chance if we implement the proper design). We'll go ahead and address our current compiler warnings using this new concept of nullable reference types, but before we do, let's discuss the different ways we might be able to accomplish this. - In our `MyObject` class, we could make our `Foo` property a `string?` type rather than just a `string` - We could keep our typing of `string` and override the compiler warnings, essentially telling it "hey, I know this could be `null`, and that's okay" - We could keep our typing of string `string` and add a single constructor to guarantee the `MyObject` class always has a valid `Foo` string value So, what should we do? Well, the answer is simple: **it depends**. Let's think about the context for our `MyObject` class. In the real world, we build applications designed to solve real world problems, usually built around a central domain architecture. What does our `MyObject` class represent? Is there a business-driven reason as to why the `Foo` property might not exist on an instance of `MyObject`? For our use case, let's define a rule that there is a valid reason for `Foo` to be absent of value. So, let's declare it as nullable: ```csharp internal class MyObject { public string? Foo { get; set; } } ``` With our build errors now fixed, let's add a method to snag the length of a `string` argument passed in back to the caller: ```csharp private static int GetLength(string someStringValue) { return someStringValue.Length; } ``` Now back in our `Main` method, let's call this function to grab a reference to the length of another `MyObject` instance: ```csharp var mySecondObject = new MyObject(); var fooLength = GetLength(mySecondObject.Foo); ``` and if we build our project, we'll get the following warnings: ``` Program.cs(23,39): warning CS8604: Possible null reference argument for parameter 'someStringValue' in 'int Program.GetLength(string someStringValue)'. ``` What caused this warning? The compiler noticed that we we're passing a nullable reference type (our `string?` type `Foo` property) to a method expecting a non-nullable string type. Without the nullable context enabled, we would get **no** warnings from the compiler; within the nullable context, the compiler is protecting us from runtime `NullReferenceException`s _because_ of this compile time analysis. Back in our `Main` method, if we add a null check before calling our `GetLength` method and compile: ```csharp var mySecondObject = new MyObject(); if (mySecondObject.Foo != null) { var fooLength = GetLength(mySecondObject.Foo); } ``` our project compiles successfully with no warnings, since the compiler sees that we are _guaranteeing_ a non-nullable string will be passed into `GetLength()`. We could also make `GetLength()` accept a nullable string type and using the `?` operator while dereferencing `someStringValue`; the compiler will be happy either way. ### Another Example Let's see how we might be able to leverage the power of nullable reference types with another example. I've recently written a series of posts guiding readers as we build a real world application using .NET Core and Dapper centered around a fictional brewery management software called Dappery. I'll take a few examples from those posts and use them here, since you might already be familiar. Let's create a brewery class, and see how we can use nullable reference types to safely construct instances of this class and add beers to an associated brewery: ```csharp using System; using System.Collections.Generic; namespace NullableReferencesExample { public class Brewery { public ICollection? Beers { get; set; } public string Name { get; set; } public void AddBeer(string name, double abv, byte ibu) { Beers?.Add(new Beer { Name = name, Style = style, Abv = abv, Ibu = ibu }); } public void PrintBeers() { Console.WriteLine($"----- {Name} has {Beers?.Count} beers -----"); foreach (var beer in Beers) { Console.WriteLine($"----- {beer.Name} -----"); Console.WriteLine($"| Style: {beer.Style} |"); Console.WriteLine($"| ABV: {beer.Abv} |"); Console.WriteLine($"| IBU: {beer.Ibu} |"); } } } } ``` and our associated beer class that we'll use in conjunction with our `Brewery` class: ### Beer.cs ```csharp namespace NullableReferencesExample { public class Beer { public string Name { get; set; } public string Style { get; set; } public double Abv { get; set; } public byte Ibu { get; set; } public Brewery Brewery { get; set; } } } ``` Take a look at our `Brewery` class and notice how we've declared our collection of `Beer`s as a nullable reference; this is an intentional design decision (something we should always keep in mind when working in the nullable context). We're expressing our intent to both the compiler and developers working with this code after us that "hey, there is a _chance_ that a brewery could not have a reference to any beers." Should we initialize our list to an empty collection of beers? We totally could, but for the purpose of us exploring the nullable reference context, we'll leave our collection as nullable for now. To not overload us with compiler warnings, let's add the new `#nullable disable` preprocessor directive at the top of our `Beer.cs` class and run `dotnet build` to see what warnings we might get: ``` Brewery.cs(20,34): warning CS8602: Dereference of a possibly null reference. Brewery.cs(10,23): warning CS8618: Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable. ``` Interesting... take a look at that first warning: `Brewery.cs(20,34): warning CS8602: Dereference of a possibly null reference.` Looking at our code in `Brewery.cs`, we're iterating over our `Beers` collection, which we declared as nullable, telling the compiler there's a chance the enumerable could be null. Let's fix this by checking that we have a collection before iterating through each beer within the `PrintBeers` method: ```csharp public void PrintBeers() { Console.WriteLine($"----- {Name} has {Beers?.Count} beers -----"); if (Beers != null) { foreach (var beer in Beers) { Console.WriteLine($"----- {beer.Name} -----"); Console.WriteLine($"| Style: {beer.Style} |"); Console.WriteLine($"| ABV: {beer.Abv} |"); Console.WriteLine($"| IBU: {beer.Ibu} |"); } } } ``` Addressing our second compiler warning, let's initialize the `Name` property since we're telling the compiler this is a non-nullable string reference, so callers of our code can safely dereference a brewery object's `Name` without fear of the value being null. We'll initialize the `Name` property while instantiating a `Brewery` object, making our code correct by construction. While we're at it, we'll refactor the initialization of the `Beers` collection as well: ```csharp using System; using System.Collections.Generic; namespace NullableReferencesExample { public class Brewery { public Brewery(string name) => Name = name; public ICollection? Beers { get; set; } public string Name { get; } public void AddBeer(string name, string style, double abv, byte ibu) { Beers ??= new List(); Beers.Add(new Beer { Name = name, Style = style, Abv = abv, Ibu = ibu }); } public void PrintBeers() { Console.WriteLine($"----- {Name} has {Beers?.Count ?? 0} beers -----"); if (Beers is null) { Console.WriteLine("No beers found"); return; } foreach (var beer in Beers) { Console.WriteLine($"----- {beer.Name} -----"); Console.WriteLine($"| Style: {beer.Style} |"); Console.WriteLine($"| ABV: {beer.Abv} |"); Console.WriteLine($"| IBU: {beer.Ibu} |"); } } } } ``` In our `AddBeer` method, notice our usage of the null-coalescing assignment operator `??=`. Recently added in C# 8.0, `??=` is a derivative of the `??` null-coalescing operator. Using `??=`, our operand on the left side will be assigned to the evaluation of the right side, if our left side operand evaluates to null. Anytime we instantiate a brewery object and attempt to add a beer, we'll initialize our beer collection on the first beer added. To make things interesting, let's give our breweries an address, since I've never heard of a virtual brewery (yet): ### Address.cs ```csharp namespace NullableReferencesExample { public class Address { public Address(string streetAddress, string city, string state, string zipCode, string? zipCodeExtension) => (StreetAddress, City, State, ZipCode, ZipCodeExtension) = (streetAddress, city, state, zipCode, zipCodeExtension); public string StreetAddress { get; } public string City { get; } public string State { get; } public string ZipCode { get; } public string? ZipCodeExtension { get; } } } ``` and back in our `Brewery.cs` class, let's add an `Address` property: ```csharp using System; using System.Collections.Generic; namespace NullableReferencesExample { public class Brewery { public Brewery(string name, Address address) => (Name, Address) = (name, address); public ICollection? Beers { get; set; } public Address Address { get; } public string Name { get; } public void AddBeer(string name, string style, double abv, byte ibu) { Beers ??= new List(); Beers.Add(new Beer(name, style, abv, ibu, this)); } public void PrintBeers() { Console.WriteLine($"----- {Name} has {Beers?.Count ?? 0} beers -----"); if (Beers is null) { Console.WriteLine("No beers found"); return; } foreach (var beer in Beers) { Console.WriteLine($"----- {beer.Name} -----"); Console.WriteLine($"| Style: {beer.Style} |"); Console.WriteLine($"| ABV: {beer.Abv} |"); Console.WriteLine($"| IBU: {beer.Ibu} |"); } } public void PrintAddress() { Console.WriteLine("---- Address -----"); Console.WriteLine($"Street: {Address.StreetAddress}"); Console.WriteLine($"City: {Address.City}"); Console.WriteLine($"State: {Address.State}"); Console.WriteLine(string.IsNullOrWhiteSpace(Address.ZipCodeExtension) ? $"Zip Code: {Address.ZipCode}" : $"Zip Code: {Address.ZipCode}-{Address.ZipCodeExtension}"); } } } ``` Notice that our `Address` property, within our nullable reference context, is _not_ allowed to be null as we did not declare it as nullable. In our `PrintAddress`, the compiler knows our `Address` property is non-nullable, and we can safely dereference the property without having to check if `Address` exists, avoiding any chance of a `NullReferenceException`. If we build our project at this point, we should see no warnings and the compiler should happy. Let's go ahead and remove the `#nullable disable` preprocessor directive from out `Beer.cs` file and build our project one more time to see the new set of compiler warnings we'll get: ``` Beer.cs(8,23): warning CS8618: Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable. Beer.cs(10,23): warning CS8618: Non-nullable property 'Style' is uninitialized. Consider declaring the property as nullable. Beer.cs(16,24): warning CS8618: Non-nullable property 'Brewery' is uninitialized. Consider declaring the property as nullable. ``` As expected, we get three warnings due to our three reference type properties not being initialized in the `Beer.cs` class. Notice we only received the warnings for our `string` and `Brewery` reference types, as the `byte` and `double` are value types stored on the stack that will take on their `default` value if not supplied a value at construction time. For our use case, let's about think our intent for the `Beer` class: a beer should have a name, a brewing style ( lager, IPA, etc.), and should be associated to a brewery. Each of these properties should be a **non-nullable** reference type, which means that at construction time, we need to supply an initial **non-null** value. To best solve our problem, rather than using the override `default!` assignment on each of these properties, or declaring each as nullable when we know these properties should _not_ be null, let's add a constructor that will assure us values of each of these properties anytime we instantiate a `Beer` object: ```csharp namespace NullableReferencesExample { public class Beer { public Beer(string name, string style, double abv, byte ibu, Brewery brewery) => (Name, Style, Abv, Ibu, Brewery) = (name, style, abv, ibu, brewery); public string Name { get; } public string Style { get; } public double Abv { get; } public byte Ibu { get; } public Brewery Brewery { get; } } } ``` This is all fine and dandy, as we've explored some of the compiler warnings we could possibly get in a nullable context, but what happens if we ignore those warnings and assign things as `null` anyway? Let's clean out our `Main` method in `Program.cs` and start adding some breweries and beers: ```csharp namespace NullableReferencesExample { class Program { static void Main(string[] args) { var breweryAddress = new Address("1030 E Cypress Ave.", "Redding", "CA", "96002"); var brewery = new Brewery("Fall River Brewery", breweryAddress); var anotherBreweryAddress = new Address("1075 E 20th St.", "Chico", null, "95928"); var anotherBrewery = new Brewery("Sierra Nevada Brewing Company", anotherBreweryAddress); } } } ``` and if we build our project: ``` Program.cs(10,83): warning CS8625: Cannot convert null literal to non-nullable reference type. ``` Again, the compiler is warning us that we've assigned a `null` value to a non-nullable string reference type in our driver program. Coupled with the declaration of non-null reference types within our `Beer` class, we've now protected any instantiation of a `Beer` object to _always_ expect a non-null value for any of it's injected values. Now, what if we choose to ignore this warning and do something like: ```csharp Console.WriteLine(anotherBreweryAddress.State.Length); ``` in our application? The compiler _won't_ give us any warnings, because from its perspective, the `State` property is non-null so this code would be totally valid. Enabling the nullable reference type context is only half of the solution to eliminating `NullReferenceException`s; we, the developers, are response to design our code with _intent_. Coupled with the nullable context, we are now responsible for architecting our code, declaring which properties the compiler should expect to be null, and conversely allowing the compiler to warn us when we've dereferenced a value that could possibly be null. Let's give our `Main` method a little more logic to top things off: ```csharp namespace NullableReferencesExample { class Program { static void Main(string[] args) { var breweryAddress = new Address("1030 E Cypress Ave.", "Redding", "CA", "96002"); var brewery = new Brewery("Fall River Brewery", breweryAddress); brewery.AddBeer("Hexagenia", "Indian Pale Ale", 7.1, 120); brewery.PrintAddress(); brewery.PrintBeers(); var anotherBreweryAddress = new Address("1075 E 20th St.", "Chico", "CA", "95928"); var anotherBrewery = new Brewery("Sierra Nevada Brewing Company", anotherBreweryAddress); anotherBrewery.PrintAddress(); anotherBrewery.PrintBeers(); } } } ``` If we build our project now with a quick `dotnet build`, we see there are no compiler warnings, ensuring us each of our instantiated breweries has been properly constructed, and we've eliminated any chance of null references. If we run our project with `dotnet run`, we see the following: ``` ---- Address ----- Street: 1030 E Cypress Ave. City: Redding State: CA Zip Code: 96002 ----- Fall River Brewery has 1 beers ----- ----- Hexagenia ----- | Style: Indian Pale Ale | | ABV: 7.1 | | IBU: 120 | ---- Address ----- Street: 1075 E 20th St. City: Chico State: CA Zip Code: 95928 ----- Sierra Nevada Brewing Company has 0 beers ----- No beers found ``` ### Wrapping Things Up The nullable reference context new to C# 8.0, allowing us to leverage nullable reference types, is an incredibly powerful tool for developers to construct clean, safe code. With nullable reference types and proper application architecture, we can nearly eliminate nearly _any_ chance of a `NullReferenceException` within our code, guaranteeing compile and runtime safety while building an added layer of code security within our programs. Until next time, amigos!