Nullable Reference Types
Nullable reference types (NRTs) have been in C# since version 8, and at first glance, the concept seems simple:
Just turn on the feature and use ?
to indicate a nullable reference.
But in real-world code, especially in APIs, libraries, and layered architectures, it’s not enough. In this post, we’ll explore:
- What NRTs actually do
- Where they fall short
- How to guide the compiler using attributes
- And how to use NRTs to write safer, clearer code
Step 1: Enabling Nullable Reference Types
To turn on NRTs, add this to your project file:
<Nullable>enable</Nullable>
Now, the compiler will warn you if you dereference a variable that might be null.
string? name = GetName(); // nullable
Console.WriteLine(name.Length); // ⚠️ Warning: possible null reference
But what happens when things get more complex?
Step 2: Where Things Get Tricky
public string GetValue(bool isValid)
{
if (isValid)
{
return "Hello";
}
return null; // ⚠️ Warning
}
Or in methods where nullability depends on external logic:
string? GetNameFromDb() => ...;
void GreetUser()
{
var name = GetNameFromDb();
if (!string.IsNullOrEmpty(name))
{
Console.WriteLine($"Hello {name}"); // ⚠️ Still warns
}
}
Despite our null check, the compiler can’t prove name is not null here.
Step 3: Use Attributes to Help the Compiler
C# provides nullable annotations in System.Diagnostics.CodeAnalysis
to inform the compiler of intent and flow.
[NotNullWhen(true)]
Used on method parameters to tell the compiler a value will be non-null when the condition is true.
bool TryGetName([NotNullWhen(true)] out string? name)
{
name = "Moran";
return true;
}
if (TryGetName(out var result))
{
Console.WriteLine(result.Length); // ✅ No warning
}
[MaybeNull]
Use when a method returns a non-nullable type but might return null anyway, Without this, the compiler expects a guaranteed non-null string.
[return: MaybeNull]
public string GetCachedItem()
{
return null;
}
[MemberNotNull]
Use in methods that ensure one or more class members are not-null after execution, Now you can safely use _connection after calling Initialize().
private string? _connection;
[MemberNotNull(nameof(_connection))]
void Initialize()
{
_connection = "Ready!";
}
When to Use These Attributes?
Use them when:
- You’re implementing custom TryX patterns
- You initialize members conditionally
- You return null for performance reasons
- Your API has known behaviors the compiler can’t infer
Final Thoughts
Nullable reference types are one of the most misunderstood features in C# — not because they’re hard, but because they require you to think like the compiler. By combining the ? operator with flow-aware attributes, you give the compiler the tools it needs to truly help you prevent bugs.