Loading...
Loading...
Using records, pattern matching, primary constructors, collection expressions. C# 12-15 by TFM.
npx skill4agent add wshaddix/dotnet-skills dotnet-csharp-modern-patterns| TFM | C# | Key Language Features |
|---|---|---|
| net8.0 | 12 | Primary constructors, collection expressions, alias any type |
| net9.0 | 13 | |
| net10.0 | 14 | |
| net11.0 | 15 (preview) | Collection expression |
// 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 | |
| Domain value objects (Money, Email) | |
| Entities with identity (User, Order) | |
| High-throughput, small data | |
| Inheritance needed | |
var updated = order with { Total = order.Total + tax };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);
}
}readonlyreadonly// Explicit readonly field when immutability matters
public class Config(string connectionString)
{
private readonly string _connectionString = connectionString
?? throw new ArgumentNullException(nameof(connectionString));
}[...]// 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 = [];// 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. Do not use on earlier TFMs.<LangVersion>preview</LangVersion>
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
};requiredpublic 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 = "alice@example.com" };requiredfieldpublic class TemperatureSensor
{
public double Reading
{
get => field;
set => field = value >= -273.15
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
}net10.0+ only. On earlier TFMs, use a traditional private field.
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 traditionalextension methods.static
usingusing Point = (double X, double Y);
using UserId = System.Guid;
Point origin = (0, 0);
UserId id = UserId.NewGuid();paramsparamsSpan<T>ReadOnlySpan<T>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,only supports arrays.params
LockSystem.Threading.Lockobjectprivate readonly Lock _lock = new();
public void DoWork()
{
lock (_lock)
{
// thread-safe operation
}
}LockScopelock (object)net9.0+ only. On net8.0, useandprivate readonly object _gate = new();.lock (_gate)
// 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.
nameofstring name = nameof(List<>); // "List"
string name2 = nameof(Dictionary<,>); // "Dictionary"net10.0+ only.
IsExternalInitRequiredMemberAttributeinitrequiredrecordstring.Contains(char)#if#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); }
#endiffieldNote: This skill applies publicly documented design rationale. It does not represent or speak for the named sources.