C# Story

Chapter #16 – New Things

significant additions in C# 11, 12, and 13

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 typeusing 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