CSharpStory_NewThings.html
copyright © James Fawcett
Revised: 05/01/2026
16.0 Prologue
C# 10 (.NET 6, 2021) established a strong baseline with record structs, global
using directives, file-scoped namespaces, and interpolated string handlers.
The three releases that followed — C# 11, 12, and 13 — added features that have a
significant impact on how everyday C# code is written: less boilerplate for constructors
and initialization, a unified syntax for all collection types, and richer pattern
matching. This chapter surveys the most impactful additions.
16.1 C# 11 — Raw String Literals
Raw string literals begin and end with three or more double-quote
characters ("""). The content is taken literally — no escape sequences,
no doubling of backslashes. They are ideal for embedded JSON, XML, SQL, HTML, and
regular expressions:
// Before C# 11 — escape sequences everywhere
string json1 = "{\n \"name\": \"Alice\",\n \"age\": 30\n}";
// C# 11 raw string literal — no escaping at all
string json2 = """
{
"name": "Alice",
"age": 30
}
""";
// Interpolated raw string — $""" ... """
string name = "Alice";
string html = $"""
<p class="greeting">Hello, {name}!</p>
""";
The closing """ controls indentation: the compiler strips leading
whitespace equal to the indentation of the closing delimiter, so the resulting string
is not polluted by source-code indentation. Newlines in string interpolation
expressions (also C# 11) allow multi-line expressions inside the { }
braces of any interpolated string.
16.2 C# 11 — Required Members
The required modifier on a field or property forces callers to supply
a value through an object initializer. The compiler enforces this — missing a required
member is a compile error, without the need for a constructor that takes every property
as a parameter:
public class Address {
public required string Street { get; init; }
public required string City { get; init; }
public string? PostalCode { get; init; } // optional
}
// Compiler error if Street or City is omitted
var addr = new Address { Street = "123 Main St", City = "Springfield" };
// A constructor can opt out of the requirement with [SetsRequiredMembers]
[SetsRequiredMembers]
public Address(string street, string city) {
Street = street; City = city;
}
required pairs naturally with init-only properties and
records, making immutable data transfer objects concise and safe.
16.3 C# 11 — List Patterns
List patterns match arrays and lists by shape and element values
inside switch expressions, switch statements, and
is expressions. The .. slice pattern matches any number of
elements:
int[] nums = { 1, 2, 3, 4, 5 };
string desc = nums switch {
[] => "empty",
[var only] => $"one element: {only}",
[1, 2, ..] => "starts with 1, 2",
[.., > 0] => "last element is positive",
[var first, .., var last] => $"first={first}, last={last}",
_ => "other"
};
// In an if statement
if (nums is [_, _, 3, ..])
Console.WriteLine("Third element is 3");
List patterns complement the property and deconstruct patterns from earlier C# versions,
completing the ability to match on both the structure and the contents of any sequence.
16.4 C# 11 — Generic Math
Static abstract members in interfaces (C# 11) allow interfaces to
declare static methods and operators. The System.Numerics library uses this
to expose generic math interfaces such as INumber<T>,
IAdditionOperators<T,T,T>, and IComparisonOperators<T,T,bool>.
You can write numeric algorithms that work over any numeric type:
using System.Numerics;
// T.Zero and + operator resolved at compile time for each concrete type
static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
=> values.Aggregate(T.Zero, (acc, x) => acc + x);
Console.WriteLine(Sum(new[] { 1, 2, 3, 4 })); // 10 (int)
Console.WriteLine(Sum(new[] { 1.5, 2.5, 3.0 })); // 7.0 (double)
Console.WriteLine(Sum(new[] { 1m, 2.5m, 0.75m })); // 4.25 (decimal)
// Interface-level operator overloads on your own type
public readonly struct Celsius(double value) : IAdditionOperators<Celsius, Celsius, Celsius> {
public double Value => value;
public static Celsius operator +(Celsius a, Celsius b) => new(a.Value + b.Value);
}
16.5 C# 11 — UTF-8 String Literals
Appending u8 to a string literal produces a
ReadOnlySpan<byte> containing the UTF-8 encoded bytes with no
heap allocation. This is useful in network, serialization, and I/O code that works
directly with byte buffers:
// Type is ReadOnlySpan<byte> — no allocation
ReadOnlySpan<byte> crlf = "\r\n"u8;
// Writing HTTP response headers directly to a stream
static async Task WriteResponseAsync(Stream s) {
await s.WriteAsync("HTTP/1.1 200 OK\r\n"u8.ToArray());
await s.WriteAsync("Content-Type: application/json\r\n"u8.ToArray());
await s.WriteAsync("\r\n"u8.ToArray());
}
// Efficient constant-time comparisons in parsers
static bool IsGet(ReadOnlySpan<byte> method) => method.SequenceEqual("GET"u8);
UTF-8 literals are evaluated entirely at compile time and stored in the read-only
data section of the assembly, so there is zero runtime cost to obtaining the span.
16.6 C# 12 — Primary Constructors
Primary constructors place parameters directly on the class or struct
declaration. The parameters are in scope throughout the entire type body — in field
initializers, properties, and methods — eliminating the backing-field + constructor +
assignment boilerplate that dominated dependency-injection code:
// Before C# 12 — explicit fields and constructor body
public class OrderService {
private readonly IOrderRepo _repo;
private readonly ILogger<OrderService> _logger;
public OrderService(IOrderRepo repo, ILogger<OrderService> logger) {
_repo = repo; _logger = logger;
}
public Task<Order> GetAsync(int id) => _repo.FindAsync(id);
}
// C# 12 primary constructor — same behaviour, far less code
public class OrderService(IOrderRepo repo, ILogger<OrderService> logger) {
public async Task<Order> GetAsync(int id) {
logger.LogInformation("Fetching order {Id}", id);
return await repo.FindAsync(id);
}
}
// Also works on structs and records
public readonly struct Vector2(double x, double y) {
public double X { get; } = x;
public double Y { get; } = y;
public double Length => Math.Sqrt(X * X + Y * Y);
}
If a primary constructor parameter needs to be captured as a field (for mutation or
to prevent capture of a mutable local), declare an explicit field initialized from the
parameter: private readonly IOrderRepo _repo = repo;
16.7 C# 12 — Collection Expressions
Collection expressions introduce a single [ ] syntax
that works uniformly for arrays, List<T>, Span<T>,
immutable collections, and any type that opts in via
[CollectionBuilder]. This replaces four different initialization syntaxes
that existed before:
// All four use identical syntax
int[] arr = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];
ImmutableArray<int> imm = [1, 2, 3];
// Spread operator .. inlines another collection inline
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];
int[] c = [..a, ..b, 7]; // [1, 2, 3, 4, 5, 6, 7]
// Works directly in method calls
static void Print(int[] vals) => Console.WriteLine(string.Join(", ", vals));
Print([10, 20, 30]);
The compiler selects the most efficient construction path for each target type.
For Span<T> it can stack-allocate the buffer; for
ImmutableArray<T> it calls the appropriate factory.
16.8 C# 12 — Additional Features
Two smaller additions round out C# 12:
Default lambda parameters — lambdas can now have optional parameters
with defaults, just like regular methods:
var greet = (string name, string prefix = "Hello") => $"{prefix}, {name}!";
Console.WriteLine(greet("Alice")); // Hello, Alice!
Console.WriteLine(greet("Bob", "Hi")); // Hi, Bob!
// Useful when passing a lambda to an API that supplies the argument optionally
Func<int, int, int> add = (x, y = 0) => x + y;
Alias any type — using aliases can now name tuples,
arrays, nullable types, and pointer types, not only named types:
using Point = (double X, double Y);
using Matrix = double[,];
using MaybeId = int?;
Point origin = (0.0, 0.0);
Point offset = (3.5, -1.2);
MaybeId id = null;
16.9 C# 13 — params Collections
Before C# 13 the params modifier only accepted arrays. C# 13 lifts that
restriction: params works with any collection type, including
ReadOnlySpan<T> — which avoids the heap allocation that a
params T[] parameter forces at every call site:
// Before C# 13 — array allocated on each call
void LogAll(params string[] messages) { ... }
// C# 13 — no allocation when called with a fixed argument list
void LogAll(params ReadOnlySpan<string> messages) { ... }
// Works with any collection interface too
void AddRange<T>(List<T> target, params IEnumerable<T> items)
=> target.AddRange(items);
// Call sites look identical
LogAll("starting", "loading", "done");
AddRange(myList, 10, 20, 30);
When the call site passes a fixed-length argument list to a
params ReadOnlySpan<T> parameter, the compiler can stack-allocate
the buffer, making the call as efficient as a direct span creation.
16.10 C# 13 — field Keyword (Semi-auto Properties)
The contextual keyword field refers to the compiler-generated backing
field of a property. This makes it possible to add validation or transformation logic
to one accessor while keeping the other auto-generated — without declaring an explicit
backing field:
// Before C# 13 — explicit backing field required just to add one guard
private int _count;
public int Count {
get => _count;
set { ArgumentOutOfRangeException.ThrowIfNegative(value); _count = value; }
}
// C# 13 — field refers to the hidden backing field
public int Count {
get;
set { ArgumentOutOfRangeException.ThrowIfNegative(value); field = value; }
}
// Computed get with lazily stored value
public string Label {
get => field ?? "(none)";
set;
}
// Trimming on set, auto get
public string Name {
get;
set => field = value.Trim();
}
16.11 C# 13 — Additional Features
System.Threading.Lock — a new dedicated lock type that replaces
object as the monitor target. The lock statement recognises
Lock and calls Lock.EnterScope() instead of
Monitor.Enter, eliminating boxing and providing a faster,
allocation-free scope:
// Before C# 13 — object used as monitor; boxes on every lock
private readonly object _sync = new();
void SafeUpdate() { lock (_sync) { /* ... */ } }
// C# 13 — dedicated Lock type, no boxing, faster scope enter/exit
private readonly Lock _lock = new();
void SafeUpdate() { lock (_lock) { /* ... */ } }
// Explicit scope for try/finally patterns
using (_lock.EnterScope()) {
/* guaranteed exit even on exception */
}
Partial properties extend partial classes so that a property can be
declared in one file and its accessor bodies implemented in another. This completes
source-generator support for properties — generators previously had to work around the
limitation that only methods could be partial:
// ViewModel.cs — declaration (written by developer)
public partial class PersonViewModel {
public partial string Name { get; set; }
}
// ViewModel.g.cs — implementation (generated by source generator)
public partial class PersonViewModel {
private string _name = "";
public partial string Name {
get => _name;
set { _name = value; OnPropertyChanged(); }
}
}
16.12 Epilogue
This chapter surveyed the most impactful additions in C# 11, 12, and 13: raw string
literals and UTF-8 literals that simplify text and binary work; required members and
list patterns that improve initialization safety and expressive matching; generic math
that generalises numeric algorithms; primary constructors and collection expressions
that cut boilerplate; and params collections, the field keyword, the dedicated Lock
type, and partial properties that sharpen performance and tooling in C# 13. The
language continues to evolve with each annual .NET release.
16.13 References
What's new in C# 11 — Microsoft docs
What's new in C# 12 — Microsoft docs
What's new in C# 13 — Microsoft docs
Collection expressions — language reference
required modifier — language reference