.cursor/skills/dotnet-csharp-modern-patterns/SKILL.md
Using records, pattern matching, primary constructors, collection expressions. C# 12-15 by TFM.
npx skillsauth add AGIBuild/Fulora dotnet-csharp-modern-patternsInstall 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.
Modern C# language feature guidance adapted to the project's target framework. Always run [skill:dotnet-version-detection] first to determine TFM and C# version.
Cross-references: [skill:dotnet-csharp-coding-standards] for naming/style conventions, [skill:dotnet-csharp-async-patterns] for async-specific patterns.
| TFM | C# | Key Language Features |
|-----|----|-----------------------|
| net8.0 | 12 | Primary constructors, collection expressions, alias any type |
| net9.0 | 13 | params collections, Lock type, partial properties |
| net10.0 | 14 | field keyword, extension blocks, nameof unbound generics |
| net11.0 | 15 (preview) | Collection expression with() arguments |
Use records for immutable data transfer objects, value semantics, and domain modeling where equality is based on values rather than identity.
// Positional record: concise, immutable, value equality
public record OrderSummary(int OrderId, decimal Total, DateOnly OrderDate);
// With additional members
public record Customer(string Name, string Email)
{
public string DisplayName => $"{Name} <{Email}>";
}
// Positional record struct: value type with value semantics
public readonly record struct Point(double X, double Y);
// Mutable record struct (rare -- prefer readonly)
public record struct MutablePoint(double X, double Y);
| Use Case | Prefer |
|----------|--------|
| DTOs, API responses | record |
| Domain value objects (Money, Email) | readonly record struct |
| Entities with identity (User, Order) | class |
| High-throughput, small data | readonly record struct |
| Inheritance needed | record (class-based) |
var updated = order with { Total = order.Total + tax };
Capture constructor parameters directly in the class/struct body. Parameters become available throughout the type but are not fields or properties -- they are captured state.
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
public async Task<Order> GetAsync(int id)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await repo.GetByIdAsync(id);
}
}
readonly fields. If immutability matters, assign to a readonly field in the body.// Explicit readonly field when immutability matters
public class Config(string connectionString)
{
private readonly string _connectionString = connectionString
?? throw new ArgumentNullException(nameof(connectionString));
}
Unified syntax for creating collections with [...].
// Array
int[] numbers = [1, 2, 3];
// List
List<string> names = ["Alice", "Bob"];
// Span
ReadOnlySpan<byte> bytes = [0x00, 0xFF];
// Spread operator
int[] combined = [..first, ..second, 99];
// Empty collection
List<int> empty = [];
Specify capacity, comparers, or other constructor arguments:
// Capacity hint
List<int> nums = [with(capacity: 1000), ..Generate()];
// Custom comparer
HashSet<string> set = [with(comparer: StringComparer.OrdinalIgnoreCase), "Alice", "bob"];
// Dictionary with comparer
Dictionary<string, int> map = [with(comparer: StringComparer.OrdinalIgnoreCase),
new("key1", 1), new("key2", 2)];
net11.0+ only. Requires
<LangVersion>preview</LangVersion>. Do not use on earlier TFMs.
string GetDiscount(Customer customer) => customer switch
{
{ Tier: "Gold", YearsActive: > 5 } => "30%",
{ Tier: "Gold" } => "20%",
{ Tier: "Silver" } => "10%",
_ => "0%"
};
bool IsValid(int[] data) => data is [> 0, .., > 0]; // first and last positive
string Describe(int[] values) => values switch
{
[] => "empty",
[var single] => $"single: {single}",
[var first, .., var last] => $"range: {first}..{last}"
};
decimal CalculateShipping(object package) => package switch
{
Letter { Weight: < 50 } => 0.50m,
Parcel { Weight: var w } when w < 1000 => 5.00m + w * 0.01m,
Parcel { IsOversized: true } => 25.00m,
_ => 10.00m
};
required Members (C# 11+)Force callers to initialize properties at construction via object initializers.
public class UserDto
{
public required string Name { get; init; }
public required string Email { get; init; }
public string? Phone { get; init; }
}
// Compiler enforces Name and Email
var user = new UserDto { Name = "Alice", Email = "[email protected]" };
Useful for DTOs that need to be deserialized (System.Text.Json honors required in .NET 8+).
field Keyword (C# 14, net10.0+)Access the compiler-generated backing field directly in property accessors.
public class TemperatureSensor
{
public double Reading
{
get => field;
set => field = value >= -273.15
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
}
Replaces the manual pattern of declaring a private field plus a property with custom logic. Use when you need validation or transformation in a setter without a separate backing field.
net10.0+ only. On earlier TFMs, use a traditional private field.
Group extension members for a type in a single block.
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source) where T : class
{
public IEnumerable<T> WhereNotNull()
=> source.Where(x => x is not null);
public bool IsEmpty()
=> !source.Any();
}
}
net10.0+ only. On earlier TFMs, use traditional
staticextension methods.
using, C# 12+, net8.0+)using Point = (double X, double Y);
using UserId = System.Guid;
Point origin = (0, 0);
UserId id = UserId.NewGuid();
Useful for tuple aliases and domain type aliases without creating a full type.
params Collections (C# 13, net9.0+)params now supports additional collection types beyond arrays, including Span<T>, ReadOnlySpan<T>, and types implementing certain collection interfaces.
public void Log(params ReadOnlySpan<string> messages)
{
foreach (var msg in messages)
Console.WriteLine(msg);
}
// Callers: compiler may avoid heap allocation with span-based params
Log("hello", "world");
net9.0+ only. On net8.0,
paramsonly supports arrays.
Lock Type (C# 13, net9.0+)Use System.Threading.Lock instead of object for locking.
private readonly Lock _lock = new();
public void DoWork()
{
lock (_lock)
{
// thread-safe operation
}
}
Lock provides a Scope-based API for advanced scenarios and is more expressive than lock (object).
net9.0+ only. On net8.0, use
private readonly object _gate = new();andlock (_gate).
Partial properties enable source generators to define property signatures that users implement, or vice versa.
// In generated file
public partial class ViewModel
{
public partial string Name { get; set; }
}
// In user file
public partial class ViewModel
{
private string _name = "";
public partial string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
}
net9.0+ only. See [skill:dotnet-csharp-source-generators] for generator patterns.
nameof for Unbound Generic Types (C# 14, net10.0+)string name = nameof(List<>); // "List"
string name2 = nameof(Dictionary<,>); // "Dictionary"
Useful in logging, diagnostics, and reflection scenarios.
net10.0+ only.
When targeting multiple TFMs, newer language features may not compile on older targets. Use these approaches:
IsExternalInit, RequiredMemberAttribute, etc.) so language features like init, required, and record work on older TFMs.string.Contains(char) for netstandard2.0).#if for features that cannot be polyfilled:#if NET10_0_OR_GREATER
// Use field keyword
public double Value { get => field; set => field = Math.Max(0, value); }
#else
private double _value;
public double Value { get => _value; set => _value = Math.Max(0, value); }
#endif
See [skill:dotnet-multi-targeting] for comprehensive polyfill guidance.
Feature guidance in this skill is grounded in publicly available language design rationale from:
field keyword (eliminating backing field ceremony), and extension blocks (grouping extensions by target type). Each feature balances expressiveness with safety -- e.g., primary constructor parameters are intentionally mutable captures (not readonly) to keep the feature simple; use explicit readonly fields when immutability is needed. Source: https://github.com/dotnet/csharplang/tree/main/meetingsNote: This skill applies publicly documented design rationale. It does not represent or speak for the named sources.
tools
Captures learnings, errors, and corrections to enable continuous improvement. Use when: (1) A command or operation fails unexpectedly, (2) User corrects Claude ('No, that's wrong...', 'Actually...'), (3) User requests a capability that doesn't exist, (4) An external API or tool fails, (5) Claude realizes its knowledge is outdated or incorrect, (6) A better approach is discovered for a recurring task. Also review learnings before major tasks.
testing
Security headers configuration and best practices for ASP.NET Core Razor Pages applications. Covers CSP, HSTS, X-Frame-Options, and comprehensive security middleware setup. Use when configuring security headers in ASP.NET Core applications, implementing Content Security Policy (CSP), or setting up HSTS and other security-related HTTP headers.
development
Reviews designs and business goals for security vulnerabilities, data protection (in transit/at rest), authorization, and compliance alignment. Use when the user asks for a security review, threat modeling, attack surface analysis, data leakage prevention, or compliance/security assessment.
development
Best practices for building production-grade ASP.NET Core Razor Pages applications. Focuses on structure, lifecycle, binding, validation, security, and maintainability in web apps using Razor Pages as the primary UI framework. Use when building Razor Pages applications, designing PageModels and handlers, implementing model binding and validation, or securing Razor Pages with authentication and authorization.