Updated November 2026: the samples now target .NET 10, where nullable reference types are on by default, and I added the tools we use every day with them: the
!operator, the#nullabledirective, the[NotNullWhen]attribute and turning the warnings into errors.
C# 8.0 brought several new features. In this article, we focus on one of them: nullable reference types.
Nullable reference types
A value type only accepts null when we add ? to it:
int? a = null;
// error CS0037: Cannot convert null to 'int' because it is a non-nullable value type
int b = null;A reference type, on the other hand, has always accepted null. Nothing in its declaration says whether null is expected, so we have to check before using it.
Let's take an extension method that counts the words in a string:
public static class StringExtensions
{
public static int CountWords(this string text)
{
return text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
}We use it in the top-level statements of Program.cs:
string message = "We love C# and .NET";
int count = string.IsNullOrWhiteSpace(message) ? 0 : message.CountWords();
Console.WriteLine($"Count words: {count}");Count words: 5CountWords throws a NullReferenceException when text is null, so we check message before calling it. Nullable reference types let the compiler track which references can be null, the same way it does for value types.
We enable them with the Nullable element of the .csproj file:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable></PropertyGroup>Note: Since .NET 6, the project templates (
dotnet new console,dotnet new webapi…) already contain<Nullable>enable</Nullable>, so a new project has nullable reference types on from the start. We only add it ourselves to older projects. The element exists since .NET Core 3.0 and C# 8.0.
Now, if we set message to null, the compiler warns us:
string message = null;warning CS8600: Converting null literal or possible null value to non-nullable type.To declare that a reference can be null, we add ? to its type:
string? message = null;Why no warning on message.CountWords()?
message can now be null, yet the compiler says nothing about message.CountWords() on the next line. The compiler follows the flow of our code, and string.IsNullOrWhiteSpace tells it something: its parameter is declared [NotNullWhen(false)] string? value, meaning "if I return false, the value was not null". In the : branch, the compiler therefore knows that message is not null.
We can put the same attribute, from System.Diagnostics.CodeAnalysis, on our own methods:
using System.Diagnostics.CodeAnalysis;
public static class TextHelper
{
public static bool HasWords([NotNullWhen(true)] string? text)
{
return !string.IsNullOrWhiteSpace(text);
}
}string? message = Console.ReadLine();
if (TextHelper.HasWords(message))
Console.WriteLine($"Count words: {message.CountWords()}");Without [NotNullWhen(true)], the compiler cannot know what HasWords checks, and warns us:
warning CS8604: Possible null reference argument for parameter 'text' in 'int StringExtensions.CountWords(string text)'.Other attributes of the same family, such as [NotNull], [MaybeNullWhen] and [MemberNotNull], cover the other situations.
The null-forgiving operator !
Sometimes we know better than the compiler. Console.ReadLine() returns a string? because it returns null when the input has no more lines. In a small tool that always reads from the keyboard, the null-forgiving operator ! tells the compiler that the value is not null:
string input = Console.ReadLine()!;
Console.WriteLine($"Count words: {input.CountWords()}");The ! only removes the warning. Nothing is checked at runtime: if we are wrong, we get a NullReferenceException. Use it sparingly.
The #nullable directive
On a large existing project, enabling nullable reference types everywhere at once can produce hundreds of warnings. The #nullable directive changes the setting for part of a file, so we can migrate step by step:
#nullable disable
public class LegacyCustomer
{
public string Name { get; set; } // no warning: nullable is disabled here
}
#nullable restore
public class Customer
{
// warning CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor.
// Consider adding the 'required' modifier or declaring the property as nullable.
public string Name { get; set; }
}#nullable restore returns to the project's setting. As the warning suggests, the fix for Customer is either string? or the required modifier (public required string Name { get; set; }), which makes every caller set Name.
We can also work the other way round: keep <Nullable>disable</Nullable> in the project and add #nullable enable at the top of each migrated file.
Turning the warnings into errors
A warning is easy to ignore. To stop null problems from getting in, we can treat every nullable warning as an error without touching the other warnings:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors></PropertyGroup>The string message = null; sample above no longer builds:
error CS8600: Converting null literal or possible null value to non-nullable type.Conclusion
A ? after a reference type can look strange at first. Once we know it, nullable reference types let the compiler point out null references while we write the code, instead of at runtime. And since .NET 6, they are on by default.
Do you use nullable reference types in your projects? Tell me in the comments.
References
- Nullable reference types — Microsoft Learn: null-state analysis, annotations and the nullable context.
- Attributes for null-state static analysis —
[NotNullWhen],[NotNull],[MaybeNullWhen],[MemberNotNull]and the others. - Null-forgiving operator (!) — what
!does and does not do. - Nullable migration strategies — enabling nullable reference types in an existing code base.
- Preprocessor directives —
#nullable enable,disableandrestore. - Compiler options for errors and warnings —
WarningsAsErrorsand thenullableshorthand. - Resolve nullable warnings — the CS86xx warnings and how to fix each one.
- Nullable value types —
int?andNullable<T>. - required modifier — making callers initialize a property.