dotnet-types
Skill1 starsChanged 27 days agoMITView on GitHub
C# and .NET type system advisor. Always use this skill when working with C# nullable reference types, records, pattern matching, switch expressions, generics, value types, or sealed classes. Use when the user asks "how do I handle nulls in C#", "record vs class", "how do I use pattern matching", "CS8600 nullable warning", "how do I enable nullable reference types", "sealed class hierarchy", "switch expression in C#", "struct vs class", or "how do I constrain a generic in C#". Read this skill before answering any C# type system question.
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 1 is distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 33
Don't suppress the warnings with !
- Very specific: line 46
Use record for immutable data transfer objects, domain value types, and anything where value equality matters.
- Distinctive: line 57
Record vs class: use a record when the identity of the object is defined by its data (two users with the same fields are equal), not its …
- Very specific: line 150
Avoid mutable structs; they produce surprising copy semantics.
- Very specific: line 154
Use var when the type is obvious from the right-hand side or when the exact type is not important:
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
C# Types
C#'s type system has grown significantly: nullable reference types, records, and pattern matching change how you model data and handle null. Use them. Code written without these features tends to be both more verbose and less safe.
Enable nullable reference types
Enable <Nullable>enable</Nullable> in your project file. This makes all reference types non-nullable by default: you must opt into nullability with ?.
<!-- .csproj -->
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>// Non-nullable — compiler guarantees this is never null
string name = GetName();
// Nullable — must check before use
string? email = GetEmail();
if (email is not null)
{
Send(email); // safe
}Don't suppress the warnings with ! (null-forgiving operator) unless you genuinely know more than the compiler; it's the C# equivalent of as any.
// Bad: suppresses the warning without checking
string email = GetEmail()!;
// Good: check or propagate nullability
string? email = GetEmail();
string resolvedEmail = email ?? throw new InvalidOperationException("Email required");Records for immutable data
Use record for immutable data transfer objects, domain value types, and anything where value equality matters. Records generate Equals, GetHashCode, ToString, and deconstruction automatically.
// Immutable record — value equality by default
public record UserId(string Value);
public record User(UserId Id, string Name, string? Email);
// With-expression for non-destructive mutation
var updated = user with { Email = "new@example.com" };Record vs class: use a record when the identity of the object is defined by its data (two users with the same fields are equal), not its reference. Use a class when identity is reference-based or the type has mutable state.
Record struct (C# 10+): value type semantics with record convenience (good for small value objects in performance-sensitive code):
public readonly record struct Money(decimal Amount, string Currency);Pattern matching
Pattern matching reduces defensive null-checking boilerplate and replaces long if/else chains with readable switch expressions.
// Switch expression with property patterns
string Describe(Shape shape) => shape switch
{
Circle { Radius: > 10 } => "large circle",
Circle c => $"circle with radius {c.Radius}",
Rectangle { Width: var w, Height: var h } when w == h => "square",
Rectangle r => $"rectangle {r.Width}×{r.Height}",
_ => "unknown shape"
};Is-pattern for null checking:
if (user is not null && user.Email is string { Length: > 0 } email)
{
Send(email);
}List patterns (C# 11):
int[] arr = { 1, 2, 3 };
bool result = arr is [1, .., 3]; // true — starts with 1, ends with 3Exhaustiveness: the compiler warns when a switch expression doesn't cover all cases for discriminated union types (closed hierarchies). Use abstract base + sealed derived types to get this guarantee:
public abstract record Result<T>;
public sealed record Ok<T>(T Value) : Result<T>;
public sealed record Err<T>(string Message) : Result<T>;
string Display<T>(Result<T> result) => result switch
{
Ok<T> ok => ok.Value?.ToString() ?? "",
Err<T> err => $"Error: {err.Message}",
// compiler warns if a case is missing
};Generics and constraints
Constrain type parameters when you need to access members:
// Bad: T is unconstrained — can't call any members
public T First<T>(IEnumerable<T> items) { ... }
// Good: constrain to what you need
public T Max<T>(IEnumerable<T> items) where T : IComparable<T>
{
return items.Aggregate((a, b) => a.CompareTo(b) > 0 ? a : b);
}Common constraints:
| Constraint | Meaning |
where T : class | Reference type |
where T : struct | Value type |
where T : new() | Has parameterless constructor |
where T : IInterface | Implements interface |
where T : BaseClass | Inherits from class |
where T : notnull | Non-nullable (NRT-aware) |
Value types vs reference types
struct is a value type: copied on assignment, no heap allocation, no inheritance. Use for small, immutable data with value semantics (coordinates, money, identifiers).
// Good as a struct: small, immutable, value equality makes sense
public readonly struct Temperature(double Celsius)
{
public double Fahrenheit => Celsius * 9 / 5 + 32;
}Avoid mutable structs; they produce surprising copy semantics. Mark structs readonly to enforce immutability.
var usage
Use var when the type is obvious from the right-hand side or when the exact type is not important:
var users = new List<User>(); // obvious
var user = repository.FindById(id); // fine — type isn't the point
// Don't use var when it hides intent
var x = Calculate(); // what type is x? use explicit type hereWhat to watch for in code review
- Reference types without
?in a nullable-enabled project (missing nullability annotation) !(null-forgiving) without a comment explaining why the value can't be null- Classes with value semantics that should be records
objectparameters or return types that could be generic- Mutable structs: they produce confusing copy behavior
- Long if/else chains over type checks that pattern matching would simplify
In this repository25 other files it gives its agents.
- api-designSkill
- arch-reviewSkill
- db-reviewSkill
- debugSkill
- dotnet-asyncSkill
- dotnet-opsSkill
- dotnet-testingSkill
- go-concurrencySkill
- go-opsSkill
- go-testingSkill
- go-typesSkill
- incidentSkill
- node-asyncSkill
- node-opsSkill
- perfSkill
- pr-prepSkill
- py-asyncSkill
- py-opsSkill
- py-testingSkill
- py-typesSkill
- refactorSkill
- securitySkill
- test-genSkill
- ts-testingSkill
- ts-typesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

