skills/core/modern-csharp/SKILL.md
Use when adopting C# 12/13/14 features — primary constructors, collection expressions, or field keyword.
npx skillsauth add faysilalshareef/dotnet-ai-kit modern-csharpInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
[..] over explicit collection initializersFeature C# Version Min .NET
--------------------------------------------------
File-scoped namespaces C# 10 .NET 6
Global usings C# 10 .NET 6
Required members C# 11 .NET 7
Raw string literals C# 11 .NET 7
List patterns [a, .., z] C# 11 .NET 7
Primary constructors C# 12 .NET 8
Collection expressions [..] C# 12 .NET 8
Inline arrays C# 12 .NET 8
using aliases for generics C# 12 .NET 8
params collections C# 13 .NET 9
Lock object C# 13 .NET 9
field keyword C# 14 .NET 10
Extension types C# 14 .NET 10
// Services -- capture DI parameters directly
public sealed class OrderService(
IOrderRepository repository,
ILogger<OrderService> logger)
{
public async Task<Order?> GetAsync(Guid id, CancellationToken ct)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await repository.FindAsync(id, ct);
}
}
// Records already had primary constructors -- still the best fit for DTOs
public sealed record OrderResponse(Guid Id, string CustomerName, decimal Total);
// Structs
public readonly record struct Money(decimal Amount, string Currency);
// Array, List, Span -- all use [..]
int[] numbers = [1, 2, 3];
List<string> names = ["Alice", "Bob"];
ReadOnlySpan<byte> bytes = [0x00, 0xFF];
// Spread operator
List<string> merged = [..existing, ..additional, "extra"];
// Empty collection (replaces Array.Empty<T>())
int[] empty = [];
// Command message
public sealed record Create{Entity}Command(
string Name,
string Description) : IRequest<Result<Guid>>;
// Value object
public sealed record Address(
string Street,
string City,
string PostalCode,
string Country);
// Record struct for small value types (no heap allocation)
public readonly record struct Coordinate(double Latitude, double Longitude);
// Switch expression with property pattern
string description = order switch
{
{ Status: OrderStatus.Pending, Total: > 1000 } => "Requires approval",
{ Status: OrderStatus.Pending } => "Awaiting processing",
{ Status: OrderStatus.Shipped } => "In transit",
{ Status: OrderStatus.Delivered } => "Complete",
_ => "Unknown"
};
// List patterns (C# 11)
var result = args switch
{
[] => "No arguments",
[var single] => $"One argument: {single}",
[var first, ..] => $"Multiple arguments, first: {first}"
};
// Relational and logical patterns
bool IsValidAge(int age) => age is >= 0 and <= 150;
// Multi-line SQL or JSON without escaping
var sql = """
SELECT o.Id, o.CustomerName, o.Total
FROM Orders o
WHERE o.Status = @Status
ORDER BY o.CreatedAt DESC
""";
// Interpolated raw strings
var json = $$"""
{
"name": "{{name}}",
"total": {{total}}
}
""";
public sealed class {Company}.{Domain}.Models.ProductOptions
{
public required string Name { get; init; }
public required decimal Price { get; init; }
public string? Description { get; init; }
}
// Caller must set required properties
var options = new ProductOptions
{
Name = "Widget", // required
Price = 9.99m // required
// Description is optional
};
// Auto-property with custom validation -- no backing field declaration needed
public string Name
{
get => field;
set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
public int Quantity
{
get => field;
set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
}
using OrderList = System.Collections.Generic.List<{Company}.{Domain}.Models.Order>;
using OrderDict = System.Collections.Generic.Dictionary<System.Guid, {Company}.{Domain}.Models.Order>;
// Then use the alias
OrderList orders = [new Order(), new Order()];
// PREFER: file-scoped (saves one level of indentation)
namespace {Company}.{Domain}.Features.Orders;
public sealed class OrderService { }
// AVOID: block-scoped
namespace {Company}.{Domain}.Features.Orders
{
public sealed class OrderService { }
}
| Anti-Pattern | Problem | Correct Approach |
|---|---|---|
| Using new List<int> { 1, 2, 3 } on C# 12+ | Verbose, ignores collection expressions | [1, 2, 3] |
| Mutable class for DTO | Unintended mutation | sealed record |
| Long if/else chains for type checks | Hard to read, error-prone | Switch expression with patterns |
| string.Format() or $"" for multi-line SQL | Escaping issues, poor readability | Raw string literals """ |
| field keyword on .NET 8/9 projects | Compilation error | Check <LangVersion> first |
| Primary constructors storing to private readonly | C-Q5: the compiler generates backing storage only when an instance member captures the primary-constructor parameter. Adding an explicit private readonly field allocates a second storage slot and double-writes both — the constructor param is then captured into the implicit field AND copied to your explicit field. Wasted memory + drift risk. | Use the captured parameter directly (e.g., IService _svc; is unnecessary if Foo(IService svc) already captures svc via a method body referencing it). |
| Block-scoped namespaces | Wastes indentation | File-scoped namespace X; |
| == null / != null comparisons | Operator overloading can break intent | is null / is not null |
<LangVersion> in .csproj or Directory.Build.props (preview, latest, 12, 13, 14)<TargetFramework> for net8.0, net9.0, net10.0class Foo( or struct Foo(record keyword usage in model/DTO files= [ in existing coderequired modifier on propertiesDirectory.Build.props or .csproj for <LangVersion>sealed record[..] syntax on C# 12+ projectsif/switch blocks with switch expressionsdotnet formatfield keyword or extension types on .NET 10+ projects| Scenario | Recommendation |
|----------|---------------|
| .NET 8 project | C# 12: primary constructors, collection expressions, records |
| .NET 9 project | C# 13: all above + params collections, lock object |
| .NET 10+ project | C# 14: all above + field keyword, extension types |
| Shared library targeting multiple TFMs | Use lowest common C# version across targets |
| New greenfield project | Use <LangVersion>latest</LangVersion> and latest TFM |
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.