Incrementally Adopting Nullable Reference Types in an Existing C# Codebase
Most NullReferenceExceptions are contract failures, not logic bugs. Here is how to enable nullable reference types per project, annotate boundaries honestly, and treat compiler warnings as a migration backlog instead of a big-bang rewrite.
13 Aug 2026, 05:52 UTC

The Problem: NullReferenceException Is Usually a Contract Failure
Most NullReferenceExceptions are not exotic logic bugs. They happen because a method returned null and the caller had no way to know that was allowed. The API contract was implicit, and the runtime paid the price.
C# nullable reference types change that. They let you declare whether a reference is expected to exist, moving many of those failures from runtime to compile time. The catch: they are compiler warnings and metadata, not runtime enforcement. They do not stop null from flowing into your application; they make the intent visible so you can fix it deliberately.
Enable Nullable Analysis One Project at a Time
You do not need to flip a switch for the entire solution. In a project file (.csproj), add the following inside a <PropertyGroup>:
<Nullable>enable</Nullable>
If you want finer control, place #nullable enable at the top of individual files, or use <Nullable>warnings</Nullable> to surface warnings without requiring annotations. Start with new code, a leaf library, or one assembly. Leave the rest of the solution in a disabled or warnings-only context until you have triaged the backlog. A big-bang rewrite across hundreds of files usually produces thousands of warnings that nobody fixes.
Annotate Public Boundaries Honestly
Once nullable is enabled, annotate your public surface truthfully. Use string? for values that can be absent and string for values that must exist. If a method can return nothing, say so:
public Customer? FindById(int id);
For public methods that accept non-nullable references, still add an explicit runtime guard. Callers may ignore annotations, come from unannotated assemblies, or use reflection. The annotation documents intent; the guard enforces it.
Use Compiler Flow Analysis, Not Defensive Checks Everywhere
After if (customer is null) return; or customer?.Name, the compiler tracks that customer is non-null in the following scope. You rarely need to add redundant checks. Reserve the null-forgiving operator ! for cases where an invariant is obvious to a human reader but invisible to the compiler—for example, after a helper method that the compiler cannot analyze.
A Worked Example
Consider a repository method that may not find a record, and a display method that must always return a string:
public Customer? FindById(int id) =>
_customers.FirstOrDefault(c => c.Id == id);
public string GetDisplayName(int id)
{
var customer = FindById(id);
if (customer is null) return "Unknown";
return customer.Name;
}
The first method admits absence. The second converts absence into a defined result. The compiler can prove customer.Name is safe on the last line because the preceding if statement already handled the null case.
Limitations: Warnings Are a Signal, Not Proof
Nullable analysis is version- and context-sensitive. It requires a C# 8 or later compiler, and exact warning behavior depends on your language version, target framework, and referenced assemblies. Unannotated dependencies, generics, reflection, dynamic code, deserialization, and fields initialized later can produce false positives or hide real nulls.
Treat every warning as a prompt to investigate, not as a defect by itself. Blanket use of !, #pragma warning disable, or global warning filters defeats the analysis and can hide real defects. Changing public signatures to nullable can also introduce warnings for downstream consumers, so treat annotation changes as an API compatibility decision.
Actionable Close
Pick one project. Set <Nullable>enable</Nullable> in its .csproj and build with warnings visible. Configure the desired severities in .editorconfig, or set <TreatWarningsAsErrors>true</TreatWarningsAsErrors> temporarily to see how the rules would behave in CI. Add a guard for public non-null parameters. Then fix or suppress warnings deliberately, one at a time. Run the existing unit tests for that project to catch assumptions the compiler cannot see. Once the backlog is under control, repeat for the next assembly.
Verification is straightforward. Create a small console app with <Nullable>enable</Nullable> and a method returning string?. Dereference the result without a null check and confirm the build warns. Add an explicit guard and rebuild: the warning should disappear only after the compiler can prove the value is non-null.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.