C# 12, 13, and 14 Features — What's New and When to Use It
A practical guide to modern C# features: primary constructors, collection expressions, inline arrays, interceptors, params spans, lock object, field keyword, and more — with real-world examples.
C# 12, 13, and 14 Features — What's New and When to Use It
C# has evolved rapidly. This guide covers the practical features added in C# 12 (.NET 8), C# 13 (.NET 9), and C# 14 (.NET 10) that you will use in everyday .NET work.
C# 12 (.NET 8) Features
Primary Constructors on Classes
// Before: boilerplate constructor + field declarations
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly IEventBus _bus;
public OrderService(IOrderRepository repo, IEventBus bus)
{
_repo = repo;
_bus = bus;
}
}
// C# 12: primary constructor — parameters available throughout the class
public class OrderService(IOrderRepository repo, IEventBus bus)
{
public async Task<int> CreateAsync(CreateOrderDto dto, CancellationToken ct)
{
var order = Order.Create(dto);
await repo.AddAsync(order, ct); // repo is captured
await bus.PublishAsync(new OrderCreated(order.Id), ct); // bus is captured
return order.Id;
}
}
// Caution: primary constructor parameters are NOT fields — they are closures
// If you need a field (for mutation, serialisation, or debugging), declare it explicitly:
public class OrderService(IOrderRepository repo)
{
private readonly IOrderRepository _repo = repo; // explicit backing field
}Collection Expressions
// Unified syntax for any collection type
int[] array = [1, 2, 3];
List<int> list = [1, 2, 3];
ImmutableArray<int> immutable = [1, 2, 3];
// Spread operator ..
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] all = [..first, ..second]; // [1, 2, 3, 4, 5, 6]
// Empty collection — cleaner than new List<int>() or Array.Empty<int>()
IReadOnlyList<string> tags = [];
// In method calls
ProcessOrders([1, 2, 3]);Default Lambda Parameters
// Lambda parameters can now have default values
var greet = (string name, string greeting = "Hello") => $"{greeting}, {name}!";
Console.WriteLine(greet("Alice")); // Hello, Alice!
Console.WriteLine(greet("Bob", "Hi")); // Hi, Bob!Alias Any Type
// using alias for any type, not just named types
using OrderId = int;
using OrderPair = (int Id, string Status);
using Processor = System.Func<Order, CancellationToken, System.Threading.Tasks.Task>;
OrderId id = 42;
OrderPair pair = (1, "Pending");Inline Arrays
// Fixed-size buffer on the stack — high performance, no heap allocation
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element;
}
// Use like a regular array
var buffer = new Buffer10<int>();
buffer[0] = 42;C# 13 (.NET 9) Features
params with Span and Collections
// Before C# 13: params only worked with arrays — heap allocation on every call
void LogMessages(params string[] messages) { }
// C# 13: params works with any collection type — Span avoids heap allocation
void LogMessages(params ReadOnlySpan<string> messages) { }
void LogItems(params IEnumerable<string> items) { }
// Call site is unchanged:
LogMessages("Hello", "World"); // no array allocated — stack spanNew Lock Object
// Before: lock(object) — uses Monitor under the hood, no async support
private readonly object _lock = new();
lock (_lock) { /* critical section */ }
// C# 13: System.Threading.Lock — explicit, more expressive
private readonly System.Threading.Lock _lock = new();
lock (_lock) // same syntax
{
// critical section
}
// Also supports scoped usage:
using (_lock.EnterScope())
{
// automatically released at end of scope
}Semi-Auto Properties — field Keyword
// Before: manual backing field to add validation
private string _name = "";
public string Name
{
get => _name;
set => _name = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Name cannot be empty")
: value;
}
// C# 13: field keyword refers to the auto-generated backing field
public string Name
{
get;
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Name cannot be empty")
: value;
}Partial Properties and Indexers
// Partial methods existed since C# 2 — C# 13 extends this to properties
// Useful in source-generated code
public partial class Order
{
public partial int Id { get; set; }
}
// Generated part (e.g., by a source generator)
public partial class Order
{
private int _id;
public partial int Id
{
get => _id;
set => _id = value > 0 ? value : throw new ArgumentOutOfRangeException();
}
}Escape Character \e
// \e is the escape character (ESC, Unicode U+001B)
// Used for ANSI terminal colour codes
Console.Write("\e[32m"); // green
Console.Write("Hello");
Console.Write("\e[0m"); // reset
// Previously needed: or (char)27C# 14 (.NET 10) Features
Implicit Span Conversions
// Before: explicit conversion to Span/ReadOnlySpan
void ProcessBytes(ReadOnlySpan<byte> data) { }
byte[] buffer = GetBuffer();
ProcessBytes(buffer.AsSpan()); // explicit
// C# 14: implicit conversion
ProcessBytes(buffer); // implicit — compiler inserts the conversionExtension Members (Preview in C# 14)
// C# 14 extends the extension method concept to properties and static members
// Traditional extension methods continue to work
// New syntax: extension block on a type
extension(Order order)
{
// Extension property
public bool IsHighValue => order.Total > 1000m;
// Extension static method
public static Order CreateDraft(int customerId) => new() { CustomerId = customerId, Status = "Draft" };
}
// Usage:
var order = new Order { Total = 1500m };
Console.WriteLine(order.IsHighValue); // true (extension property)
var draft = Order.CreateDraft(42); // extension static methodFeatures to Know from C# 10–11 (Still Widely Used)
// File-scoped namespaces (C# 10) — removes one level of indentation
namespace OrderService.Domain; // applies to entire file
// Global usings (C# 10) — add to a single file, applies project-wide
global using System;
global using System.Collections.Generic;
// Required members (C# 11) — must be set at initialisation
public class Order
{
public required int CustomerId { get; init; }
public required string Status { get; init; }
}
var order = new Order { CustomerId = 42, Status = "Pending" }; // required
// new Order() — compile error: CustomerId and Status are required
// Generic math (C# 11) — numeric operations on generic types
public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
=> values.Aggregate(T.Zero, (acc, v) => acc + v);
// Raw string literals (C# 11)
var json = """
{
"name": "Alice",
"age": 30
}
""";Interview Answer
"C# 12 introduced primary constructors on classes (parameters captured as closures throughout the class — useful for DI), collection expressions with unified [ ] syntax and spread operator .., and default lambda parameters. C# 13 added params ReadOnlySpan (avoids heap allocation for variadic calls), System.Threading.Lock as a first-class type with scoped usage, and the field keyword for semi-auto properties (add validation without declaring a backing field). C# 14 is adding implicit Span conversions and extension members — properties and static methods on extension blocks. For everyday .NET 8/9 development: primary constructors reduce constructor boilerplate significantly, collection expressions replace new List , and required members (C# 11) replace constructor parameter validation for DTOs and configuration objects. Global usings (C# 10) clean up repetitive using statements across a project."
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.