Nullability in .NET With C#

Photo of a laptop screen displaying computer code

In the world of software development, handling null values is a constant challenge. Nullability isnโ€™t just a quirk of the .NET framework; itโ€™s a pivotal concept that can make or break the stability of your applications. 

Understanding nullability when coding in C#, why it matters, and how to reframe your thinking around it is essential to writing cleaner, safer, and more efficient code. In this article, we’ll dive deep into nullability in .NET, exploring its intricacies and arming you with the tools to tackle it head-on.

What is Nullability?

Nullability refers to variables, properties, and functions that contain or return a reference to null, rather than the actual type declared. When consuming an object or variable that might contain null and without explicitly guarding against null in your code, it could result in a NullReferenceException.

However, when programming in C# 8.0 and beyond,ย  nullability rules provide a structured approach to address this issue. Reference objects (such as objects, strings, etc.) in .NET have always had the chance to be a reference to null. Value types (structs, int, bool, etc.) gained the ability to be explicitly nullable via the Nullable<T> generic wrapper (later available via the ? operator following the type declaration) since .NET Framework 2.0.

Now, since C# 8.0 (.NET Core 3.0 / .NET Standard 2.1) nullable types in C# have evolved with the introduction of the Nullable Reference Type feature of the language. By enabling this feature in C#, nullability rules give you compile-time warnings (and possibly compile errors) to help reduce runtime encounters of a NullReferenceException.

Enabling Nullable Reference Type

Your project must be using the C# language version 8.0 or greater, and either .NET Core 3.0 or .NET Standard 2.1. If your project doesnโ€™t meet these conditions, youโ€™ll need to upgrade your project first.

In your .csproj file, add the following property

C/C++
<PropertyGroup>
    	<Nullable>enable</Nullable>
</PropertyGroup>

You have a few options for the value of Nullable, though enable should be your first choice. Your projects will 99 out of 100 times use enable. For other value types, please refer to Microsoftโ€™s documentation.

A Preview of its Effects

The main takeaway from enabling this feature is that C# enforces nullability by treating reference types in C# as non-nullable by default unless proven otherwise.

With <nullable>disable</nullable>, this class declaration will not trigger any kind of warning for its string property declaration:

Screenshot showing a class declaration

However, by setting <nullable>enable</nullable> you will receive a warning that the string could possibly contain a null reference and that you should take steps to either ensure it contains a value at the time of instantiation or declare the property as nullable.

Screenshot showing a class declaration with nullable enabled.

Since it is absolutely true that the Line1 string could contain a null reference, this feature in C# enforces nullability rules to help you understand your code much better in order to avoid a NullReferenceException.

Controlling Feature Per File

Enabling this feature via your project file will enable it for all files in your project. If you want to convert an existing project, itโ€™s best if you disable this in problematic files (i.e. files that prevent you from compiling), and then fix those files, re-enabling nullability as you go.

To disable this feature in a file, add the following directive to the top of the file.

#nullable disable.

C# Coding With Nullability in Mind

Now that your project has the Nullable Reference Type feature enabled, letโ€™s start coding for it.

Letโ€™s consider the following class which defines a basic United States postal address.

C/C++
public class PostalAddress {
  public string Line1 { get; set; }
  public string Line2 { get; set; }
  public string City { get; set; }
  public string State { get; set; }
  public string ZipCode { get; set; }
}

Now, thereโ€™s nothing particularly wrong with this object, except that the Line2 property is not always required. Also, letโ€™s consider that we donโ€™t care about City or State all of the time, because that could be populated via ZipCode after weโ€™ve already constructed an instance of this object.

If we were to use this object as-is ( and without C#โ€™s nullability rules), how do we know whatโ€™s ok to be null and what isnโ€™t?

Letโ€™s refine this object to be closer to our expectations of actual usage.

C/C++
public class PostalAddress {
  public required string Line1 { get; set; }
  public string? Line2 { get; set; }
  public string? City { get; set; }
  public string? State { get; set; }
  public required string ZipCode { get; set; }
}

The two changes we made were to introduce the required modifier for the two properties we know are always required, and made the others explicitly nullable.

When using this updated class, along with the Nullable Reference Type feature in C# versions that have nullability modifiers and operators, we will see errors and warnings based on the propertyโ€™s declaration and value.

Letโ€™s consider the following code example:

C/C++
var address = new PostalAddress {
  Line1 = "123 Main St",
  ZipCode = "90210"
};
 
Console.WriteLine(address.Line2);

The compiler will give us a warning that Line2 may be null here:

Screenshot showing a line of C# code with a warning

Whereas, if we try Console.WriteLine(address.Line1) we will see the compiler tell us that Line1 is not null at that point, as Line1 is required and therefore should be populated at instantiation of the object:

Screenshot showing a line of C# ode with a warning

By using C#โ€™s nullability modifiers and operators, we have more confidence in what our properties contain.

May Be Null / Is Not Null

The terms โ€œmay be nullโ€ and โ€œis not nullโ€ are used to indicate how the compiler sees the status of a value. The compilerโ€™s warnings and errors will never say that a value is absolutely null; itโ€™ll always be โ€œmay be nullโ€. Be sure to pay attention to this distinction.

C#โ€™s Nullability Modifiers and Operators

The following modifiers and operators will help you define the nullability of your properties, fields, and variables including nullable types in C#. You should absolutely make use of all of these in order to define your code in a consistent and predictable way.

Required Modifier

This modifier requires the decorated property or field to contain a non-null value at the time of instantiation. You must provide values for these properties either through a constructor or with the object initializer pattern.

C/C++
public class PostalAddress {
  public required string Line1 { get; set; }
 
  public PostalAddress(string line1) {
	this.Line1 = line1;
  }
 
  public PostalAddress() {
  }
}
 
var address1 = new PostalAddress("123 Main St"); // ok
var address2 = new PostalAddress { Line1 = "123 Main St" }; // ok
var address3 = new PostalAddress(); // compile error

When coding in C# with nullability in mind, there are a few drawbacks to using the required modifier. Read through the docs to understand them better.

Nullable Operator โ€œ?โ€

When this operator is placed after a type declaration, the value of that property, field, or variable is known by the compiler to potentially contain a reference to null.ย 

This is especially useful for value types, where a null reference is never allowed unless explicitly declared as nullable.

C/C++
int? myNullableInt = null; // completely ok
int myNonNullableInt = null; // compiler error

Assigning a Null-Forgiving Null โ€œnull!โ€

While this pattern isnโ€™t used in our example, itโ€™s absolutely worth mentioning in the context of being explicit with your nullability while coding in C#.

Sometimes, using the required modifier is not possible, due to some of its restrictions, but you still want to declare that your property is absolutely not null, nor should it ever be considered as such.

In such a case, when working with nullable reference types in C#, you can assign a null with a null-forgiving operator ! to your field or property.

C/C++
public class PostalAddress {
  public string Line1 { get; set; } = null!;
}

This tells the compiler that we have already set the property to a value that is not null, therefore, this property is not null, but also not required.

Thanks to C# nullability, we can ensure that any time we create a new PostalAddress, the Line1 property is set to a real non-null value. Additionally, since we are free to change the property later, nullability rules help maintain clarity and safety in our code.

The bonus point to this approach is that Line1 is still null at the time of instantiation, and checking for null will result in the correct response. Youโ€™re in full control to set this value to something non-null at a later time while expressing in your object that the consumer should assume itโ€™s a non-null value.

Just keep in mind that, because of nullability rules in C#, the compiler helps identify potential null issues, but it is ultimately your responsibility to ensure this value is non-null before it is accessed or used by other code.

A display of computer code

Static Analyzers

Now that you have an understanding of nullability, letโ€™s explore Static Analyzers to take your nullability coding to astronomical levels.

What are Static Analyzers?

The .NET compiler ( Roslyn ) can analyze your code in real time, offering many services, such as compiler warnings, refactoring, and generated source code.

The .NET team has provided a handful of very useful attributes to be used by the C# Nullable Reference Type system. These attributes enhance C# nullability by helping the compiler understand when a property or variable may or may not be referencing null, offering warnings or errors to help prevent your code from encountering a NullReferenceException.

You are already benefiting from this system, whether you know about these attributes or not. Letโ€™s go through these attributes as youโ€™ll definitely want to use them directly in your code.

The compiler will believe you when you use these attributes, so be sure you are using them correctly, otherwise, you are giving developers a false sense of security.

NotNull

In C#, this attribute enforces nullability by indicating that the decorated argument is not null (even if the type allows it) if the function returns.

C/C++
public static void ThrowIfNull([NotNull] string? input){
  if (string.IsNullOrEmpty(input)){
	throw new Exception();
  }
}
 
string? test = null;
// test may be null here
 
ThrowIfNull(test);
 
// test is not null here

This is a bit of a contrived example, as the static analyzer will already tell you that test is null as it knows you assigned null to it.

You are probably already using this with something like the following:

C/C++
// input may be null

ArgumentNullException.ThrowIfNullOrEmpty(input);

// if code continues here, input is not null

NotNullWhen

In C#’s nullability system, this attribute of nullable reference types is great if you donโ€™t want to disrupt the flow of your code, as NotNull will do. You can define what value, when returned, indicates that the argument will not be null.

C/C++
public static bool NotNullWhen([NotNullWhen(true)] string? input)
{
    	return !string.IsNullOrEmpty(input);
}
if(NotNullWhen("Test") == true){
  // not null here
} else {
  // may be null here
}

You are probably already using this with common Try* pattern functions.

C/C++
public static bool Parse(string? input){
  // input may be null here
  bool parsed = int.TryParse(input, out int value);
  // if parsed == true, input is not null here
}

MaybeNull

In C#’s nullability system, this attribute indicates that the decorated argument may be null, even when the type doesnโ€™t allow it.

In this example, you can see the argument input is not declared as a nullable string, yet since itโ€™s a reference type, we can set it to null. By using [MaybeNull] we are being good citizens and informing the compiler that we could set an otherwise-to-be-known non-null value to null.

C/C++
public static void MaybeNull([MaybeNull] string input)
{
    	input = null;
}
 
string test = "Test";
// test is not null here
 
MaybeNull(test);
// test may be null here

MaybeNullWhen

Much like NotNullWhen this allows you to specify that a return value indicates an argument may be null when coding in C# with nullability in mind.

C/C++
public static bool MaybeNullWhen([MaybeNullWhen(false)] string input)
{
    	return false;
}
 
string test = "Test";
// test is not null here
 
bool isNull = MaybeNullWhen(test);
// if isNull == false, test may be null here

NotNullIfNotNull

For C# nullable types, this attributeโ€™s name sounds like the start of a stack overflow exception, but itโ€™s actually to indicate that one property or return value will not be null, if the provided property or argument names are not null.

C/C++
[return: NotNullIfNotNull(nameof(input))]
public static string? NotNullIfNotNull(string? input)
{
    	return input;
}
 
var nullTest = NotNullIfNotNull(null);
// nullTest may be null because the input was null
 
var notNullTest = NotNullIfNotNull("Test");
// notNullTest is not null because the input was not null

MemberNotNull

Letโ€™s revisit the AddressInfo class we created earlier. If you remember, in the C# nullability version,ย  we left City and State as nullable strings.

What if we wanted to let the compiler know that these two properties are going to be not null after we call a function in that class?

C/C++
public class AddressInfo
{
	/* ... other properties removed for brevity ... */
    	public string? City { get; set; }
    	public string? State { get; set; }
 
    	[MemberNotNull(nameof(City), nameof(State))]
    	public void SetPostalInfo(string city, string state)
    	{
           	this.City = city;
           	this.State = state;
    	}
}
 
var address = new AddressInfo();
 
// address.City and address.State may be null here.
 
address.SetPostalInfo("Denver", "Colorado");
 
// address.City and address.State are not null here.

C# Nullability rules ensure that by using the MemberNotNull attribute on our function, we can say the provided properties will not be null after the function returns.

Static Analyzer Scope

It is important to note that the compiler has a limited scope in knowing the effects of these attributes especially when working with nullable types in C#.

Consider the following code:

C/C++
public class MyClass {
  public string? MyProperty { get; set;}
}
 
public static void Run()
{
  // this is our entry point
  // we get a string that may be null from some source
  string? input = SomeService.GetString(); 
  
  // input may be null here
 
  ArgumentNullException.ThrowIfNull(input);
 
  // input is not null here
 
  MyClass instance = new MyClass { MyProperty = input };
 
  // instance.MyProperty is not null here
 
  Process(instance);
}
 
public static void Process(MyClass value)
{
	Console.WriteLine(value.MyProperty);
	// value.myPropertyValue may be null here
}

Thanks to C#โ€™s nullability rules, we know that MyProperty is not null when we create an instance of MyClass; however, once we leave the scope of Run(), the compiler no longer knows that MyProperty is not null when it enters another function.

Screenshot showing a line of C# code

Summary

By leveraging features like Nullable Reference Types, and static analyzers, and carefully managing your property declarations, you can significantly reduce the likelihood of run-time NullReferenceExceptions. This approach allows you to clearly communicate nullability expectations in your design, while the compiler provides valuable feedback on potential issues.

Work With Us

At Pell Software, we specialize in building robust, error-resistant systems including in .NET development to help you implement these best practices seamlessly.ย 

Whether you are a business or development team in need of expert outside help, we are here to help. Our team is made up of 100% US-based engineers and it is top-rated with 55+ 5-star reviews for our services.ย 

With their expertise in C# coding with Nullability in mind, our team ensures that your code not only compiles but is optimized to avoid run-time errors. And remember, if you see green squiggles, donโ€™t ignore them! These warnings, while not always blocking compilation, can signal potential issues that may lead to costly run-time exceptions.ย 

Let us help you address these warnings early on, ensuring smoother deployments and stable software performance. Connect now to propel your business forward.

Contact Us

Reach out today to get a free consultation for your next project

  • Client-oriented
  • Results-driven
  • Independent
  • Problem-solving
  • Competent
  • Transparent

Schedule Free Consultation

Name(Required)