Use when C# nullable reference types, null safety patterns, and migration strategies. Use when ensuring null safety in C# code.
Implements C# nullable reference types and null safety patterns for compile-time null checking.
/plugin marketplace add TheBushidoCollective/han/plugin install jutsu-cpp@hanThis skill is limited to using the following tools:
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 provide compile-time null safety by distinguishing between nullable and non-nullable reference types.
// Project-wide in .csproj
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// File-level directive
#nullable enable
public class User
{
// Non-nullable - must be initialized
public string Name { get; set; } = string.Empty;
// Nullable - can be null
public string? MiddleName { get; set; }
// Non-nullable - must be set in constructor
public string Email { get; set; }
public User(string email)
{
Email = email;
}
}
// Disable for legacy code
#nullable disable
public class LegacyClass
{
public string Name { get; set; } // Warning suppressed
}
#nullable restore // Return to project default
#nullable enable
public class PersonService
{
// ✅ Non-nullable parameter and return type
public string FormatName(string firstName, string lastName)
{
return $"{firstName} {lastName}";
}
// ✅ Nullable parameter
public string FormatNameWithMiddle(string firstName, string? middleName, string lastName)
{
if (middleName != null)
{
return $"{firstName} {middleName} {lastName}";
}
return $"{firstName} {lastName}";
}
// ✅ Nullable return type
public string? FindUserEmail(int userId)
{
var user = _repository.Find(userId);
return user?.Email; // May return null
}
// ⚠️ Warning - possible null reference
public string GetUpperName(string? name)
{
// CS8602: Possible null reference
return name.ToUpper();
}
// ✅ Fixed with null check
public string GetUpperNameSafe(string? name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
return name.ToUpper();
}
}
Value types can be made nullable using Nullable<T> or the ? syntax.
public class NullableValueTypes
{
// Nullable value types
public int? Age { get; set; }
public DateTime? BirthDate { get; set; }
public decimal? Salary { get; set; }
public bool? IsActive { get; set; }
// Equivalent to:
public Nullable<int> AgeVerbose { get; set; }
public void WorkWithNullables()
{
int? value = null;
// HasValue and Value properties
if (value.HasValue)
{
int actualValue = value.Value;
Console.WriteLine(actualValue);
}
// GetValueOrDefault
int result1 = value.GetValueOrDefault(); // 0
int result2 = value.GetValueOrDefault(42); // 42
// Null coalescing
int result3 = value ?? 100; // 100
}
public int CalculateAge(DateTime? birthDate)
{
// ⚠️ Warning - possible null reference
// return DateTime.Now.Year - birthDate.Value.Year;
// ✅ Correct with null check
if (!birthDate.HasValue)
{
throw new ArgumentException("Birth date is required", nameof(birthDate));
}
return DateTime.Now.Year - birthDate.Value.Year;
}
}
public class NullableOperations
{
public void ArithmeticOperations()
{
int? a = 5;
int? b = 10;
int? c = null;
// Arithmetic with nullables
int? sum = a + b; // 15
int? nullSum = a + c; // null
// Comparison
bool? equal = a == b; // false
bool? nullEqual = a == c; // null (neither true nor false)
// Lifted operators
int? result = (a > 0) ? a * 2 : null;
}
public decimal? CalculateDiscount(decimal? price, decimal? discountPercent)
{
// If either is null, result is null
return price * (1 - discountPercent / 100);
}
public void BooleanLogic()
{
bool? a = true;
bool? b = false;
bool? c = null;
// Three-valued logic
bool? and1 = a & b; // false
bool? and2 = a & c; // null
bool? and3 = b & c; // false (false & anything = false)
bool? or1 = a | b; // true
bool? or2 = a | c; // true (true | anything = true)
bool? or3 = b | c; // null
}
}
Attributes that provide additional null-safety information to the compiler.
using System.Diagnostics.CodeAnalysis;
public class AnnotationExamples
{
// [NotNull] - Parameter won't be null when method returns normally
public void ProcessUser([NotNull] User? user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
// Compiler knows user is not null here
Console.WriteLine(user.Name);
}
// [MaybeNull] - Return value may be null even if type is non-nullable
[return: MaybeNull]
public T GetValueOrDefault<T>(string key)
{
if (_dictionary.TryGetValue(key, out var value))
{
return value;
}
return default; // May be null for reference types
}
// [NotNullWhen] - Parameter is not null when method returns specified bool
public bool TryGetUser(int id, [NotNullWhen(true)] out User? user)
{
user = _repository.Find(id);
return user != null;
}
public void UseUser(int id)
{
if (TryGetUser(id, out var user))
{
// Compiler knows user is not null here
Console.WriteLine(user.Name);
}
}
// [NotNullIfNotNull] - Return value is not null if parameter
// is not null
[return: NotNullIfNotNull(nameof(value))]
public string? ProcessString(string? value)
{
return value?.Trim().ToUpperInvariant();
}
// [DoesNotReturn] - Method never returns normally
[DoesNotReturn]
public void ThrowError(string message)
{
throw new InvalidOperationException(message);
}
public void ValidateUser(User? user)
{
if (user == null)
{
ThrowError("User is required");
}
// Compiler knows this is unreachable if user is null
Console.WriteLine(user.Name);
}
}
public class InitializationExample
{
private string _name;
private string _email;
public InitializationExample()
{
Initialize("Default", "default@example.com");
}
// Tells compiler these members are initialized
[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;
}
}
The null-forgiving operator (!) suppresses nullable warnings when you know better than the compiler.
public class NullForgivingExamples
{
private User? _currentUser;
public void Initialize()
{
_currentUser = LoadUser();
}
public void ProcessCurrentUser()
{
// ⚠️ Warning: Possible null reference
// Console.WriteLine(_currentUser.Name);
// ✅ Use ! when you know it's not null
Console.WriteLine(_currentUser!.Name);
}
// ⚠️ Use sparingly and carefully
public string GetUserName()
{
// Only use ! if you're absolutely sure
return _currentUser!.Name;
}
// ✅ Better: check explicitly
public string GetUserNameSafe()
{
if (_currentUser == null)
{
throw new InvalidOperationException("User not initialized");
}
return _currentUser.Name;
}
// Common pattern with dictionary
public void DictionaryPattern()
{
var dict = new Dictionary<string, User>();
dict["key"] = new User("test@example.com");
// You know key exists
var user = dict["key"];
Console.WriteLine(user.Email); // No warning needed
// But with TryGetValue
if (dict.TryGetValue("key", out var foundUser))
{
// foundUser is User?, but you know it's not null here
Console.WriteLine(foundUser!.Email); // Or better: check in if
}
}
}
public class BadNullForgiving
{
// ❌ BAD - Hiding real problems
public void ProcessData(string? input)
{
var result = input!.ToUpper(); // Will crash if input is null
}
// ✅ GOOD - Proper null handling
public void ProcessDataSafe(string? input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input));
}
var result = input.ToUpper();
}
// ❌ BAD - False confidence
public User GetUser(int id)
{
return _repository.Find(id)!; // May actually be null!
}
// ✅ GOOD - Handle null case
public User GetUserSafe(int id)
{
return _repository.Find(id)
?? throw new KeyNotFoundException($"User {id} not found");
}
}
Safe navigation operators for accessing members that might be null.
public class NullConditionalExamples
{
public void SafeNavigation()
{
User? user = GetUser();
// ✅ Null-conditional member access
string? name = user?.Name; // null if user is null
// ✅ Chaining null-conditional operators
string? city = user?.Address?.City;
// ✅ Null-conditional indexing
char? firstChar = user?.Name?[0];
// ✅ Combining with method calls
int? nameLength = user?.Name?.Length;
// ✅ With null coalescing
string displayName = user?.Name ?? "Guest";
// ✅ Null-conditional with invocation
int? result = user?.CalculateAge();
}
public void ArrayAndCollectionAccess()
{
int[]? numbers = GetNumbers();
// ✅ Null-conditional array access
int? first = numbers?[0];
// ✅ Null-conditional with LINQ
int? max = numbers?.Max();
// ✅ Dictionary access
Dictionary<string, User>? users = GetUsers();
User? user = users?["key"];
}
public void InvocationExamples()
{
Action? callback = GetCallback();
// ✅ Null-conditional invocation
callback?.Invoke();
// Equivalent to:
if (callback != null)
{
callback.Invoke();
}
// ✅ With events
EventHandler? handler = SomeEvent;
handler?.Invoke(this, EventArgs.Empty);
}
}
The ?? and ??= operators provide default values for null expressions.
public class NullCoalescingExamples
{
public void BasicCoalescing()
{
string? name = GetName();
// ✅ Provide default if null
string displayName = name ?? "Unknown";
// ✅ Chain multiple coalescing
string result = GetPrimaryName()
?? GetSecondaryName()
?? GetDefaultName()
?? "Fallback";
// ✅ With value types
int? nullableValue = GetValue();
int value = nullableValue ?? 0;
// ✅ Combine with null-conditional
int length = user?.Name?.Length ?? 0;
}
public User GetUserOrDefault(int id)
{
// ✅ Return default if null
return _repository.Find(id) ?? new User("guest@example.com");
}
public string GetConfigValue(string key, string defaultValue)
{
// ✅ Configuration pattern
return _config[key] ?? defaultValue;
}
}
public class NullCoalescingAssignment
{
private User? _cachedUser;
private List<string>? _items;
public User GetUser(int id)
{
// ✅ Lazy initialization pattern
_cachedUser ??= LoadUser(id);
return _cachedUser;
}
public void EnsureListInitialized()
{
// ✅ Ensure collection is initialized
_items ??= new List<string>();
_items.Add("item");
}
public void UpdateNameIfNull(User user)
{
// ✅ Set only if currently null
user.MiddleName ??= "N/A";
}
// Before C# 8, you would write:
public void OldWay()
{
if (_items == null)
{
_items = new List<string>();
}
// Or:
_items = _items ?? new List<string>();
}
}
C# 9+ pattern matching enhancements for null checking.
public class PatternMatchingExamples
{
public void IsPatterns()
{
object? obj = GetObject();
// ✅ Check for null
if (obj is null)
{
Console.WriteLine("Object is null");
}
// ✅ Check for not null
if (obj is not null)
{
Console.WriteLine("Object is not null");
}
// ✅ Type pattern with null check
if (obj is string s)
{
// s is not null here
Console.WriteLine(s.ToUpper());
}
// ✅ Property pattern
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();
// ✅ Complex pattern matching
var status = order switch
{
null => "No order",
{ Customer: null } => "Order without customer",
{ Customer.Address: null } => "Customer without address",
{ Customer.Address.City: var city } => $"Shipping to {city}",
};
}
}
Gradually migrate existing code to nullable reference types.
// Step 1: Enable nullable in .csproj with warnings as errors
<PropertyGroup>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
// Step 2: Migrate file by file
#nullable enable
public class MigratedClass
{
// Fix all warnings in this file
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
// Step 3: Use #nullable disable for legacy code
#nullable disable
public class LegacyClass
{
// No nullable warnings here
public string Name { get; set; }
}
#nullable restore
public class MigrationPatterns
{
// Before: Everything nullable by default
#nullable disable
public string GetUserName(User user)
{
return user.Name;
}
#nullable restore
// After: Explicit nullability
#nullable enable
public string GetUserNameNullable(User? user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
return user.Name ?? throw new InvalidOperationException("Name is required");
}
// Pattern: Make optional parameters explicit
// Before
#nullable disable
public void ProcessData(string data, string format)
{
format = format ?? "json";
}
#nullable restore
// After
#nullable enable
public void ProcessDataNullable(string data, string? format = null)
{
format ??= "json";
}
// Pattern: Use nullable return types
// Before
#nullable disable
public User FindUser(int id)
{
return _repository.Find(id); // May return null
}
#nullable restore
// After
#nullable enable
public User? FindUserNullable(int id)
{
return _repository.Find(id);
}
}
Understanding and configuring nullable warning levels.
// In .csproj
<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>
// Common warnings:
// CS8600: Converting null literal or possible null value to non-nullable type
// CS8601: Possible null reference assignment
// CS8602: Dereference of a possibly null reference
// CS8603: Possible null reference return
// CS8604: Possible null reference argument
#nullable enable
public class WarningExamples
{
// CS8618: Non-nullable property must contain non-null value when exiting constructor
public string Name { get; set; } = string.Empty;
// CS8603: Possible null reference return
public string GetName(User? user)
{
return user?.Name ?? string.Empty; // Fix
}
// CS8602: Dereference of possibly null reference
public int GetLength(string? value)
{
return value?.Length ?? 0; // Fix
}
// Suppress specific warning
#pragma warning disable CS8602
public void LegacyCode(string? value)
{
Console.WriteLine(value.Length); // Warning suppressed
}
#pragma warning restore CS8602
}
Handling nullability in generic type parameters.
#nullable enable
public class GenericNullability
{
// T? is nullable for both reference and value types
public T? FindOrDefault<T>(int id)
{
var result = _repository.Find<T>(id);
return result; // May be null
}
// where T : class - T is a reference type
public T Create<T>(string name) where T : class, new()
{
var instance = new T();
return instance; // Never null
}
// where T : class? - T is a nullable reference type
public void Process<T>(T? value) where T : class
{
if (value == null)
{
return;
}
// value is not null here
Console.WriteLine(value.ToString());
}
// where T : struct - T is a non-nullable value type
public T GetValue<T>() where T : struct
{
return default; // Returns default value, never null
}
// where T : notnull - T cannot be nullable
public void RequireNonNull<T>(T value) where T : notnull
{
// value is guaranteed not to be null
Console.WriteLine(value.ToString());
}
}
public class Repository<T> where T : class
{
private readonly Dictionary<int, T> _cache = new();
// Return nullable when not found
public T? Find(int id)
{
_cache.TryGetValue(id, out var result);
return result;
}
// Throw when not found
public T Get(int id)
{
return _cache[id]; // Throws if not found
}
// Try pattern
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();
// First or null
public T? FirstOrDefault()
{
return _items.Count > 0 ? _items[0] : default;
}
// Find with predicate
public T? Find(Predicate<T> predicate)
{
foreach (var item in _items)
{
if (predicate(item))
{
return item;
}
}
return default;
}
}
<Nullable>enable</Nullable> in .csprojUse this skill when:
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.