dotnet-modern-csharp
Write modern, version-aware C# for .NET repositories. Use when choosing language features across C# versions, especially C# 13 and C# 14, while staying compatible with the repo's target framework and `LangVersion`.
Install
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-modern-csharp
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Modern C# for .NET
Trigger On
- the repo wants more modern idiomatic C# code
- a change depends on language-version compatibility
- the team is upgrading or reviewing C# feature usage across versions
- you need to know whether a C# 13 or C# 14 feature is safe to use
Value
- produce a concrete project delta: code, docs, config, tests, CI, or review artifact
- reduce ambiguity through explicit planning, verification, and final validation skills
- leave reusable project context so future tasks are faster and safer
Do Not Use For
- non-C# .NET languages such as F# or VB
- analyzer-only or formatter-only setup with no language feature choice
Inputs
- target
TFMorTFMs - explicit
LangVersion, if any - current SDK version
- team style rules in
.editorconfigandAGENTS.md
Quick Start
- Read the nearest
AGENTS.mdand confirm scope and constraints. - Run this skill's
Workflowthrough theRalph Loopuntil outcomes are acceptable. - Return the
Required Result Formatwith concrete artifacts and verification evidence.
Workflow
- Detect the real language ceiling from the repo's target framework and explicit
LangVersion. - Prefer stable features that the current repo actually supports.
- Use modern syntax when it reduces ceremony, improves correctness, or makes invariants clearer.
- Do not mass-rewrite a codebase into newer syntax unless the repo wants that churn.
- Treat preview features as opt-in only. Never assume preview because the current machine has a newer SDK.
- Pay special attention to C# 13 and C# 14:
- C# 13 is the stable language for
.NET 9 - C# 14 is the stable language for
.NET 10
- C# 13 is the stable language for
- When feature selection changes architecture, style rules, or generated-code patterns, coordinate with:
dotnetdotnet-analyzer-configdotnet-architecture
- After feature-driven refactors, run the repo's .NET quality pass through
dotnet.
Bootstrap When Missing
If the requested C# feature depends on SDK or language support the repo does not have yet:
- Detect current state:
dotnet --list-sdksrg -n "TargetFramework|LangVersion|TargetFrameworks" -g '*.csproj' -g 'Directory.Build.*' .
- Confirm whether the repo wants to stay on the current stable language level or intentionally upgrade.
- If the feature requires a newer supported SDK or target framework, upgrade the repo toolchain deliberately instead of relying on the local machine by accident.
- If the repo needs explicit
LangVersion, record it in project or shared MSBuild config. - Run
dotnet build SOLUTION_OR_PROJECTafter the feature or toolchain change and returnstatus: configuredorstatus: improved. - If the repo intentionally stays below the required language level, return
status: not_applicable.
Deliver
- modern C# code that fits the repo's real language version
- fewer obsolete patterns when a newer stable feature is clearer
- no accidental preview or unsupported-language drift
Validate
- the chosen syntax is supported by the repo's
TFMandLangVersion - the feature improves clarity, correctness, or maintainability
- preview-only features are used only when the repo explicitly opted in
- style and analyzer rules still agree with the new syntax
Ralph Loop
Use the Ralph Loop for every task, including docs, architecture, testing, and tooling work.
- Plan first (mandatory):
- analyze current state
- define target outcome, constraints, and risks
- write a detailed execution plan
- list final validation skills to run at the end, with order and reason
- Execute one planned step and produce a concrete delta.
- Review the result and capture findings with actionable next fixes.
- Apply fixes in small batches and rerun the relevant checks or review steps.
- Update the plan after each iteration.
- Repeat until outcomes are acceptable or only explicit exceptions remain.
- If a dependency is missing, bootstrap it or return
status: not_applicablewith explicit reason and fallback path.
Required Result Format
status:complete|clean|improved|configured|not_applicable|blockedplan: concise plan and current iteration stepactions_taken: concrete changes madevalidation_skills: final skills run, or skipped with reasonsverification: commands, checks, or review evidence summaryremaining: top unresolved items ornone
For setup-only requests with no execution, return status: configured and exact next commands.
Load References
references/patterns.mdreferences/migration.mdreferences/csharp-modern-features.md
Example Requests
- "Make this C# code more modern."
- "Which features can we use on .NET 9?"
- "Review this repo for C# 13 or C# 14 opportunities."
Files (dotnet-skills)
-
references
-
csharp-modern-features.md 5.6 KB
# Modern C# Features For .NET Repositories Use this reference when modernizing C# code without breaking the repo's target framework, SDK, or language-version expectations. ## First Rule: Detect The Real Language Ceiling - the default C# version follows the target framework in modern SDK-style projects - `.NET 9` maps to `C# 13` - `.NET 10` maps to `C# 14` - newer language versions than the target framework supports are not supported - do not set `LangVersion=latest` because it makes builds machine-dependent - use `LangVersion=preview` only when the repo intentionally opts into preview Useful checks: ```bash rg -n "TargetFramework|TargetFrameworks|LangVersion" -g '*.csproj' -g 'Directory.Build.*' . ``` If the repo's current language version is unclear, the compiler can reveal it: ```csharp #error version ``` As of March 8, 2026: - `C# 14` is the latest stable version - `C# 15` exists in preview, but should not be a default choice for production repo guidance ## Practical Adoption Rules - prefer stable features that remove ceremony, improve correctness, or improve performance without obscuring intent - do not rewrite entire codebases just to use new syntax - modernize opportunistically when touching the code anyway - coordinate feature adoption with `.editorconfig`, analyzers, and architecture rules ## Version Guide ### C# 8 Typical pairing: `.NET Core 3.x` Key features: - readonly members - default interface members - switch expressions - property patterns - tuple patterns - positional patterns - using declarations - static local functions - disposable `ref struct` - nullable reference types - asynchronous streams - indices and ranges - null-coalescing assignment - unmanaged constructed types - stackalloc in nested expressions - improved interpolated verbatim strings ### C# 9 Typical pairing: `.NET 5` Key features: - records - init-only setters - top-level statements - relational patterns - logical patterns - native-sized integers - function pointers - module initializers - target-typed `new` - static anonymous functions - target-typed conditional expressions - covariant return types - extension `GetEnumerator` support in `foreach` - lambda discard parameters - attributes on local functions ### C# 10 Typical pairing: `.NET 6` Key features: - record structs - struct initialization improvements - interpolated string handlers - `global using` - file-scoped namespaces - extended property patterns - lambda natural type - explicit lambda return types - attributes on lambda expressions - `const` interpolated strings - `sealed` `ToString` in records - more accurate definite assignment and null-state analysis - mixed assignment and declaration in deconstruction - `AsyncMethodBuilder` on methods - `CallerArgumentExpression` - new `#line` format ### C# 11 Typical pairing: `.NET 7` Key features: - raw string literals - generic math support - generic attributes - UTF-8 string literals - newlines inside interpolation expressions - list patterns - file-local types - required members - auto-default structs - matching `Span<char>` against a constant string - extended `nameof` scope - `nint` and `nuint` aliases - `ref` fields and `scoped ref` - improved method-group conversion to delegate - warning wave 7 ### C# 12 Typical pairing: `.NET 8` Key features: - primary constructors - collection expressions - inline arrays - optional parameters in lambda expressions - `ref readonly` parameters - alias any type - `Experimental` attribute - interceptors as a preview feature ### C# 13 Released November 2024. Stable on `.NET 9`. Key features: - `params` collections - new `lock` type and semantics with `System.Threading.Lock` - `\e` escape sequence - method-group natural type improvements - implicit indexer access in object initializers - `ref` locals and `unsafe` contexts in iterators and async methods - `ref struct` can implement interfaces - `allows ref struct` anti-constraint for generics - partial properties and partial indexers - overload resolution priority - `field` contextual keyword as a preview feature in C# 13-era tooling Practical use: - use `System.Threading.Lock` when the repo is on `.NET 9` and wants the newer synchronization model - use `params ReadOnlySpan<T>` or related collection forms in performance-sensitive APIs - use partial properties or indexers only where partial-type generation patterns already exist - do not assume `field` is safe unless the repo explicitly opted into preview behavior ### C# 14 Released November 2025. Stable on `.NET 10`. Key features: - extension members - null-conditional assignment - `nameof` with unbound generic types such as `nameof(List<>)` - more implicit conversions for `Span<T>` and `ReadOnlySpan<T>` - modifiers on simple lambda parameters - field-backed properties via `field` - partial events and partial constructors - user-defined compound assignment operators - file-based app preprocessor directives Practical use: - use null-conditional assignment to remove boilerplate null guards when side effects are clear - use field-backed properties when you need simple validation in an otherwise auto-property style - use extension members when the repo wants richer extension APIs, not for cosmetic rewrites - use span-related improvements in performance-sensitive code paths, not as blanket style churn ## Sources - [Configure C# language version](https://learn.microsoft.com/dotnet/csharp/language-reference/configure-language-version) - [The history of C#](https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-version-history) - [What's new in C# 13](https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-13) - [What's new in C# 14](https://learn.microsoft.com/dotnet/csharp/whats-new/csharp-14) -
migration.md 15.2 KB
# C# Version Migration Guide This reference provides guidance for migrating codebases from older C# versions to modern C# (12, 13, 14). All examples demonstrate the modern approach using primary constructors and current patterns. ## Migration Strategy ### Assess Before Migrating 1. Identify your current `LangVersion` and `TargetFramework` 2. Review breaking changes for each version jump 3. Prioritize high-value modernizations over cosmetic changes 4. Use analyzers to identify migration opportunities ```bash # Check current settings rg -n "TargetFramework|LangVersion|TargetFrameworks" -g '*.csproj' -g 'Directory.Build.*' . ``` ### Incremental Approach Migrate in phases rather than all at once: 1. **Phase 1**: Non-breaking syntax updates (expression bodies, null operators) 2. **Phase 2**: Type modernization (records, primary constructors) 3. **Phase 3**: Collection and pattern updates 4. **Phase 4**: Advanced features (generic math, ref structs) ## Migrating to C# 12 ### Constructor to Primary Constructor **Before (Traditional Constructor)**: ```csharp public class OrderService { private readonly IOrderRepository _repository; private readonly ILogger<OrderService> _logger; public OrderService(IOrderRepository repository, ILogger<OrderService> logger) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task<Order?> GetAsync(Guid id) { _logger.LogInformation("Fetching order {Id}", id); return await _repository.FindAsync(id); } } ``` **After (Primary Constructor)**: ```csharp public class OrderService(IOrderRepository repository, ILogger<OrderService> logger) { public async Task<Order?> GetAsync(Guid id) { logger.LogInformation("Fetching order {Id}", id); return await repository.FindAsync(id); } } ``` ### When Primary Constructors Are Not Suitable Keep traditional constructors when: - You need multiple constructor overloads with complex logic - Constructor parameters require significant validation - You need explicit field visibility modifiers - The class is a base class and derived classes need constructor chaining ```csharp // Primary constructor not ideal here - complex validation needed public class ValidatedEntity { public Guid Id { get; } public string Name { get; } public ValidatedEntity(Guid id, string name) { if (id == Guid.Empty) throw new ArgumentException("Id cannot be empty", nameof(id)); ArgumentException.ThrowIfNullOrWhiteSpace(name); if (name.Length > 100) throw new ArgumentException("Name too long", nameof(name)); Id = id; Name = name.Trim(); } } ``` ### Array Initialization to Collection Expressions **Before**: ```csharp private readonly string[] _allowedExtensions = new[] { ".jpg", ".png", ".gif" }; private readonly List<int> _numbers = new List<int> { 1, 2, 3 }; private readonly HashSet<string> _tags = new HashSet<string>(); ``` **After**: ```csharp private readonly string[] _allowedExtensions = [".jpg", ".png", ".gif"]; private readonly List<int> _numbers = [1, 2, 3]; private readonly HashSet<string> _tags = []; ``` ### Spread Operator for Collection Concatenation **Before**: ```csharp public string[] GetAllArgs(string[] baseArgs, string outputPath) { var result = new List<string>(baseArgs); result.Add("--output"); result.Add(outputPath); return result.ToArray(); } ``` **After**: ```csharp public string[] GetAllArgs(string[] baseArgs, string outputPath) => [..baseArgs, "--output", outputPath]; ``` ## Migrating to C# 11 ### String Concatenation to Raw String Literals **Before**: ```csharp public class QueryGenerator { public string GenerateQuery(string table) { return "SELECT *\n" + "FROM " + table + "\n" + "WHERE active = true\n" + "ORDER BY created_at DESC"; } } ``` **After**: ```csharp public class QueryGenerator(string defaultSchema) { public string GenerateQuery(string table) => $""" SELECT * FROM {defaultSchema}.{table} WHERE active = true ORDER BY created_at DESC """; } ``` ### Generic Constraints to Generic Math **Before**: ```csharp public static class MathUtils { public static int Sum(IEnumerable<int> values) { int sum = 0; foreach (var v in values) sum += v; return sum; } public static double Sum(IEnumerable<double> values) { double sum = 0; foreach (var v in values) sum += v; return sum; } } ``` **After**: ```csharp public static class MathUtils { public static T Sum<T>(IEnumerable<T> values) where T : INumber<T> { T sum = T.Zero; foreach (var v in values) sum += v; return sum; } } ``` ### Required Members **Before**: ```csharp public class UserRequest { public string Email { get; set; } = null!; // Suppress warning, hope caller sets it public string Name { get; set; } = null!; } ``` **After**: ```csharp public class UserRequest { public required string Email { get; init; } public required string Name { get; init; } } ``` ### List Patterns **Before**: ```csharp public string ParseCommand(string[] args) { if (args.Length == 0) return "No command"; if (args.Length == 1 && args[0] == "--help") return "Show help"; if (args.Length >= 2 && args[0] == "--config") return $"Config: {args[1]}"; return "Unknown"; } ``` **After**: ```csharp public string ParseCommand(string[] args) => args switch { [] => "No command", ["--help"] => "Show help", ["--config", var path, ..] => $"Config: {path}", _ => "Unknown" }; ``` ## Migrating to C# 10 ### Record Structs **Before**: ```csharp public struct Point : IEquatable<Point> { public double X { get; } public double Y { get; } public Point(double x, double y) { X = x; Y = y; } public bool Equals(Point other) => X == other.X && Y == other.Y; public override bool Equals(object? obj) => obj is Point p && Equals(p); public override int GetHashCode() => HashCode.Combine(X, Y); } ``` **After**: ```csharp public readonly record struct Point(double X, double Y); ``` ### Global Usings **Before (every file)**: ```csharp using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; ``` **After (GlobalUsings.cs or Directory.Build.props)**: ```csharp // GlobalUsings.cs global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading.Tasks; global using Microsoft.Extensions.Logging; ``` ### File-Scoped Namespaces **Before**: ```csharp namespace MyApp.Services { public class OrderService { // Implementation } } ``` **After**: ```csharp namespace MyApp.Services; public class OrderService(IOrderRepository repository) { // Implementation } ``` ### Null Parameter Checks **Before**: ```csharp public void Process(string value) { if (value == null) throw new ArgumentNullException(nameof(value)); if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Value cannot be empty", nameof(value)); // Process } ``` **After**: ```csharp public void Process(string value) { ArgumentNullException.ThrowIfNull(value); ArgumentException.ThrowIfNullOrWhiteSpace(value); // Process } ``` ## Migrating to C# 9 ### Classes to Records **Before**: ```csharp public class OrderDto { public Guid Id { get; set; } public string CustomerName { get; set; } = ""; public decimal Total { get; set; } public override bool Equals(object? obj) { return obj is OrderDto dto && Id == dto.Id && CustomerName == dto.CustomerName && Total == dto.Total; } public override int GetHashCode() => HashCode.Combine(Id, CustomerName, Total); } ``` **After**: ```csharp public record OrderDto(Guid Id, string CustomerName, decimal Total); ``` ### Init-Only Properties **Before**: ```csharp public class Config { public string ConnectionString { get; } public Config(string connectionString) { ConnectionString = connectionString; } } ``` **After**: ```csharp public class Config { public required string ConnectionString { get; init; } } ``` ### Target-Typed New **Before**: ```csharp private readonly Dictionary<string, List<Order>> _ordersByCustomer = new Dictionary<string, List<Order>>(); ``` **After**: ```csharp private readonly Dictionary<string, List<Order>> _ordersByCustomer = []; ``` ### Pattern Matching Improvements **Before**: ```csharp public decimal CalculateDiscount(object customer) { if (customer is PremiumCustomer premium && premium.YearsActive > 5) return 0.20m; if (customer is PremiumCustomer) return 0.10m; if (customer is Customer c && c.OrderCount > 100) return 0.05m; return 0m; } ``` **After**: ```csharp public decimal CalculateDiscount(object customer) => customer switch { PremiumCustomer { YearsActive: > 5 } => 0.20m, PremiumCustomer => 0.10m, Customer { OrderCount: > 100 } => 0.05m, _ => 0m }; ``` ## Migrating to C# 8 ### Nullable Reference Types Enable in project file: ```xml <PropertyGroup> <Nullable>enable</Nullable> </PropertyGroup> ``` **Before**: ```csharp public class UserService { public User GetUser(string id) { // May return null, caller doesn't know return _repository.Find(id); } } ``` **After**: ```csharp public class UserService(IUserRepository repository) { public User? GetUser(string id) => repository.Find(id); public User GetUserOrThrow(string id) => repository.Find(id) ?? throw new UserNotFoundException(id); } ``` ### Switch Expressions **Before**: ```csharp public string GetStatusText(OrderStatus status) { switch (status) { case OrderStatus.Pending: return "Waiting for processing"; case OrderStatus.Processing: return "Being prepared"; case OrderStatus.Shipped: return "On the way"; case OrderStatus.Delivered: return "Arrived"; default: throw new ArgumentOutOfRangeException(nameof(status)); } } ``` **After**: ```csharp public string GetStatusText(OrderStatus status) => status switch { OrderStatus.Pending => "Waiting for processing", OrderStatus.Processing => "Being prepared", OrderStatus.Shipped => "On the way", OrderStatus.Delivered => "Arrived", _ => throw new ArgumentOutOfRangeException(nameof(status)) }; ``` ### Using Declarations **Before**: ```csharp public async Task<string> ReadFileAsync(string path) { using (var stream = File.OpenRead(path)) using (var reader = new StreamReader(stream)) { return await reader.ReadToEndAsync(); } } ``` **After**: ```csharp public async Task<string> ReadFileAsync(string path) { using var stream = File.OpenRead(path); using var reader = new StreamReader(stream); return await reader.ReadToEndAsync(); } ``` ### Async Streams **Before**: ```csharp public async Task<List<Order>> GetAllOrdersAsync() { var result = new List<Order>(); // Load all into memory foreach (var batch in await _repository.GetBatchesAsync()) { result.AddRange(batch); } return result; } ``` **After**: ```csharp public class OrderReader(IOrderRepository repository) { public async IAsyncEnumerable<Order> GetAllOrdersAsync( [EnumeratorCancellation] CancellationToken ct = default) { await foreach (var batch in repository.GetBatchesAsync(ct)) { foreach (var order in batch) { yield return order; } } } } ``` ### Indices and Ranges **Before**: ```csharp var lastItem = array[array.Length - 1]; var lastThree = array.Skip(array.Length - 3).ToArray(); var middle = array.Skip(1).Take(array.Length - 2).ToArray(); ``` **After**: ```csharp var lastItem = array[^1]; var lastThree = array[^3..]; var middle = array[1..^1]; ``` ## C# 13 Migration ### params Collections **Before**: ```csharp public void Log(params string[] messages) { foreach (var msg in messages) Console.WriteLine(msg); } ``` **After (more flexible)**: ```csharp public void Log(params IEnumerable<string> messages) { foreach (var msg in messages) Console.WriteLine(msg); } // Or for zero-allocation public int Sum(params ReadOnlySpan<int> values) { int sum = 0; foreach (var v in values) sum += v; return sum; } ``` ### Lock Object **Before**: ```csharp public class Counter { private readonly object _lock = new(); private int _count; public int Increment() { lock (_lock) { return ++_count; } } } ``` **After**: ```csharp public class Counter { private readonly Lock _lock = new(); private int _count; public int Increment() { lock (_lock) { return ++_count; } } } ``` ## C# 14 Migration ### Field Keyword **Before**: ```csharp public class Person { private string _name = ""; public string Name { get => _name; set => _name = value?.Trim() ?? throw new ArgumentNullException(nameof(value)); } } ``` **After**: ```csharp public class Person { public string Name { get => field; set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value)); } } ``` ## Common Migration Anti-Patterns ### Do Not Over-Modernize Avoid changing working code just to use new syntax. Prioritize: 1. Bug fixes and correctness improvements 2. Readability improvements 3. Performance improvements 4. Cosmetic syntax updates (lowest priority) ### Do Not Break Compatibility Before using new features, verify your target framework supports them: | Feature | Minimum C# | Minimum .NET | |---------|------------|--------------| | Primary Constructors | C# 12 | .NET 8 | | Collection Expressions | C# 12 | .NET 8 | | Raw String Literals | C# 11 | .NET 7 | | Required Members | C# 11 | .NET 7 | | Records | C# 9 | .NET 5 | | Nullable Reference Types | C# 8 | .NET Core 3.0 | ### Do Not Ignore Analyzers Enable and fix analyzer warnings during migration: ```xml <PropertyGroup> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <WarningsAsErrors>nullable</WarningsAsErrors> </PropertyGroup> ``` ## Project File Updates ### Enable Modern Features ```xml <PropertyGroup> <TargetFramework>net9.0</TargetFramework> <LangVersion>13</LangVersion> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> ``` ### For Libraries Supporting Multiple Frameworks ```xml <PropertyGroup> <TargetFrameworks>net8.0;net9.0</TargetFrameworks> <LangVersion>12</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> ``` ## Validation After Migration After migrating, verify: 1. **Build succeeds**: `dotnet build` 2. **Tests pass**: `dotnet test` 3. **No new warnings**: Check build output 4. **Analyzer compliance**: Run `dotnet format --verify-no-changes` 5. **Runtime behavior**: Run integration/E2E tests -
patterns.md 12.5 KB
# Modern C# Patterns This reference covers idiomatic patterns for C# 12, C# 13, and C# 14. All examples use primary constructors and modern syntax. ## Primary Constructors (C# 12+) Primary constructors reduce boilerplate by declaring constructor parameters directly on the type declaration. ### Classes with Primary Constructors ```csharp // Modern: Primary constructor with dependency injection public class OrderService(IOrderRepository repository, ILogger<OrderService> logger) { public async Task<Order?> GetOrderAsync(Guid id) { logger.LogInformation("Fetching order {OrderId}", id); return await repository.FindAsync(id); } } // Primary constructor with field initialization public class CacheService(IMemoryCache cache, TimeSpan defaultExpiration) { private readonly TimeSpan _expiration = defaultExpiration; public void Set<T>(string key, T value) => cache.Set(key, value, _expiration); } ``` ### When to Capture vs. Assign ```csharp // Capture directly when parameter is readonly and used as-is public class UserValidator(IUserRepository repository) { public async Task<bool> ExistsAsync(string email) => await repository.ExistsAsync(email); } // Assign to field when you need mutability or different visibility public class Counter(int initialValue) { private int _count = initialValue; public int Increment() => ++_count; } ``` ## Records (C# 9+) Records provide value semantics with minimal ceremony. ### Record Patterns ```csharp // Immutable data transfer public record OrderDto(Guid Id, string CustomerName, decimal Total); // Record with computed property public record Rectangle(double Width, double Height) { public double Area => Width * Height; } // Record struct for high-performance scenarios (C# 10+) public readonly record struct Point(double X, double Y); // Record with validation in primary constructor body public record Email { public string Value { get; } public Email(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); if (!value.Contains('@')) throw new ArgumentException("Invalid email format", nameof(value)); Value = value; } } ``` ### With-Expressions ```csharp var order = new OrderDto(Guid.NewGuid(), "Alice", 99.99m); var updated = order with { Total = 149.99m }; ``` ## Pattern Matching (C# 8-14) ### Type Patterns ```csharp public class PaymentProcessor(ILogger<PaymentProcessor> logger) { public decimal ProcessPayment(Payment payment) => payment switch { CreditCardPayment { Amount: > 1000 } cc => ProcessLargeCredit(cc), CreditCardPayment cc => ProcessCredit(cc), BankTransfer bt => ProcessTransfer(bt), CryptoPayment { Currency: "BTC" } crypto => ProcessBitcoin(crypto), CryptoPayment crypto => ProcessCrypto(crypto), null => throw new ArgumentNullException(nameof(payment)), _ => throw new NotSupportedException($"Unknown payment type: {payment.GetType()}") }; } ``` ### Property Patterns ```csharp public static string DescribeOrder(Order order) => order switch { { Status: OrderStatus.Pending, Total: > 500 } => "Large pending order", { Status: OrderStatus.Pending } => "Pending order", { Status: OrderStatus.Shipped, TrackingNumber: not null } => "In transit", { Status: OrderStatus.Delivered } => "Delivered", _ => "Unknown status" }; ``` ### List Patterns (C# 11+) ```csharp public static string AnalyzeArgs(string[] args) => args switch { [] => "No arguments", [var single] => $"Single argument: {single}", ["--help" or "-h", ..] => "Help requested", ["--config", var path, ..] => $"Config file: {path}", [var first, .., var last] => $"First: {first}, Last: {last}", _ => $"Multiple arguments: {args.Length}" }; ``` ### Relational and Logical Patterns ```csharp public static string GetDiscount(int quantity) => quantity switch { <= 0 => throw new ArgumentOutOfRangeException(nameof(quantity)), < 10 => "No discount", >= 10 and < 50 => "10% discount", >= 50 and < 100 => "20% discount", >= 100 => "30% discount" }; ``` ## Collection Expressions (C# 12+) Collection expressions provide a unified syntax for creating collections. ### Basic Collection Expressions ```csharp // Arrays int[] numbers = [1, 2, 3, 4, 5]; // Lists List<string> names = ["Alice", "Bob", "Charlie"]; // Immutable collections ImmutableArray<int> immutable = [1, 2, 3]; // Empty collections List<Order> orders = []; // HashSets HashSet<string> tags = ["dotnet", "csharp", "modern"]; ``` ### Spread Operator ```csharp int[] first = [1, 2, 3]; int[] second = [4, 5, 6]; int[] combined = [..first, ..second]; // [1, 2, 3, 4, 5, 6] // Useful for building commands or arguments string[] baseArgs = ["--verbose", "--format=json"]; string[] allArgs = [..baseArgs, "--output", outputPath]; ``` ### Collection Expressions in Methods ```csharp public class ConfigurationBuilder(string environment) { public IReadOnlyList<string> GetSearchPaths() => [ $"/etc/app/{environment}", $"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}/.app", "./config" ]; } ``` ## Required Members (C# 11+) ```csharp public class CreateUserRequest { public required string Email { get; init; } public required string Name { get; init; } public string? PhoneNumber { get; init; } } // Usage var request = new CreateUserRequest { Email = "user@example.com", Name = "John Doe" }; ``` ## File-Scoped Types (C# 11+) ```csharp // Internal helper visible only within this file file class TemporaryBuffer(int capacity) { private readonly byte[] _buffer = new byte[capacity]; public Span<byte> GetSpan() => _buffer; } ``` ## Raw String Literals (C# 11+) ```csharp public class QueryBuilder(string tableName) { public string BuildSelectQuery(IEnumerable<string> columns) => $""" SELECT {string.Join(", ", columns)} FROM {tableName} WHERE deleted_at IS NULL ORDER BY created_at DESC """; public string BuildJsonTemplate() => """ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } } """; } ``` ## Generic Math (C# 11+) ```csharp public static class MathExtensions { public static T Sum<T>(this IEnumerable<T> source) where T : INumber<T> { T result = T.Zero; foreach (var item in source) result += item; return result; } public static T Average<T>(this IEnumerable<T> source) where T : INumber<T>, IDivisionOperators<T, int, T> { T sum = T.Zero; int count = 0; foreach (var item in source) { sum += item; count++; } return count == 0 ? T.Zero : sum / count; } } ``` ## ref Fields and scoped (C# 11+) ```csharp public ref struct SpanReader(ReadOnlySpan<byte> buffer) { private ReadOnlySpan<byte> _buffer = buffer; private int _position = 0; public bool TryReadByte(out byte value) { if (_position >= _buffer.Length) { value = 0; return false; } value = _buffer[_position++]; return true; } } ``` ## Inline Arrays (C# 12+) ```csharp [InlineArray(16)] public struct Buffer16 { private byte _element0; } // Usage in performance-critical code public ref struct SmallBuffer(Buffer16 storage) { private Buffer16 _storage = storage; public Span<byte> AsSpan() => _storage; } ``` ## Interceptors (C# 12+ Preview) Interceptors are a preview feature for compile-time method interception, primarily used by source generators. ```csharp // Note: Preview feature, requires <InterceptorsPreviewNamespaces> [InterceptsLocation("Program.cs", line: 10, column: 5)] public static void InterceptedMethod() { } ``` ## C# 13 Features ### params Collections ```csharp // params now works with any collection type public static void LogMessages(params IEnumerable<string> messages) { foreach (var message in messages) Console.WriteLine(message); } // Can pass spans for zero-allocation scenarios public static int Sum(params ReadOnlySpan<int> values) { int sum = 0; foreach (var value in values) sum += value; return sum; } ``` ### Lock Object ```csharp public class ThreadSafeCounter { private readonly Lock _lock = new(); private int _count; public int Increment() { lock (_lock) { return ++_count; } } } ``` ### Escape Sequence \e ```csharp // ANSI escape for terminal colors public static string Red(string text) => $"\e[31m{text}\e[0m"; public static string Green(string text) => $"\e[32m{text}\e[0m"; ``` ### Implicit Index Access in Initializers ```csharp public class Buffer { public byte[] Data { get; } = new byte[10]; } var buffer = new Buffer { Data = { [^1] = 255, // Last element [^2] = 128 // Second to last } }; ``` ## C# 14 Features ### Field Keyword ```csharp // Direct access to auto-property backing field public class Person { public string Name { get => field; set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value)); } public int Age { get => field; set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value)); } } ``` ### Extension Types (Preview) ```csharp // Extension everything - methods, properties, operators extension StringExtensions for string { public bool IsValidEmail => this.Contains('@') && this.Contains('.'); public string Reverse() { var chars = this.ToCharArray(); Array.Reverse(chars); return new string(chars); } } ``` ## Async Patterns ### Modern Async with Primary Constructors ```csharp public class DataSyncService( ISourceRepository source, IDestinationRepository destination, ILogger<DataSyncService> logger) { public async IAsyncEnumerable<SyncResult> SyncAllAsync( [EnumeratorCancellation] CancellationToken ct = default) { await foreach (var item in source.GetAllAsync(ct)) { var result = await SyncItemAsync(item, ct); yield return result; } } private async Task<SyncResult> SyncItemAsync(DataItem item, CancellationToken ct) { try { await destination.UpsertAsync(item, ct); return new SyncResult(item.Id, Success: true); } catch (Exception ex) { logger.LogError(ex, "Failed to sync item {Id}", item.Id); return new SyncResult(item.Id, Success: false, Error: ex.Message); } } } public record SyncResult(string Id, bool Success, string? Error = null); ``` ## Null Handling Patterns ### Modern Null Checks ```csharp public class UserService(IUserRepository repository) { // ArgumentNullException helpers (C# 10+) public async Task<User> GetUserAsync(string id) { ArgumentNullException.ThrowIfNull(id); ArgumentException.ThrowIfNullOrWhiteSpace(id); return await repository.FindAsync(id) ?? throw new UserNotFoundException(id); } // Null-coalescing assignment public User GetOrCreateDefault(string id) { _cache[id] ??= new User(id, "Default"); return _cache[id]; } private readonly Dictionary<string, User> _cache = []; } ``` ### Pattern-Based Null Checks ```csharp public static string FormatUser(User? user) => user switch { null => "No user", { Name: null or "" } => $"User {user.Id} (no name)", { Name: var name, Email: var email } => $"{name} <{email}>" }; ``` ## LINQ Modernization ### Index and Range in LINQ ```csharp public class PagedQuery(IQueryable<Order> source) { public IQueryable<Order> GetPage(int pageNumber, int pageSize) => source .OrderByDescending(o => o.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize); // Using ranges with materialized collections public Order[] GetLastN(Order[] orders, int n) => orders[^n..]; } ``` ### Collection Builders ```csharp public static class QueryBuilder { public static IEnumerable<T> BuildQuery<T>( IEnumerable<T> source, Func<T, bool>? filter = null, Func<T, object>? orderBy = null) { var query = source; if (filter is not null) query = query.Where(filter); if (orderBy is not null) query = query.OrderBy(orderBy); return query; } } ```
-
-
SKILL.md 5.1 KB
--- name: dotnet-modern-csharp version: "1.0.0" category: "Code Quality" description: "Write modern, version-aware C# for .NET repositories. Use when choosing language features across C# versions, especially C# 13 and C# 14, while staying compatible with the repo's target framework and `LangVersion`." compatibility: "Requires a C# or .NET repository; respects explicit `LangVersion` and target framework settings." --- # Modern C# for .NET ## Trigger On - the repo wants more modern idiomatic C# code - a change depends on language-version compatibility - the team is upgrading or reviewing C# feature usage across versions - you need to know whether a C# 13 or C# 14 feature is safe to use ## Value - produce a concrete project delta: code, docs, config, tests, CI, or review artifact - reduce ambiguity through explicit planning, verification, and final validation skills - leave reusable project context so future tasks are faster and safer ## Do Not Use For - non-C# .NET languages such as F# or VB - analyzer-only or formatter-only setup with no language feature choice ## Inputs - target `TFM` or `TFMs` - explicit `LangVersion`, if any - current SDK version - team style rules in `.editorconfig` and `AGENTS.md` ## Quick Start 1. Read the nearest `AGENTS.md` and confirm scope and constraints. 2. Run this skill's `Workflow` through the `Ralph Loop` until outcomes are acceptable. 3. Return the `Required Result Format` with concrete artifacts and verification evidence. ## Workflow 1. Detect the real language ceiling from the repo's target framework and explicit `LangVersion`. 2. Prefer stable features that the current repo actually supports. 3. Use modern syntax when it reduces ceremony, improves correctness, or makes invariants clearer. 4. Do not mass-rewrite a codebase into newer syntax unless the repo wants that churn. 5. Treat preview features as opt-in only. Never assume preview because the current machine has a newer SDK. 6. Pay special attention to C# 13 and C# 14: - C# 13 is the stable language for `.NET 9` - C# 14 is the stable language for `.NET 10` 7. When feature selection changes architecture, style rules, or generated-code patterns, coordinate with: - `dotnet` - `dotnet-analyzer-config` - `dotnet-architecture` 8. After feature-driven refactors, run the repo's .NET quality pass through `dotnet`. ## Bootstrap When Missing If the requested C# feature depends on SDK or language support the repo does not have yet: 1. Detect current state: - `dotnet --list-sdks` - `rg -n "TargetFramework|LangVersion|TargetFrameworks" -g '*.csproj' -g 'Directory.Build.*' .` 2. Confirm whether the repo wants to stay on the current stable language level or intentionally upgrade. 3. If the feature requires a newer supported SDK or target framework, upgrade the repo toolchain deliberately instead of relying on the local machine by accident. 4. If the repo needs explicit `LangVersion`, record it in project or shared MSBuild config. 5. Run `dotnet build SOLUTION_OR_PROJECT` after the feature or toolchain change and return `status: configured` or `status: improved`. 6. If the repo intentionally stays below the required language level, return `status: not_applicable`. ## Deliver - modern C# code that fits the repo's real language version - fewer obsolete patterns when a newer stable feature is clearer - no accidental preview or unsupported-language drift ## Validate - the chosen syntax is supported by the repo's `TFM` and `LangVersion` - the feature improves clarity, correctness, or maintainability - preview-only features are used only when the repo explicitly opted in - style and analyzer rules still agree with the new syntax ## Ralph Loop Use the Ralph Loop for every task, including docs, architecture, testing, and tooling work. 1. Plan first (mandatory): - analyze current state - define target outcome, constraints, and risks - write a detailed execution plan - list final validation skills to run at the end, with order and reason 2. Execute one planned step and produce a concrete delta. 3. Review the result and capture findings with actionable next fixes. 4. Apply fixes in small batches and rerun the relevant checks or review steps. 5. Update the plan after each iteration. 6. Repeat until outcomes are acceptable or only explicit exceptions remain. 7. If a dependency is missing, bootstrap it or return `status: not_applicable` with explicit reason and fallback path. ### Required Result Format - `status`: `complete` | `clean` | `improved` | `configured` | `not_applicable` | `blocked` - `plan`: concise plan and current iteration step - `actions_taken`: concrete changes made - `validation_skills`: final skills run, or skipped with reasons - `verification`: commands, checks, or review evidence summary - `remaining`: top unresolved items or `none` For setup-only requests with no execution, return `status: configured` and exact next commands. ## Load References - `references/patterns.md` - `references/migration.md` - `references/csharp-modern-features.md` ## Example Requests - "Make this C# code more modern." - "Which features can we use on .NET 9?" - "Review this repo for C# 13 or C# 14 opportunities."
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.