Annotations: ?, !, MaybeNull & NotNull
Use ? for nullable types, ! for null-forgiving, and attributes like MaybeNull and NotNullWhen for precise flow analysis.
Annotations: ?, !, MaybeNull & NotNull is a free C# Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Beyond ? and !: Nullability Attributes
The basic ? annotation and null check handle most cases, but some patterns require more expressive annotations. The System.Diagnostics.CodeAnalysis namespace provides attributes that give the compiler deeper insight into null flow.
MaybeNull and NotNull
[MaybeNull] tells the compiler a non-nullable return type may actually be null in practice (e.g., a generic method). [NotNull] promises a nullable parameter will not be null after the call.
using System.Diagnostics.CodeAnalysis;
// [MaybeNull]: return might be null even though T is non-nullable
[return: MaybeNull]
public T Find<T>(int id)
{
// Returns default(T) which is null for reference types
return _cache.TryGetValue(id, out var val) ? val : default!;
}
// [NotNull]: after this call, output is guaranteed non-null
public static void EnsureNotNull<T>(
[NotNull] ref T? value,
T defaultValue) where T : class
{
value ??= defaultValue;
}NotNullWhen: Conditional Non-Null
[NotNullWhen(true)] tells the compiler that an output parameter is non-null when the method returns true. This is how the standard TryParse pattern is annotated.
using System.Diagnostics.CodeAnalysis;
public static bool TryParseEmail(
string? input,
[NotNullWhen(true)] out string? email)
{
if (input?.Contains('@') == true)
{
email = input.Trim().ToLower();
return true;
}
email = null;
return false;
}
// Usage — no warning after true check:
if (TryParseEmail(raw, out var email))
Console.WriteLine(email.Length); // safe — email is non-null hereMaybeNullWhen: Conditional Null
[MaybeNullWhen(false)] is the inverse — the output may be null when the method returns false. Used in dictionary TryGetValue patterns.
// This is how Dictionary<K,V>.TryGetValue is annotated:
public bool TryGetValue(
TKey key,
[MaybeNullWhen(false)] out TValue value)
{ ... }
// Usage:
if (!dict.TryGetValue("key", out var value))
return; // early return — value is null in this branch
Console.WriteLine(value.Length); // safe after the guardAllowNull and DisallowNull
[AllowNull] on a non-nullable property allows callers to pass null to it (e.g., a setter that converts null to empty string). [DisallowNull] prohibits null on a nullable type.
public class Config
{
private string _name = "";
// Allow setting null (setter converts null -> empty)
[AllowNull]
public string Name
{
get => _name;
set => _name = value ?? "";
}
// Getter always returns non-null: fine
// Setter accepts null: [AllowNull] tells compiler that's OK
}NotNullIfNotNull: Propagating Nullability
[NotNullIfNotNull(paramName)] says: if parameter X is non-null, the return value is also non-null. Useful for transformation functions.
[return: NotNullIfNotNull(nameof(value))]
public static string? Normalize(string? value)
{
return value?.Trim().ToLower();
}
// Usage:
string name = " Alice ";
string norm1 = Normalize(name)!; // guaranteed non-null
string? raw = GetRaw();
string? norm2 = Normalize(raw); // still nullable (raw might be null)DoesNotReturn
[DoesNotReturn] marks a method that always throws. The compiler knows code after the call is unreachable, suppressing spurious null warnings.
using System.Diagnostics.CodeAnalysis;
[DoesNotReturn]
public static void ThrowNotFound(int id)
=> throw new KeyNotFoundException($"Entity {id} not found");
// Usage — no null warning after the call:
var order = _db.Orders.Find(id);
if (order is null) ThrowNotFound(id);
Console.WriteLine(order.Id); // no CS8602 — compiler knows ThrowNotFound threwMemberNotNull: Post-Condition on Fields
[MemberNotNull] tells the compiler that a method guarantees certain fields are non-null after it returns. Useful for lazy initialization helpers.
public class DataLoader
{
private string? _data;
[MemberNotNull(nameof(_data))]
private void EnsureLoaded()
{
if (_data is null)
_data = LoadFromFile();
}
public string GetData()
{
EnsureLoaded();
return _data; // no CS8603 — compiler knows _data is set
}
}Combining Annotations
Attributes can be combined for precise API contracts. Here's a fluent guard helper that combines several attributes.
public static class Guard
{
[return: NotNull]
public static T NotNull<T>(
[NotNull][AllowNull] T? value,
[CallerArgumentExpression(nameof(value))] string? name = null)
where T : class
{
ArgumentNullException.ThrowIfNull(value, name);
return value;
}
}
// Usage:
var product = Guard.NotNull(await _repo.FindAsync(id));
Console.WriteLine(product.Name); // no warningNull Operators: ?. ?? ??=
The null-conditional (?.), null-coalescing (??), and null-coalescing assignment (??=) operators write concise null-safe code without verbose if-checks.
string? name = GetName();
// ?. safe navigation
int? len = name?.Length;
string? upper = name?.ToUpper().Trim();
// ?? default value
string display = name ?? "Anonymous";
// ??= lazy init
name ??= "Default";
// Chaining
string result = user?.Profile?.DisplayName ?? user?.Name ?? "Guest";Quick Check
What does [NotNullWhen(true)] on an out parameter tell the compiler?
Recap: Nullability Annotations
Key takeaways:
[NotNullWhen(true)]: TryParse pattern — non-null on true return[MaybeNull]: non-nullable type may still return null (generic defaults)[DoesNotReturn]: method always throws — code after is unreachable[MemberNotNull]: method guarantees fields are set after it returns?.,??,??=: concise null-safe navigation and defaults
Frequently asked questions
Is the “Annotations: ?, !, MaybeNull & NotNull” lesson free?
Yes — the full text of “Annotations: ?, !, MaybeNull & NotNull” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Annotations: ?, !, MaybeNull & NotNull”?
Use ? for nullable types, ! for null-forgiving, and attributes like MaybeNull and NotNullWhen for precise flow analysis. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Annotations: ?, !, MaybeNull & NotNull” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Enabling & Understanding NRT
- Annotations: ?, !, MaybeNull & NotNull
- Null-Conditional & Null-Coalescing Operators
- Migrating a Codebase to NRT