
Modern Csharp
- 38 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
modern-csharp is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- modern-csharp
- AI & Agent Building
- AI-coding skill
Modern Csharp by the numbers
- 38 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #8,404 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill modern-csharpAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 38 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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
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:
dotnetanalyzer-configarchitecture
8. After feature-driven refactors, run the repo's .NET quality pass through dotnet.
Current Upstream Notes
.NET runtimev9.0.17is servicing. It should not by itself justify language-feature rewrites..NET SDKv8.0.422is an 8.0 servicing SDK. Do not use C# 13 or C# 14 syntax in a repo pinned to that line unless the project explicitly configures a compatible newer compiler/toolset.
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-sdksrg -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
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.
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|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.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."
{
"version": "1.0.1",
"category": "Code Quality"
}
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 9maps toC# 13.NET 10maps toC# 14- newer language versions than the target framework supports are not supported
- do not set
LangVersion=latestbecause it makes builds machine-dependent - use
LangVersion=previewonly when the repo intentionally opts into preview
Useful checks:
rg -n "TargetFramework|TargetFrameworks|LangVersion" -g '*.csproj' -g 'Directory.Build.*' .If the repo's current language version is unclear, the compiler can reveal it:
#error versionAs of March 8, 2026:
C# 14is the latest stable versionC# 15exists 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
GetEnumeratorsupport inforeach - 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
constinterpolated stringssealedToStringin records- more accurate definite assignment and null-state analysis
- mixed assignment and declaration in deconstruction
AsyncMethodBuilderon methodsCallerArgumentExpression- new
#lineformat
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
nameofscope nintandnuintaliasesreffields andscoped 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 readonlyparameters- alias any type
Experimentalattribute- interceptors as a preview feature
C# 13
Released November 2024. Stable on .NET 9.
Key features:
paramscollections- new
locktype and semantics withSystem.Threading.Lock \eescape sequence- method-group natural type improvements
- implicit indexer access in object initializers
reflocals andunsafecontexts in iterators and async methodsref structcan implement interfacesallows ref structanti-constraint for generics- partial properties and partial indexers
- overload resolution priority
fieldcontextual keyword as a preview feature in C# 13-era tooling
Practical use:
- use
System.Threading.Lockwhen the repo is on.NET 9and 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
fieldis 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
nameofwith unbound generic types such asnameof(List<>)- more implicit conversions for
Span<T>andReadOnlySpan<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
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
# 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):
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):
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
// 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:
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:
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:
public string[] GetAllArgs(string[] baseArgs, string outputPath)
{
var result = new List<string>(baseArgs);
result.Add("--output");
result.Add(outputPath);
return result.ToArray();
}After:
public string[] GetAllArgs(string[] baseArgs, string outputPath) =>
[..baseArgs, "--output", outputPath];Migrating to C# 11
String Concatenation to Raw String Literals
Before:
public class QueryGenerator
{
public string GenerateQuery(string table)
{
return "SELECT *\n" +
"FROM " + table + "\n" +
"WHERE active = true\n" +
"ORDER BY created_at DESC";
}
}After:
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:
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:
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:
public class UserRequest
{
public string Email { get; set; } = null!; // Suppress warning, hope caller sets it
public string Name { get; set; } = null!;
}After:
public class UserRequest
{
public required string Email { get; init; }
public required string Name { get; init; }
}List Patterns
Before:
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:
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:
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:
public readonly record struct Point(double X, double Y);Global Usings
Before (every file):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;After (GlobalUsings.cs or Directory.Build.props):
// 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:
namespace MyApp.Services
{
public class OrderService
{
// Implementation
}
}After:
namespace MyApp.Services;
public class OrderService(IOrderRepository repository)
{
// Implementation
}Null Parameter Checks
Before:
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:
public void Process(string value)
{
ArgumentNullException.ThrowIfNull(value);
ArgumentException.ThrowIfNullOrWhiteSpace(value);
// Process
}Migrating to C# 9
Classes to Records
Before:
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:
public record OrderDto(Guid Id, string CustomerName, decimal Total);Init-Only Properties
Before:
public class Config
{
public string ConnectionString { get; }
public Config(string connectionString)
{
ConnectionString = connectionString;
}
}After:
public class Config
{
public required string ConnectionString { get; init; }
}Target-Typed New
Before:
private readonly Dictionary<string, List<Order>> _ordersByCustomer =
new Dictionary<string, List<Order>>();After:
private readonly Dictionary<string, List<Order>> _ordersByCustomer = [];Pattern Matching Improvements
Before:
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:
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:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>Before:
public class UserService
{
public User GetUser(string id)
{
// May return null, caller doesn't know
return _repository.Find(id);
}
}After:
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:
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:
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:
public async Task<string> ReadFileAsync(string path)
{
using (var stream = File.OpenRead(path))
using (var reader = new StreamReader(stream))
{
return await reader.ReadToEndAsync();
}
}After:
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:
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:
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:
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:
var lastItem = array[^1];
var lastThree = array[^3..];
var middle = array[1..^1];C# 13 Migration
params Collections
Before:
public void Log(params string[] messages)
{
foreach (var msg in messages)
Console.WriteLine(msg);
}After (more flexible):
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:
public class Counter
{
private readonly object _lock = new();
private int _count;
public int Increment()
{
lock (_lock)
{
return ++_count;
}
}
}After:
public class Counter
{
private readonly Lock _lock = new();
private int _count;
public int Increment()
{
lock (_lock)
{
return ++_count;
}
}
}C# 14 Migration
Field Keyword
Before:
public class Person
{
private string _name = "";
public string Name
{
get => _name;
set => _name = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
}After:
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:
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>Project File Updates
Enable Modern Features
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>13</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>For Libraries Supporting Multiple Frameworks
<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
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
// 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
// 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
// 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
var order = new OrderDto(Guid.NewGuid(), "Alice", 99.99m);
var updated = order with { Total = 149.99m };Pattern Matching (C# 8-14)
Type Patterns
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
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+)
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
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
// 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
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
public class ConfigurationBuilder(string environment)
{
public IReadOnlyList<string> GetSearchPaths() =>
[
$"/etc/app/{environment}",
$"{Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}/.app",
"./config"
];
}Required Members (C# 11+)
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+)
// 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+)
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+)
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+)
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+)
[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.
// Note: Preview feature, requires <InterceptorsPreviewNamespaces>
[InterceptsLocation("Program.cs", line: 10, column: 5)]
public static void InterceptedMethod() { }C# 13 Features
params Collections
// 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
public class ThreadSafeCounter
{
private readonly Lock _lock = new();
private int _count;
public int Increment()
{
lock (_lock)
{
return ++_count;
}
}
}Escape Sequence \e
// 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
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
// 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)
// 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
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
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
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
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
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;
}
}