From csharp-foundation
Modern C# idioms for any .NET project (C# 10+, .NET 6+): nullable reference types, records, readonly structs, primary constructors, pattern matching, async/await with CancellationToken, IDisposable/IAsyncDisposable, file-scoped namespaces, var usage, naming conventions (PascalCase members, _camelCase fields, I-prefixed interfaces), and class design rules. Apply whenever the project is a .NET 6+ project. Stack-agnostic — referenced by every .NET plugin in the marketplace. Use this skill to: - Write self-documenting immutable value types with records and readonly structs. - Handle nullable reference types explicitly to eliminate NullReferenceException at compile time. - Implement async/await correctly with CancellationToken propagation and ConfigureAwait(false) in libraries. - Dispose unmanaged resources correctly via IDisposable / IAsyncDisposable and using statements. - Apply C# pattern matching (switch expressions, property patterns, list patterns) for cleaner branching logic. Do NOT use this skill for: - Framework-specific idioms (ASP.NET Core controllers, minimal APIs, EF Core — those live in aspnet-core-plugin skills). - Build tooling (dotnet CLI, NuGet, csproj) — see csharp-foundation:dotnet-tooling. - Testing patterns — see csharp-foundation:dotnet-testing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/csharp-foundation:csharp-conventionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill encodes idioms that reduce bugs and improve readability in any C# codebase. Apply alongside the active framework plugin's conventions skill (e.g., `aspnet-core-plugin:aspnet-conventions`).
This skill encodes idioms that reduce bugs and improve readability in any C# codebase. Apply alongside the active framework plugin's conventions skill (e.g., aspnet-core-plugin:aspnet-conventions).
Project is C# 10+ / .NET 6+ when:
.csproj has <TargetFramework>net6.0</TargetFramework> or higher (net7.0, net8.0, net9.0, net10.0).global.json pins sdk.version to 6.0.x or higher.Read the .csproj <TargetFramework> before making any version-specific decisions.
Enable in every project. New projects get it by default from dotnet new; older projects need a one-time migration.
<!-- .csproj: enable project-wide -->
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// Non-nullable: compiler guarantees non-null, no null check needed
public string Name { get; }
// Nullable: caller must check before dereferencing
public string? MiddleName { get; }
// Null-forgiving operator — use only when you have proven non-null
var definitelySet = _cache[key]!;
// Null-conditional + null-coalescing
string display = user?.FullName ?? "Guest";
Never silence nullable warnings with ! without a comment explaining why the value is guaranteed non-null. Prefer redesigning the API to avoid the need.
Use record (class) for immutable reference-type value objects; readonly record struct for small value types.
// Immutable DTO — all positional parameters become init-only properties
public record Money(decimal Amount, string Currency)
{
// Compact validation in the record body
public Money
{
if (Amount < 0) throw new ArgumentOutOfRangeException(nameof(Amount), "Amount must be non-negative.");
ArgumentException.ThrowIfNullOrWhiteSpace(Currency);
}
public Money Add(Money other)
{
if (Currency != other.Currency) throw new InvalidOperationException("Currency mismatch.");
return this with { Amount = Amount + other.Amount };
}
}
// Small stack-allocated value type
public readonly record struct Point(double X, double Y);
with expressions create modified copies — preserve immutability instead of mutating.
// Switch expression (C# 8+)
string Describe(object obj) => obj switch
{
int n when n > 0 => $"positive int: {n}",
int n => $"non-positive int: {n}",
string s => $"string of length {s.Length}",
null => "null",
_ => obj.GetType().Name,
};
// Property patterns (C# 8+)
string Category(Order order) => order switch
{
{ Total: > 1000, IsPriority: true } => "VIP",
{ Total: > 500 } => "Large",
_ => "Standard",
};
// List patterns (C# 11+)
bool StartsWithOne(int[] nums) => nums is [1, ..];
Never cast ((T)obj) without a prior is check. Use type patterns (obj is T t) to combine the check and the cast.
// Propagate CancellationToken everywhere
public async Task<User> GetUserAsync(int id, CancellationToken ct = default)
{
var user = await _repository.FindAsync(id, ct);
return user ?? throw new KeyNotFoundException($"User {id} not found.");
}
// ConfigureAwait(false) in library code (not in application code / controllers)
var data = await _client.GetStringAsync(url, ct).ConfigureAwait(false);
// Avoid async void — use async Task instead
// BAD: public async void OnSomeEvent(...)
// GOOD: public async Task HandleAsync(...)
// Fire-and-forget requires explicit error handling
_ = Task.Run(async () =>
{
try { await DoBackgroundWorkAsync(); }
catch (Exception ex) { _logger.LogError(ex, "Background work failed"); }
});
Never use .Result or .Wait() on a Task — it risks deadlocks on synchronisation-context–bound runtimes (ASP.NET Core, WinForms).
// Implement IDisposable when owning unmanaged resources or disposable children
public sealed class DatabaseConnection : IDisposable
{
private readonly SqlConnection _connection;
private bool _disposed;
public DatabaseConnection(string connectionString)
=> _connection = new SqlConnection(connectionString);
public void Dispose()
{
if (_disposed) return;
_connection.Dispose();
_disposed = true;
}
}
// Prefer IAsyncDisposable for async cleanup (e.g., flushing async streams)
public sealed class FileWriter : IAsyncDisposable
{
private readonly StreamWriter _writer;
public async ValueTask DisposeAsync() => await _writer.DisposeAsync();
}
// Always use using declarations / using statements
await using var writer = new FileWriter(path);
using var conn = new DatabaseConnection(connStr);
Seal classes that implement IDisposable unless they are designed for inheritance. Add a protected virtual void Dispose(bool disposing) pattern only when the class is unsealed.
| Symbol | Convention | Example |
|---|---|---|
| Types, methods, properties, events | PascalCase | OrderService, GetUserAsync |
| Private / protected fields | _camelCase | _repository, _logger |
| Local variables, parameters | camelCase | userId, cancellationToken |
| Constants, static readonly fields | PascalCase | MaxRetries, DefaultTimeout |
| Interfaces | I prefix + PascalCase | IOrderRepository |
| Generic type parameters | T or descriptive T-prefix | T, TKey, TValue |
| Async methods | Async suffix | GetOrderAsync, SaveAsync |
public interface IUserRepository
{
Task<User?> FindAsync(int id, CancellationToken ct = default);
Task<IReadOnlyList<User>> ListActiveAsync(CancellationToken ct = default);
}
public sealed class UserRepository : IUserRepository
{
private readonly AppDbContext _db;
public UserRepository(AppDbContext db) => _db = db;
public async Task<User?> FindAsync(int id, CancellationToken ct = default)
=> await _db.Users.FindAsync(new object[] { id }, ct);
public async Task<IReadOnlyList<User>> ListActiveAsync(CancellationToken ct = default)
=> await _db.Users.Where(u => u.IsActive).ToListAsync(ct);
}
Prefer file-scoped namespaces to reduce indentation:
// Preferred (C# 10+)
namespace MyApp.Users;
public record User(int Id, string Email);
// Avoid for new code (block-scoped adds one level of indentation)
namespace MyApp.Users
{
public record User(int Id, string Email);
}
// Good — type is clear from the right-hand side
var users = new List<User>();
var order = await _orderRepo.FindAsync(id, ct);
var (first, rest) = GetParts();
// Avoid — type is not obvious
var result = Process(data); // What type is result?
var is for local variables only. Never use for fields, parameters, or return types.
sealed class).internal by default, public only when the type/member is part of the contract.// Prefer static factory when construction can fail
public sealed class Email
{
private readonly string _value;
private Email(string value) => _value = value;
public static Email Parse(string raw)
{
if (!raw.Contains('@')) throw new FormatException($"'{raw}' is not a valid email.");
return new Email(raw.Trim().ToLowerInvariant());
}
public override string ToString() => _value;
}
npx claudepluginhub aratkruglik/claude-sdlc --plugin csharp-foundationWrite modern, high-performance C# code using records, pattern matching, value objects, async/await, Span<T>/Memory<T>, and best-practice API design patterns. Emphasizes functional-style programming with C# 12+ features. Use when writing new C# code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.
Guides use of modern C# language features for .NET 10 and C# 14, including primary constructors, collection expressions, records, pattern matching, spans, and the field keyword.
Provides guidance on C# language patterns (.NET 8-11), coding standards, async/await, DI, LINQ, serialization, and .NET runtime features. Loads automatically for .NET code paths.