Practical Null Safety with C# Nullable Reference Types
Nullable Reference Types in C# 8.0 move null safety from runtime to compile time, but require careful adoption at API boundaries and trust edges.
31 Aug 2025, 18:17 UTC

The Null Ambiguity Problem
In traditional C#, every reference type can be null. This means a string variable might hold text or might be absent, and the compiler won't stop you from assigning or dereferencing null. The result is the NullReferenceException, the most common runtime crash in C# applications.
Nullable Reference Types in C# 8.0
Nullable Reference Types (NRTs) make nullability explicit in the type system. Enable them with <Nullable>enable</Nullable> in your project file, and the compiler treats string as non-nullable and string? as nullable. It then warns when you might dereference a potentially null value or assign null to a non-nullable reference, moving many checks from runtime to compile time.
Incremental Adoption Pattern
Enabling NRTs globally on a legacy codebase can produce many warnings. A practical approach is to enable NRT as warnings first, annotate public API boundaries, and isolate legacy code with #nullable disable or file-level pragmas. This lets you gain safety where it matters most without an initial warning flood.
Worked Example: Records and Compiler Warnings
public record User(string Name, string? Email);
With NRT enabled, the compiler warns on new User(null, "[contact removed]") because Name is non-nullable. Accessing user.Email.Length also triggers a warning because Email is nullable. The safe pattern uses the null-conditional operator and null-coalescing:
var emailLength = user.Email?.Length ?? 0;
This provides a defined fallback without suppressing the warning. The null-forgiving operator ! can suppress warnings but provides no runtime check; overusing it re-introduces null defects.
The Trust Boundary Reality
NRTs are compile-time only. They do not prevent null values from entering your program through JSON deserialization, platform invoke, reflection, or unchecked casts. At trust boundaries, runtime validation is still required. For example, after deserializing a User with System.Text.Json, verify that Name is not null before passing it to business logic:
var user = JsonSerializer.Deserialize(json);
if (string.IsNullOrEmpty(user.Name)) throw new InvalidOperationException("Invalid user data");
Adoption Checklist
- Add
<Nullable>enable</Nullable>to your .csproj and target C# 8.0 or later. - Fix compiler warnings in public APIs first; use
#nullable disablein legacy modules if needed. - Use
?.and??to handle nullable members safely without silencing warnings. - Validate at module boundaries where external data enters your code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.