Nullable reference discipline
Skill Sarmkadan/dotnet-senior-skills/skills/nullable-reference-discipline
Enforce nullable reference type discipline in C# - annotation honesty, null-forgiveness audit, boundary validation, and EF Core interaction. Use when writing or reviewing C# code in nullable-enabled projects or migrating projects to nullable.From its SKILL.md
npx -y skills add Sarmkadan/dotnet-senior-skills --skill nullable-reference-disciplineAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
4.2 KB, 907 tokens by cl100k_base, as published. Nobody here has run it
Nullable Reference Discipline
Baseline
<Nullable>enable</Nullable> project-wide plus <WarningsAsErrors>nullable</WarningsAsErrors>. Warnings-as-suggestions rot in a week. For gradual migration, enable per-file with #nullable enable starting from the leaves (models, utilities) upward, and never add new files without it.
The annotation is a contract, not a wish
string Namemeans never null - and the type must make it true: initialized at construction (required, constructor parameter) or the annotation is a lie the compiler will now defend.string? Namemeans callers MUST handle null. Do not add?to silence a warning when the value is logically always present - fix the initialization instead.
// non-compiling: illustrative
// WRONG: lying to the compiler to make the warning go away
public string Email { get; set; } = null!;
// RIGHT: the contract is enforced at construction
public required string Email { get; set; }
= null! is acceptable in exactly two places: EF Core navigation properties (materializer sets them) and DI-populated framework hooks. Each occurrence outside those needs a comment saying why.
Null-forgiving operator audit
Every ! is a claim: "I know more than the flow analysis." In review, verify the claim:
dict[key]!afterContainsKey- fine, butTryGetValueremoves the need.FirstOrDefault()!- almost always wrong; if absence is impossible useFirst()(fails loudly at the right place), if possible, handle it.!on deserialized input (JsonSerializer.Deserialize<T>(json)!) - wrong: deserialization of"null"returns null; validate and throw a domain-meaningful error.
Grep the diff for !. and )! - more than a couple per file means the types are misdesigned, usually a half-initialized object that needs a constructor or a factory.
Boundaries: annotations do not validate
Nullable analysis is compile-time only. Data crossing a trust boundary (HTTP body, message queue, database, config) arrives unchecked:
- Request DTOs: non-null annotation +
[Required]/validator. ASP.NET Core model validation treats non-nullable reference properties as required by default - know this, because it produces 400s people then "fix" by adding?everywhere. - Public library APIs: keep
ArgumentNullException.ThrowIfNull(arg)on entry points; your consumers may compile with nullable off.
EF Core interaction
- Non-nullable property => NOT NULL column;
string?=> NULL. Check migrations after annotation changes - adding?is a schema change. - Required navigation:
public Customer Customer { get; set; } = null!;- and remember it is still null when the entity was loaded without Include; the annotation does not load data. ANullReferenceExceptionon a "non-nullable" navigation means a missing Include or projection, not a data bug. - Optional relationship: both FK and navigation nullable (
int? CustomerId,Customer? Customer), and they must agree - a non-nullable navigation over a nullable FK misleads every reader.
Patterns to prefer
is null/is not nullover== null(bypasses operator overloads, reads as intent).- Early return over nested null checks; after
if (x is null) return;flow analysis promotesxfor the rest of the method. [NotNullWhen(true)],[MemberNotNull(nameof(_field))]on Try-patterns and init helpers so callers do not need!:
public bool TryGetUser(int id, [NotNullWhen(true)] out User? user)
??with a throw for impossible states:var user = cache.Get(id) ?? throw new InvalidOperationException($"User {id} evicted mid-request");- fails at the assumption, not three frames later.
What not to do
- Do not blanket-
?a legacy codebase to make it compile; that erases the information the feature exists to capture. - Do not null-check parameters the annotation already guarantees inside private/internal code - checks belong at trust boundaries, not on every frame.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.