Nullable Reference Types (NRT), introduced in C# 8 and refined through modern .NET, transform NullReferenceException from a terrifying runtime production crash into a deterministic compile-time check. By treating all reference types as non-nullable by default, the C# compiler forces developers to explicitly declare where null is a valid domain state using ?.
C# Nullable Reference Analysis:
├── string Name ──► Compiler guarantee: Must never be null (SQL: NOT NULL)
└── string? MiddleName ──► Nullable: Must perform null check before member access (SQL: NULL)
Runtime Reality:
Both compile to System.String at the IL level.
NRT is compile-time static analysis metadata, NOT a new CLR runtime type.
New to this → start with Enabling NRT. Compiler attributes → C# Nullable Attributes. EF Core mapping → EF Core and NRT. Interview prep → If an interviewer asks.
Real-world analogy
Imagine a medical prescription pad:
- A field with a red star (non-nullable
string) means the pharmacist will refuse to dispense medication if the dosage is blank. - A field without a star (nullable
string?) indicates optional allergy notes. - The null-forgiving operator (
!) is a counterfeit approval stamp. It forces the pharmacist to accept the document without looking, but if the dosage was blank, the patient still suffers a crisis at runtime.
Enabling Nullable Reference Types in csproj
Ensure <Nullable>enable</Nullable> is configured in your project files or Directory.Build.props:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>CS8618</WarningsNotAsErrors> <!-- Optional: soften constructor warnings during migration -->
</PropertyGroup>
</Project>
Essential C# Nullable Attributes
Often, helper methods perform null validation that the Roslyn compiler's static flow analysis cannot deduce on its own. Use System.Diagnostics.CodeAnalysis attributes to inform the compiler:
1. [NotNullWhen(true)] for Try-patterns
using System.Diagnostics.CodeAnalysis;
public static class CacheExtensions
{
public static bool TryGetCachedUser(
this IMemoryCache cache,
string key,
[NotNullWhen(true)] out UserDto? user)
{
if (cache.TryGetValue(key, out var raw) && raw is UserDto dto)
{
user = dto;
return true;
}
user = null;
return false;
}
}
// Caller usage: No compiler warnings inside the if-block!
if (cache.TryGetCachedUser("user:101", out var user))
{
// Compiler knows 'user' is guaranteed NOT NULL here!
Console.WriteLine(user.Email.ToUpper());
}
2. [MemberNotNull] for Initialization Methods
public class OrderProcessor
{
private PaymentClient _paymentClient; // Non-nullable field
public OrderProcessor()
{
InitializeDependencies();
}
[MemberNotNull(nameof(_paymentClient))]
private void InitializeDependencies()
{
_paymentClient = new PaymentClient();
}
}
3. [NotNullIfNotNull(nameof(input))] for Transformers
[return: NotNullIfNotNull(nameof(input))]
public static string? SanitizeInput(string? input)
{
return input?.Trim().ToLowerInvariant();
}
// If you pass non-null, compiler returns non-null:
string clean = SanitizeInput(" Admin "); // Type is string, no warning!
string? maybe = SanitizeInput(null); // Type is string?
Modern C# 11+ required properties and DTOs
In legacy C#, non-nullable properties on DTOs threw CS8618 unless assigned a dummy empty string ("") or null!. Modern C# solves this with the required keyword:
// DTO with C# required properties
public sealed record RegisterUserRequest
{
// Required properties must be provided during object initialization
public required string Email { get; init; }
public required string FullName { get; init; }
public string? PhoneNumber { get; init; } // Optional
}
// Deserializer (System.Text.Json) enforces required properties at runtime!
EF Core entity mapping with Nullable Reference Types
EF Core 8/9/10 maps C# NRT annotations directly to database constraints:
public class Customer
{
public Guid Id { get; set; }
// Generates: [FirstName] NVARCHAR(100) NOT NULL
public string FirstName { get; set; } = string.Empty;
// Generates: [LastName] NVARCHAR(100) NOT NULL
public string LastName { get; set; } = string.Empty;
// Generates: [MiddleName] NVARCHAR(100) NULL
public string? MiddleName { get; set; }
// Navigation property: Loaded via .Include(c => c.Orders)
public List<Order> Orders { get; set; } = [];
}
Handling EF Core Navigation Warnings:
For 1-to-1 required relationships where EF Core instantiates an entity without loading children, declare the navigation as:
// EF Core binds this via reflection during queries
public CustomerProfile Profile { get; set; } = null!;
Here, null! is legitimate because EF Core navigation properties are populated by the query pipeline when included.
Common mistakes and pitfalls
- Using
null!to silence runtime validation warnings: Writingpublic string Email { get; set; } = null!;on an API request model tells the compiler Email is never null, but when a client sends{ "email": null }, your code will crash onEmail.Trim()at runtime. Use[Required]or FluentValidation. - Assuming NRT stops nulls from external libraries: If a legacy .NET Framework package or un-annotated library returns a reference, it may still return
nulleven if typed asstring. Guard external boundaries with defensive null checks. - Overusing
?on everything: Marking all properties as nullable (string?) defeats the benefits of NRT. Make domain properties non-nullable by default, and only use?whennullrepresents a valid business state (e.g.DeactivatedAt).
If an interviewer asks
30-second answer: Nullable Reference Types (NRT) is a C# compile-time static analysis feature that treats reference types as non-nullable by default. It catches potential NullReferenceException bugs at build time through Roslyn flow analysis and attributes ([NotNullWhen], [MemberNotNull]), while compiling to identical CLR IL without runtime performance overhead.
Strong answer: In production .NET architectures, enabling <Nullable>enable</Nullable> aligns compile-time type safety with database schema constraints in EF Core. We enforce non-nullability at system boundaries using C# required properties and FluentValidation, reserve ? exclusively for valid domain optionality, and avoid the null-forgiving operator (null!) except for EF Core navigation backings. This eliminates the "billion-dollar mistake" across the entire API and domain layer.