| name | csharp-nullable-types |
| user-invocable | false |
| description | Use when C# nullable reference types, null safety patterns, and migration strategies. Use when ensuring null safety in C# code. |
| allowed-tools | ["Bash","Read","Write","Edit"] |
C# Nullable Types
Master nullable reference types, null safety patterns, and migration strategies
in C# 8+. This skill covers nullable value types, nullable reference types,
null-safety annotations, operators, and best practices for writing null-safe code.
Nullable Reference Types (C# 8+)
Nullable reference types provide compile-time null safety by distinguishing
between nullable and non-nullable reference types.
Enabling Nullable Context
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
#nullable enable
public class User
{
public string Name { get; set; } = string.Empty;
public string? MiddleName { get; set; }
public string Email { get; set; }
public User(string email)
{
Email = email;
}
}
#nullable disable
public class LegacyClass
{
public string Name { get; set; }
}
#nullable restore // Return to project default
Non-nullable and Nullable References
#nullable enable
public class PersonService
{
public string FormatName(string firstName, string lastName)
{
return $"{firstName} {lastName}";
}
public string FormatNameWithMiddle(string firstName, string? middleName, string lastName)
{
if (middleName != null)
{
return $"{firstName} {middleName} {lastName}";
}
return $"{firstName} {lastName}";
}
public string? FindUserEmail(int userId)
{
var user = _repository.Find(userId);
return user?.Email;
}
public string GetUpperName(string? name)
{
name.ToUpper();
}
{
(name == )
{
ArgumentNullException((name));
}
name.ToUpper();
}
}
Nullable Value Types
Value types can be made nullable using Nullable<T> or the ? syntax.
Nullable<T> and T?
public class NullableValueTypes
{
public int? Age { get; set; }
public DateTime? BirthDate { get; set; }
public decimal? Salary { get; set; }
public bool? IsActive { get; set; }
public Nullable<int> AgeVerbose { get; set; }
public void WorkWithNullables()
{
int? value = null;
if (value.HasValue)
{
int actualValue = value.Value;
Console.WriteLine(actualValue);
}
int result1 = value.GetValueOrDefault();
int result2 = value.GetValueOrDefault(42);
int result3 = value ?? 100;
}
()
{
(!birthDate.HasValue)
{
ArgumentException(, (birthDate));
}
DateTime.Now.Year - birthDate.Value.Year;
}
}
Nullable Value Type Operations
public class NullableOperations
{
public void ArithmeticOperations()
{
int? a = 5;
int? b = 10;
int? c = null;
int? sum = a + b;
int? nullSum = a + c;
bool? equal = a == b;
bool? nullEqual = a == c;
int? result = (a > 0) ? a * 2 : null;
}
public decimal? CalculateDiscount(decimal? price, decimal? discountPercent)
{
return price * (1 - discountPercent / 100);
}
public void BooleanLogic()
{
bool? a = true;
bool? b = false;
bool? c = null;
bool? and1 = a & b;
? and2 = a & c;
? and3 = b & c;
? or1 = a | b;
? or2 = a | c;
? or3 = b | c;
}
}
Null Safety Annotations
Attributes that provide additional null-safety information to the compiler.
Common Annotations
using System.Diagnostics.CodeAnalysis;
public class AnnotationExamples
{
public void ProcessUser([NotNull] User? user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
Console.WriteLine(user.Name);
}
[return: MaybeNull]
public T GetValueOrDefault<T>(string key)
{
if (_dictionary.TryGetValue(key, out var value))
{
return value;
}
return default;
}
public bool TryGetUser(int id, [NotNullWhen(true)] out User? user)
{
user = _repository.Find(id);
return user != null;
}
public void ()
{
(TryGetUser(id, user))
{
Console.WriteLine(user.Name);
}
}
[]
? ProcessString(? )
{
?.Trim().ToUpperInvariant();
}
[]
{
InvalidOperationException(message);
}
{
(user == )
{
ThrowError();
}
Console.WriteLine(user.Name);
}
}
MemberNotNull Annotation
public class InitializationExample
{
private string _name;
private string _email;
public InitializationExample()
{
Initialize("Default", "default@example.com");
}
[MemberNotNull(nameof(_name), nameof(_email))]
private void Initialize(string name, string email)
{
_name = name;
_email = email;
}
[MemberNotNull(nameof(_name), nameof(_email))]
public void Reset()
{
_name = string.Empty;
_email = string.Empty;
}
}
Null-Forgiving Operator
The null-forgiving operator (!) suppresses nullable warnings when you know
better than the compiler.
Using the ! Operator
public class NullForgivingExamples
{
private User? _currentUser;
public void Initialize()
{
_currentUser = LoadUser();
}
public void ProcessCurrentUser()
{
Console.WriteLine(_currentUser!.Name);
}
public string GetUserName()
{
return _currentUser!.Name;
}
public string GetUserNameSafe()
{
if (_currentUser == null)
{
throw new InvalidOperationException("User not initialized");
}
return _currentUser.Name;
}
public void DictionaryPattern()
{
var dict = new Dictionary<string, User>();
dict["key"] = new User("test@example.com");
var user = dict[];
Console.WriteLine(user.Email);
(dict.TryGetValue(, foundUser))
{
Console.WriteLine(foundUser!.Email);
}
}
}
When NOT to Use the Null-Forgiving Operator
public class BadNullForgiving
{
public void ProcessData(string? input)
{
var result = input!.ToUpper();
}
public void ProcessDataSafe(string? input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
var result = input.ToUpper();
}
public User GetUser(int id)
{
return _repository.Find(id)!;
}
public User GetUserSafe(int id)
{
return _repository.Find(id)
?? throw new KeyNotFoundException($"User {id} not found");
}
}
Null-Conditional Operators
Safe navigation operators for accessing members that might be null.
?. and ?[] Operators
public class NullConditionalExamples
{
public void SafeNavigation()
{
User? user = GetUser();
string? name = user?.Name;
string? city = user?.Address?.City;
char? firstChar = user?.Name?[0];
int? nameLength = user?.Name?.Length;
string displayName = user?.Name ?? "Guest";
int? result = user?.CalculateAge();
}
public void ArrayAndCollectionAccess()
{
int[]? numbers = GetNumbers();
int? first = numbers?[0];
int? max = numbers?.Max();
Dictionary<string, User>? users = GetUsers();
User? user = users?["key"];
}
public void InvocationExamples()
{
Action? callback = GetCallback();
callback?.Invoke();
(callback != )
{
callback.Invoke();
}
EventHandler? handler = SomeEvent;
handler?.Invoke(, EventArgs.Empty);
}
}
Null-Coalescing Operators
The ?? and ??= operators provide default values for null expressions.
?? Operator
public class NullCoalescingExamples
{
public void BasicCoalescing()
{
string? name = GetName();
string displayName = name ?? "Unknown";
string result = GetPrimaryName()
?? GetSecondaryName()
?? GetDefaultName()
?? "Fallback";
int? nullableValue = GetValue();
int value = nullableValue ?? 0;
int length = user?.Name?.Length ?? 0;
}
public User GetUserOrDefault(int id)
{
return _repository.Find(id) ?? new User("guest@example.com");
}
public string GetConfigValue(string key, string defaultValue)
{
return _config[key] ?? defaultValue;
}
}
??= Operator (Null-Coalescing Assignment)
public class NullCoalescingAssignment
{
private User? _cachedUser;
private List<string>? _items;
public User GetUser(int id)
{
_cachedUser ??= LoadUser(id);
return _cachedUser;
}
public void EnsureListInitialized()
{
_items ??= new List<string>();
_items.Add("item");
}
public void UpdateNameIfNull(User user)
{
user.MiddleName ??= "N/A";
}
public void OldWay()
{
if (_items == null)
{
_items = new List<string>();
}
_items = _items ?? new List<string>();
}
}
Pattern Matching with Null
C# 9+ pattern matching enhancements for null checking.
Null Pattern Matching
public class PatternMatchingExamples
{
public void IsPatterns()
{
object? obj = GetObject();
if (obj is null)
{
Console.WriteLine("Object is null");
}
if (obj is not null)
{
Console.WriteLine("Object is not null");
}
if (obj is string s)
{
Console.WriteLine(s.ToUpper());
}
if (obj is User { Name: not null } user)
{
Console.WriteLine(user.Name);
}
}
public string GetDescription(User? user) => user switch
{
null => "No user",
{ Name: null } => "User without name",
{ Name: var name } => $"User: {name}"
};
public void RecursivePatterns()
{
Order? order = GetOrder();
status = order
{
=> ,
{ Customer: } => ,
{ Customer.Address: } => ,
{ Customer.Address.City: city } => ,
};
}
}
Migration Strategies
Gradually migrate existing code to nullable reference types.
Incremental Migration
<PropertyGroup>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
#nullable enable
public class MigratedClass
{
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
#nullable disable
public class LegacyClass
{
public string Name { get; set; }
}
#nullable restore
Migration Patterns
public class MigrationPatterns
{
#nullable disable
public string GetUserName(User user)
{
return user.Name;
}
#nullable restore
#nullable enable
public string GetUserNameNullable(User? user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return user.Name ?? throw new InvalidOperationException("Name is required");
}
#nullable disable
public void ProcessData(string data, string format)
{
format = format ?? "json";
}
#nullable restore
#nullable enable
public void ProcessDataNullable(string data, string? format = null)
{
format ??= ;
}
{
_repository.Find(id);
}
User? FindUserNullable( id)
{
_repository.Find(id);
}
}
Compiler Warnings and Strictness
Understanding and configuring nullable warning levels.
Warning Levels
<PropertyGroup>
<Nullable>enable</Nullable>
<!-- Treat nullable warnings as errors -->
<WarningsAsErrors>CS8600;CS8601;CS8602;CS8603;CS8604</WarningsAsErrors>
<!-- Or treat all nullable warnings as errors -->
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
#nullable enable
public class WarningExamples
{
public string Name { get; set; } = string.Empty;
public string GetName(User? user)
{
return user?.Name ?? string.Empty;
}
public int GetLength(string? value)
{
return value?.Length ?? 0;
}
#pragma disable CS8602
{
Console.WriteLine(.Length);
}
}
Nullable in Generic Constraints
Handling nullability in generic type parameters.
Generic Nullable Constraints
#nullable enable
public class GenericNullability
{
public T? FindOrDefault<T>(int id)
{
var result = _repository.Find<T>(id);
return result;
}
public T Create<T>(string name) where T : class, new()
{
var instance = new T();
return instance;
}
public void Process<T>(T? value) where T : class
{
if (value == null)
{
return;
}
Console.WriteLine(value.ToString());
}
public T GetValue<T>() where T : struct
{
return default;
}
{
Console.WriteLine(.ToString());
}
}
Nullable Generic Patterns
public class Repository<T> where T : class
{
private readonly Dictionary<int, T> _cache = new();
public T? Find(int id)
{
_cache.TryGetValue(id, out var result);
return result;
}
public T Get(int id)
{
return _cache[id];
}
public bool TryGet(int id, [NotNullWhen(true)] out T? result)
{
return _cache.TryGetValue(id, out result);
}
}
public class NullableGenericList<T>
{
private readonly List<T> _items = new();
public T? FirstOrDefault()
{
return _items.Count > 0 ? _items[0] : default;
}
public T? Find(Predicate<T> predicate)
{
( item _items)
{
(predicate(item))
{
item;
}
}
;
}
}
Best Practices
- Enable Nullable Globally: Use
<Nullable>enable</Nullable> in .csproj
- Explicit Nullability: Make nullability intentions clear in APIs
- Validate at Boundaries: Check for null at public API boundaries
- Use Null-Conditional Operators: Prefer ?. over explicit null checks
- Avoid Null-Forgiving: Use ! sparingly and only when truly necessary
- Return Non-Nullable: Prefer non-nullable return types when possible
- Use Annotations: Apply [NotNull], [MaybeNull] etc. appropriately
- Constructor Initialization: Initialize non-nullable properties in constructors
- Throw on Invalid State: Throw exceptions for unexpected nulls
- Gradual Migration: Migrate file-by-file with #nullable directives
Common Pitfalls
- Overusing !: Suppressing warnings instead of fixing root cause
- Not Initializing: Forgetting to initialize non-nullable properties
- Silent Failures: Not handling null cases in public APIs
- Mixing Contexts: Inconsistent nullable/disable throughout codebase
- Ignoring Warnings: Treating warnings as noise instead of issues
- Null Return Types: Returning null without nullable return type
- Unchecked Parameters: Not validating nullable parameters
- Generic Confusion: Misunderstanding T? in generic methods
- Legacy Assumptions: Assuming all references can be null
- False Confidence: Trusting ! operator without verification
When to Use
Use this skill when:
- Writing null-safe C# code
- Migrating to nullable reference types
- Preventing NullReferenceExceptions
- Designing clear APIs with explicit nullability
- Working with optional values
- Implementing defensive programming
- Refactoring legacy code
- Setting up new C# projects
- Enforcing null safety at compile time
- Working with generic nullable types
Resources