Make Nullability Explicit in .NET with Nullable Reference Types
NullReferenceException remains common because reference types were nullable by default. Enabling nullable reference types in C# shifts null checks to compile time, with adoption patterns, a worked DTO example, and trade‑offs for .NET teams.
21 Jun 2026, 04:12 UTC

NullReferenceException is still the most common runtime failure in .NET codebases. The problem is not that developers forget null checks, it is that the type system historically allowed null for every reference type. Enabling nullable reference types changes the default so nullability is part of the signature, and the compiler warns you before the exception happens.
The concrete problem: silent nulls in public APIs
In C# before version 8, string could be null and the compiler did not distinguish that from a guaranteed non‑null string. A method like Process(string input) could be called with null, and the failure only appeared at runtime when input.Length was accessed.
Nullable reference types, introduced in C# 8.0 with .NET Core 3.0+, make the default non‑nullable in an enabled context. T means “should not be null”, T? means “may be null”. The compiler tracks flow and emits warnings for possible dereference and for assignments that may introduce null.
How the nullable context works
Nullability is controlled by context, not by a global flag alone. You can enable it per project and per file.
In the project file, set:
<PropertyGroup>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
This applies to all source files in the project. Inside a file you can toggle with directives:
#nullable enable
#nullable disable
#nullable restore
With the context enabled, the compiler treats unannotated reference types as non‑nullable and requires ? where null is allowed. Warnings are CS8600, CS8602, CS8604 and related diagnostics for possible null assignments and dereferences.
An adoption pattern that reduces risk
Enabling nullable across a large solution at once creates noise. A practical engineering decision is to start at the boundaries.
- Enable nullable on new projects with
<Nullable>enable</Nullable>. - For existing code, set
<Nullable>warnings</Nullable>first, then annotate public API types and DTOs. - Use
stringfor required inputs and return values,string?for optional inputs. - Propagate nullability through signatures. If a parameter is
string?, the method must handle null or document the contract.
Domain models are a good first target because they define the contract for the rest of the code.
Worked example: DTO and processing contract
Consider a simple transfer object with a required name and an optional nickname.
#nullable enable
public class PersonDto
{
public string Name { get; set; } = default!;
public string? Nickname { get; set; }
}
public static class Processor
{
public static void Process(string input)
{
// Compiler warns if input is possibly null
Console.WriteLine(input.Length);
}
public static void ProcessOptional(string? input)
{
if (input is null) return;
Console.WriteLine(input.Length); // No warning after null check
}
}
With nullable enabled, assigning null to Name produces a warning, while Nickname accepts null without warning. Calling Process with a value of type string? produces a warning about a possible null argument. The API contract is now visible in the signature without runtime changes.
To verify the effect locally, create a minimal console project with Nullable enabled, add a method that assigns null to a non‑nullable parameter, and build. The compiler output will list the relevant CS warnings. Toggling #nullable disable around the same code shows the warnings disappear, confirming the context control.
Trade‑off and limitation
Nullable reference types are compile‑time analysis only. They do not enforce nullability at runtime. Unsafe interop, reflection, or casts can still introduce nulls, and the runtime behavior is unchanged.
Migration cost grows with external dependencies. Libraries compiled without nullable annotations appear as if all reference types are nullable, which can generate false positives. Common mitigations are explicit null checks at boundaries, suppression attributes like [AllowNull] or [NotNull], and using #nullable restore around interop sections.
Because warnings are opt‑in per project, teams must decide whether to treat them as errors via <WarningsAsErrors> to enforce the contract.
Actionable closing
Enable nullable reference types on new .NET projects and adopt it incrementally on existing ones starting with public APIs and DTOs. Annotate required members as non‑nullable and optional members as ?, propagate the annotations through method signatures, and treat compiler warnings as design feedback. Check the result by inspecting build warnings after enabling the context and by reviewing public signatures for consistent nullability.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.