| name | modern-csharp |
| description | C# 12–14 language features with practical patterns — primary constructors, collection expressions, records, pattern matching, nullable reference types, and more. |
Modern C# (12–14)
Reference for language features available in .NET 8+. Used by dnp-planner and dnp-tdd-developer-hard.
Primary Constructors (C# 12)
Eliminates constructor boilerplate. Parameters are in scope for the entire class body.
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly ILogger<OrderService> _logger;
public OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
_repo = repo;
_logger = logger;
}
}
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
{
logger.LogInformation("Fetching order {Id}", id);
return await repo.GetByIdAsync(id, ct);
}
}
Caveat: primary constructor parameters are captured by reference — if you need a private field (e.g., for mutation tracking), assign explicitly: private readonly IOrderRepository _repo = repo;
Collection Expressions (C# 12)
Unified syntax for arrays, lists, and spans. Supports spread (..) operator.
int[] ids = [1, 2, 3];
List<string> tags = ["dotnet", "csharp"];
var basePermissions = new[] { "read", "list" };
string[] adminPermissions = [..basePermissions, "write", "delete"];
IReadOnlyList<string> empty = [];
ProcessItems([1, 2, 3]);
Records and With-Expressions
Records are reference types with value semantics. Use for DTOs, domain events, and immutable data shapes.
public record Order(int Id, string CustomerId, string Status, decimal Total);
var order = new Order(1, "cust-42", "Pending", 99.99m);
var shipped = order with { Status = "Shipped" };
public record struct Money(decimal Amount, string Currency);
public record CreateOrderRequest
{
public required string CustomerId { get; init; }
public required List<OrderLine> Lines { get; init; }
}
Required Members (C# 11)
Enforces initialization at construction time — compile error if omitted in object initializer.
public class UserDto
{
public required string Email { get; init; }
public required string DisplayName { get; init; }
public string? AvatarUrl { get; init; }
}
var dto = new UserDto { Email = "a@b.com", DisplayName = "Alice" };
Pattern Matching (C# 8–13)
Switch Expressions
var label = order.Status switch
{
"Pending" => "Awaiting payment",
"Paid" => "Processing",
"Shipped" => "On the way",
"Delivered" => "Complete",
_ => throw new ArgumentOutOfRangeException(nameof(order.Status))
};
Property Patterns
bool isEligibleForDiscount = customer switch
{
{ Tier: "Gold", OrderCount: > 10 } => true,
{ Tier: "Silver", OrderCount: > 25 } => true,
_ => false
};
List Patterns (C# 11)
string Describe(int[] nums) => nums switch
{
[] => "empty",
[var single] => $"one element: {single}",
[var first, .., var last] => $"starts {first}, ends {last}",
_ => "multiple elements"
};
Type Patterns with Guards
decimal CalculateFee(PaymentMethod method) => method switch
{
CreditCard { Network: "Amex" } => 0.035m,
CreditCard card when card.IsRewards => 0.025m,
CreditCard => 0.020m,
BankTransfer => 0.005m,
_ => throw new NotSupportedException()
};
Raw String Literals (C# 11)
No escape sequences needed. Indentation is stripped based on the closing """ position.
var json = """
{
"name": "Alice",
"role": "admin"
}
""";
var sql = """
SELECT o.Id, o.Status, c.Email
FROM Orders o
JOIN Customers c ON o.CustomerId = c.Id
WHERE o.Status = 'Pending'
""";
var message = $"""
Order {order.Id} for customer "{order.CustomerName}" is {order.Status}.
""";
Nullable Reference Types
Enable project-wide in .csproj:
<Nullable>enable</Nullable>
public string Name { get; set; }
public string? MiddleName { get; set; }
var name = GetName()!;
var display = user?.Profile?.DisplayName ?? user?.Email ?? "Anonymous";
if (user.MiddleName is { } middle)
Console.WriteLine(middle.ToUpper());
Global Usings (C# 10)
Declare once in a dedicated file (e.g., Usings.cs or GlobalUsings.cs):
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.EntityFrameworkCore;
ASP.NET Web SDK enables some implicitly — check <ImplicitUsings>enable</ImplicitUsings> in .csproj.
File-Scoped Namespaces (C# 10)
namespace MyApp.Services;
public class OrderService { }
public interface IOrderService { }
One namespace per file. Saves one level of indentation throughout the codebase.
field Keyword (C# 13 — preview)
Accesses the compiler-generated backing field inside a property accessor. Eliminates explicit backing field declarations.
public class Product
{
public string Name
{
get => field;
set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
public int Quantity
{
get => field;
set => field = Math.Max(0, value);
}
}
Requires <LangVersion>preview</LangVersion> in .csproj until C# 14 ships.
Do / Don't
| Do | Don't |
|---|
var for all local variables | Explicit types for locals when type is obvious from RHS |
| Records for DTOs and domain events | Mutable classes for data transfer |
| File-scoped namespaces everywhere | Brace-wrapped namespaces |
Switch expressions over if/else chains | #region blocks |
required + init for mandatory DTO fields | Constructor overloads just to set required fields |
| Raw string literals for embedded SQL/JSON | @"..." with manual \" escaping |
| Nullable reference types enabled in new projects | Suppressing NRT warnings with ! broadly |
| Primary constructors for simple DI | Primary constructors when you need mutable private state |
See Also
skills/error-handling/SKILL.md — Result pattern with modern C# records
skills/clean-architecture/SKILL.md — layer conventions these features apply within