| name | dotnet-csharp-nullable-reference-types |
| description | Enables nullable reference types. Annotation strategies, migration, common agent mistakes. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
dotnet-csharp-nullable-reference-types
Nullable reference type (NRT) annotation strategies, migration guidance for legacy codebases, and the most common annotation mistakes AI agents make. NRT is enabled by default in all modern .NET templates (net6.0+), but many existing codebases still need migration.
Scope
- NRT annotation strategies and nullable context configuration
- Migration guidance for legacy codebases
- Nullable attributes (MaybeNull, NotNull, etc.)
- Common AI agent NRT annotation mistakes
Out of scope
- Null-handling style (pattern matching, null-conditional) -- see [skill:dotnet-csharp-coding-standards]
- Pattern matching language features -- see [skill:dotnet-csharp-modern-patterns]
Cross-references: [skill:dotnet-csharp-coding-standards] for null-handling style, [skill:dotnet-csharp-modern-patterns] for pattern matching with nulls.
Quick Reference: NRT Defaults by TFM
| TFM | <Nullable> default | Notes |
|---|
| net8.0+ | enable (in templates) | New projects have NRT enabled by default |
| net6.0/net7.0 | enable (in templates) | Same as net8.0 |
| netstandard2.0/2.1 | not set | Must opt in explicitly |
| net48 / older | not set | Must opt in explicitly |
Important: The TFM does not enforce NRT -- the <Nullable>enable</Nullable> MSBuild property does. Legacy projects upgraded to net8.0 may not have it enabled.
Enabling NRT
Project-Wide (Recommended)
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
Per-File (Migration)
#nullable enable // top of file -- enables NRT for this file only
Migration Strategy
For large codebases, enable NRT incrementally:
- Set
<Nullable>enable</Nullable> in the project
- Add
#nullable disable at the top of every existing file (script or IDE tooling)
- Remove
#nullable disable file-by-file, fixing warnings as you go
- Track progress: count remaining
#nullable disable directives
Annotation Patterns
Nullable and Non-Nullable
public class UserService
{
private readonly IUserRepository _repo;
public User? FindByEmail(string email)
{
return _repo.FindByEmail(email);
}
public async Task<User> GetByIdAsync(int id, CancellationToken ct = default)
{
return await _repo.GetByIdAsync(id, ct)
?? throw new NotFoundException($"User {id} not found");
}
}
Nullable Attributes
Use attributes from System.Diagnostics.CodeAnalysis to express nullability contracts the compiler cannot infer:
using System.Diagnostics.CodeAnalysis;
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
value = _dict.GetValueOrDefault(key);
return value is not null;
}
public class Connection
{
public string? ConnectionString { get; private set; }
[MemberNotNull(nameof(ConnectionString))]
public void Initialize(string connectionString)
{
ConnectionString = connectionString
?? throw new ArgumentNullException(nameof(connectionString));
}
}
[return: NotNullIfNotNull(nameof(input))]
public static string? Trim(string? input)
{
return input?.Trim();
}
public static void ()
{
( )
{
ArgumentNullException(paramName);
}
}
[]
{
NotFoundException(message);
}
Common Attributes Summary
| Attribute | Where | Meaning |
|---|
[NotNullWhen(true)] | out parameter | Non-null when method returns true |
[NotNullWhen(false)] | out parameter | Non-null when method returns false |
[MemberNotNull] | method | Named member is non-null after call |
[MemberNotNullWhen(true)] | method | Named member is non-null when returns true |
[NotNullIfNotNull] | return | Return is non-null if named param is non-null |
[NotNull] | parameter | Parameter is non-null after call (assertion) |
[DoesNotReturn] | method | Method never returns (always throws) |
[AllowNull] | parameter/property | Caller may pass null even if type is non-nullable |
[DisallowNull] | parameter/property | Caller must not pass null even if type is nullable |
[MaybeNull] | return/out | Return may be null even if type is non-nullable |
[MaybeNullWhen(false)] | out parameter | May be null when method returns false |
Agent Gotchas
These are the most common NRT mistakes AI agents make when generating C# code.
1. Using ! (Null-Forgiving Operator) to Silence Warnings
var user = _repo.FindByEmail(email)!;
string name = user!.Name!;
var user = _repo.FindByEmail(email)
?? throw new NotFoundException($"User with email {email} not found");
The ! operator should only be used when you have knowledge the compiler cannot verify (e.g., after a debug assertion, in test code with known data).
2. Ignoring Nullable Warnings
public string GetDisplayName(User? user)
{
return user.Name;
}
public string GetDisplayName(User? user)
{
return user?.Name ?? "Unknown";
}
3. Wrong Nullability on Interface Implementations
public interface IRepository
{
User? FindById(int id);
}
public class UserRepository : IRepository
{
public User FindById(int id)
{
return _db.Users.First(u => u.Id == id);
}
}
public class UserRepository : IRepository
{
public User? FindById(int id)
{
return _db.Users.FirstOrDefault(u => u.Id == id);
}
}
4. Missing [NotNullWhen] on Try-Pattern Methods
public bool TryParse(string input, out Order? result)
{
}
public bool TryParse(string input, [NotNullWhen(true)] out Order? result)
{
}
5. Nullable Value Types vs Nullable Reference Types Confusion
int? nullableInt = null;
string? nullableStr = null;
Generic Constraints for Nullability
public class Repository<T> where T : class
{
public T Get(int id) => ...;
public T? Find(int id) => ...;
}
public class Cache<T> where T : notnull
{
public T GetOrDefault(string key, T defaultValue) => ...;
}
public class Wrapper<T>
{
public T? Value { get; set; }
}
Collections and Nullability
Dictionary<string, User> users = new();
if (users.TryGetValue(key, out var user))
{
}
List<string?> names = ["Alice", null, "Bob"];
foreach (var name in names)
{
if (name is not null)
{
Console.WriteLine(name.Length);
}
}
IReadOnlyList<Order> orders = GetOrders();
Order? first = orders.FirstOrDefault();
EF Core and NRT
EF Core respects NRT annotations for required vs optional columns:
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = "";
public string? Notes { get; set; }
public Address Address { get; set; } = null!;
}
Note: = null! is acceptable for EF Core navigation properties where EF guarantees initialization. This is one of the few valid uses of the null-forgiving operator.
References