C# 8.0: Nullable Reference Types in Practice

The “Billion Dollar Mistake”—null content—has plagued C# developers since version 1.0. NullReferenceException is the most common runtime error. C# 8.0 introduces Nullable Reference Types (NRT), a feature that moves null checking from runtime to compile time. This is not just a syntax change; it’s a fundamental shift in how we write safety-critical C# code.

How NRT Works

Traditionally, reference types (string, class) were nullable by default. In C# 8.0 with NRT enabled:

  • string is non-nullable.
  • string? is nullable.
<PropertyGroup>
    <Nullable>enable</Nullable>
    <WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
</PropertyGroup>

The Static Analysis Engine

The compiler now tracks the “null-state” of variables flow-sensitively.

public void ProcessUser(User? user)
{
    // user is "maybe-null" here
    // Console.WriteLine(user.Name); // ❌ Compiler Warning: CS8602

    if (user != null)
    {
        // user is "not-null" here (Smart Cast)
        Console.WriteLine(user.Name); // ✅ Safe
    }
}

Advanced Annotations

Sometimes the compiler’s static analysis isn’t smart enough, especially with helper methods or generics. We use attributes from System.Diagnostics.CodeAnalysis.

[NotNullWhen]

public static bool TryGetConfig(string key, [NotNullWhen(true)] out string? value)
{
    if (_config.ContainsKey(key))
    {
        value = _config[key]; // value is not null here
        return true;
    }
    value = null;
    return false;
}

// Usage
if (TryGetConfig("db", out string? connection)) 
{
    // Compiler knows 'connection' is not null because result was true
    Connect(connection); 
}

[AllowNull] and [DisallowNull]

Useful for properties where get/set have different nullability contract.

public class Cache
{
    [AllowNull] // Setter accepts null (to clear cache)
    [return: MaybeNull] // Getter might return null
    public string LastValue
    {
        get => _cached ?? null;
        set => _cached = value ?? "";
    }
}

Migration Strategy for Large Codebases

Turning on <Nullable>enable</Nullable> on a million-line legacy codebase will generate 10,000 warnings. Don’t do it all at once.

  • Step 1: Enable for new projects only.
  • Step 2: Use #nullable enable directives at the top of actively edited files.
  • Step 3: Use the “Nullable” property in Directory.Build.props to toggle per-project.

Entity Framework Core Pitfalls

EF Core requires special handling because properties are initialized via reflection, not constructors.

public class Customer
{
    public int Id { get; set; }
    
    // ❌ Warning: Non-nullable string must contain value
    // public string Name { get; set; }

    // ✅ Solution 1: Default!
    public string Name { get; set; } = default!;

    // ✅ Solution 2: Constructor binding
    public Customer(string name) => Name = name;
}

Key Takeaways

  • NRT isn’t just metadata; it’s robust static analysis.
  • Use attributes like [NotNullWhen] to help the compiler.
  • Adopt incrementally using #nullable enable.
  • Be careful with serialization and EF Core; use default! (the null-forgiving operator) responsibly.

Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.