Articles › C#

Nullable Reference Types in C#: Eliminate NullReferenceException

Use nullable reference types to catch null bugs at compile time: enabling the feature, ? annotations, null-state analysis, operators, attributes, EF Core and DTO patterns, and migrating old code.

NullReferenceException is the most common runtime error in .NET history — Tony Hoare called the null reference his "billion-dollar mistake". Since C# 8, nullable reference types (NRT) let the compiler track which references can be null and warn you before the code runs. Enabled in every new project template, NRT is now a core part of writing reliable C#.

The idea in one example

With NRT enabled, reference types are non-nullable by default. You opt in to nullability with ?:

public class User
{
    public string Email { get; set; } = "";     // never null
    public string? MiddleName { get; set; }     // may be null
}

int Length(User user) => user.MiddleName.Length;
// ⚠️ CS8602: Dereference of a possibly null reference.

The compiler now knows MiddleName might be null and forces you to handle it:

int Length(User user) => user.MiddleName?.Length ?? 0;

Important: NRT is compile-time analysis only. It doesn't change runtime behavior — a string can still technically be null at runtime if it comes from unchecked code, JSON or reflection.

Enabling it

<PropertyGroup>
  <Nullable>enable</Nullable>
  <!-- optional: make nullable warnings errors -->
  <WarningsAsErrors>Nullable</WarningsAsErrors>
</PropertyGroup>

You can also enable it per file with #nullable enable — useful when migrating large codebases gradually.

Null-state analysis: the compiler follows your checks

The compiler tracks whether a variable is "maybe-null" or "not-null" through your code:

string? name = GetName();

if (name is null)
    return "anonymous";

return name.ToUpper();   // ✅ no warning: name is not-null here

It understands is null, is not null, == null, string.IsNullOrEmpty, pattern matching and early returns.

The null operators

string? city = customer.Address?.City;          // ?.  null-conditional
string display = city ?? "Unknown";             // ??  null-coalescing
settings.Theme ??= "dark";                      // ??= assign if null
string definitely = maybeNull!;                 // !   null-forgiving (use sparingly)

The null-forgiving operator ! tells the compiler "trust me, this isn't null". It silences warnings without adding any runtime check — if you're wrong, you still get a NullReferenceException. Use it only when you know something the compiler can't (and consider a comment explaining why).

Constructors, required members and initialization

Non-nullable properties must be initialized. Options:

// 1) Default value
public string Title { get; set; } = string.Empty;

// 2) Constructor
public class Article(string title) { public string Title { get; } = title; }

// 3) required members (C# 11) — callers must set it
public class CreateArticle
{
    public required string Title { get; init; }
    public string? Subtitle { get; init; }
}

var cmd = new CreateArticle { Title = "Nullable reference types" };   // ✅
var bad = new CreateArticle();                                         // ❌ compile error

required is often the cleanest choice for DTOs and options classes.

Helping the compiler with attributes

When your own methods check for null, attributes in System.Diagnostics.CodeAnalysis tell the compiler what they guarantee:

public static bool TryGetUser(int id, [NotNullWhen(true)] out User? user)
{
    user = _users.GetValueOrDefault(id);
    return user is not null;
}

if (TryGetUser(5, out var user))
    Console.WriteLine(user.Email);   // ✅ compiler knows user isn't null

public static void ThrowIfMissing([NotNull] object? value, string name)
{
    if (value is null) throw new ArgumentNullException(name);
}

Other useful attributes:

Attribute Meaning
[NotNullWhen(true)] argument is not null when the method returns true
[MaybeNullWhen(false)] output may be null when returning false
[NotNull] argument isn't null after the call returns
[MemberNotNull(nameof(Field))] method guarantees a field is initialized
[return: NotNullIfNotNull(nameof(input))] result isn't null if the input isn't

For argument validation, prefer the built-in helpers: ArgumentNullException.ThrowIfNull(value);.

Generics and nullability

public T? FindOrDefault<T>(IEnumerable<T> items, Func<T, bool> predicate) =>
    items.FirstOrDefault(predicate);   // T? = "may be default"

public void Store<T>(T value) where T : notnull { }   // reject nullable type arguments

Entity Framework Core and nullability

EF Core reads NRT annotations to decide column nullability:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;       // NOT NULL column
    public string? Description { get; set; }        // NULL column
    public Category Category { get; set; } = null!; // required navigation
    public int CategoryId { get; set; }
}

= null! is a common, accepted idiom for properties that EF Core sets when materializing entities: the compiler stops complaining, and the column is still NOT NULL. In EF Core 7+, required properties are another option.

APIs, JSON and DTOs

NRT doesn't validate incoming JSON — a client can still send null for a non-nullable property. Combine NRT with validation:

public record RegisterRequest(
    [property: Required] string Email,
    [property: Required, MinLength(8)] string Password,
    string? Referral);

ASP.NET Core with [ApiController] also treats non-nullable reference properties as implicitly required during model validation.

Migrating an existing project

  1. Enable <Nullable>enable</Nullable> and look at the warning count — don't panic.
  2. Fix leaf classes first (DTOs, entities, utilities), then services that use them.
  3. Annotate honestly: if something can be null, add ? — don't hide it with !.
  4. Use #nullable disable temporarily on files you can't fix yet.
  5. Turn nullable warnings into errors once the count reaches zero, so regressions can't sneak back.

Common mistakes

  1. Sprinkling ! everywhere — it defeats the purpose.
  2. Making everything nullable — then every caller needs null checks; prefer non-nullable with defaults.
  3. Trusting annotations at boundaries — data from JSON, databases or other libraries still needs validation.
  4. Returning null for collections — return an empty collection instead ([]).

Summary

Nullable reference types turn null bugs from runtime crashes into compile-time warnings. Enable them in every project, express intent with ?, initialize non-nullable members (defaults, constructors or required), let null-state analysis follow your checks, annotate helper methods with attributes, and use ! only when you truly know better than the compiler. The payoff is code that says exactly where null is allowed — and almost no NullReferenceExceptions.

FAQ

Does enabling NRT change how my program runs?

No. It's purely static analysis and annotations; the compiled IL behaves the same.

Why do I still get NullReferenceException with NRT on?

Warnings can be ignored or suppressed, and external data (JSON, reflection, older libraries) can contain nulls. Validate at boundaries.

Should collections be nullable?

Rarely. Prefer non-nullable collections initialized to empty, so callers never need to check for null.