C# 13 / 14 Features
Key Points
- C# 12 (recap, .NET 8): primary constructors on
class/struct, collection expressions[1, 2, 3],[InlineArray], default lambda parameters,usingaliases for any type. - C# 13 (.NET 9):
params Span<T>/IEnumerable<T>(not just arrays),[OverloadResolutionPriority], partial properties,allows ref structconstraint,\eescape,fieldkeyword preview. - C# 14 (.NET 10):
fieldkeyword stable, extension members (extension properties + static extensions), partial constructors, lambda parameter modifiers (ref/out/in), first-classSpan<T>conversions. - Most of these are quality-of-life — small individual wins, big cumulative effect on idiomatic C#.
Concepts (deep dive)
Collection expressions (C# 12)
int[] arr = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3]; // can stack-alloc!
ImmutableArray<int> imm = [1, 2, 3];
int[] flat = [1, 2, ..arr, 99]; // spread
The compiler picks the right construction (new[] {...}, new List<T>([...]), [CollectionBuilder]-attributed factory). Span literals stack-allocate when not escaping the method.
💡 Senior insight: for hot paths over a known small
Span<T>,Span<int> tmp = [1, 2, 3]may stack-allocate — zero allocation. Verify with[MemoryDiagnoser].
Primary constructors on class/struct (C# 12)
public class Logger(ILogger<Logger> log, IClock clock)
{
public void Track(string msg) => log.LogInformation("{T} {Msg}", clock.UtcNow, msg);
}
Parameters are captured but not auto-promoted to properties. Use them inside the class body. The compiler creates a synthetic field per parameter referenced from a non-constructor method.
⚠️ Different from
recordprimary constructors, where parameters become public init-only properties. Same syntax, different semantics by type kind.
params collections (C# 13)
public static int Sum(params Span<int> values) // ❗ Span — stackalloc-friendly!
{
int s = 0;
foreach (var v in values) s += v;
return s;
}
Sum(1, 2, 3); // compiler may pass a stack-allocated Span<int>
C# 13 generalized params. Used to be only T[]; now any Span<T>, ReadOnlySpan<T>, or any [CollectionBuilder]-supporting type. Span<T> params enables fully zero-allocation variadic APIs.
[OverloadResolutionPriority] (C# 13)
public class Logger
{
[OverloadResolutionPriority(1)]
public void Log(ReadOnlySpan<char> message) { /* preferred */ }
public void Log(string message) { /* fallback */ }
}
When both overloads match (e.g., a string literal converts to both), the higher-priority one wins. Used by BCL to prefer Span<T> overloads over array overloads.
Partial properties (C# 13) and field keyword (C# 14)
// Partial properties — useful with source generators
public partial class Vm
{
public partial string Name { get; set; } // declaration
}
public partial class Vm
{
public partial string Name { get; set => field = value.Trim(); } // implementation
}
// `field` keyword in C# 14 — refers to compiler-synthesized backing field
public class Person
{
public string Name
{
get => field;
set => field = value?.Trim() ?? ""; // no need to declare _name yourself
}
}
The field keyword removed boilerplate around "I just want a property with light validation but I have to write private string _name; ... return _name;".
allows ref struct (C# 13)
Already covered in Span, Memory & ref structs. Lets generic methods accept ref struct types.
\e escape (C# 13)
\e is the escape character (0x1B) — handy for terminal coloring without (char)0x1B.
Extension members (C# 14)
C# 14 (shipped with .NET 10, November 2025) introduced richer extension syntax — extension properties and static extensions:
// C# 14 syntax:
extension(string s)
{
// extension property
public bool IsEmail => s.Contains('@');
// extension method (existing)
public string Reverse() => new string(s.Reverse().ToArray());
// static extension on string itself
public static string FromBytes(byte[] b) => Encoding.UTF8.GetString(b);
}
string a = "x@y";
bool ok = a.IsEmail; // extension property
string built = string.FromBytes([0x68, 0x69]); // static extension
This is a major upgrade — the C# 3 static class { static T Method(this Type x, …) } extension method pattern was widely used but felt awkward. C# 14's extension(...) block clusters all extensions to a type in one place.
Partial constructors (C# 14)
public partial class Vm
{
public partial Vm(string name); // declaration
}
public partial class Vm
{
public partial Vm(string name) { Name = name.Trim(); } // implementation
}
Source-gen-friendly: a generator can declare the constructor signature; the user-written partial provides the body.
Lambda parameter modifiers (C# 14)
// C# 14: ref/out/in on lambda parameters
ProcessRef((ref int x) => x = 42);
TryGet((out int v) => { v = 1; return true; });
Previously Func/Action couldn't hold ref/out/in parameter shapes; you had to write a custom delegate. C# 14 lifted the restriction.
Default lambda parameters (C# 12)
Lambdas can now have default values — closer parity with named methods.
using aliases for any type (C# 12)
using OrderId = (int Tenant, int Local); // alias a tuple
using Entry = System.Collections.Generic.KeyValuePair<string, object?>;
OrderId id = (1, 42);
Entry e = new("k", "v");
Alias any type, not just generic types. Cleans up signatures with complex types.
[InlineArray] (C# 12)
[InlineArray(8)]
public struct EightInts
{
private int _e;
}
EightInts arr = new();
arr[0] = 1;
Span<int> span = arr; // implicit conversion
Fixed-size array as a value type — usable from Span<T> directly. Useful for SIMD-style or interop scenarios where you want fixed inline storage.
Compatibility matrix
| Feature | First in | Stable in |
|---|---|---|
| Collection expressions | C# 12 (.NET 8) | C# 12 |
| Primary ctors on class/struct | C# 12 | C# 12 |
[InlineArray] | C# 12 | C# 12 |
using aliases for any type | C# 12 | C# 12 |
| Default lambda params | C# 12 | C# 12 |
params Span<T> / IEnumerable<T> | C# 13 (.NET 9) | C# 13 |
[OverloadResolutionPriority] | C# 13 | C# 13 |
| Partial properties | C# 13 | C# 13 |
allows ref struct | C# 13 | C# 13 |
\e escape | C# 13 | C# 13 |
field keyword | C# 13 (preview) | C# 14 (.NET 10) |
| Extension members | C# 14 | C# 14 |
| Partial constructors | C# 14 | C# 14 |
Lambda ref/out/in params | C# 14 | C# 14 |
First-class Span<T> | C# 14 | C# 14 |
How it works under the hood
Most of these are pure compiler / Roslyn features — no runtime changes. The compiler lowers them to plain IL that's been valid since .NET Framework.
- Collection expressions lower to the most efficient construction available. For
int[] arr = [1,2,3], justnew int[] {1,2,3}. ForSpan<int> s = [1,2,3], an inline array +Spanconstructor — sometimes stack-allocated. - Primary constructors on class create a synthetic constructor and capture parameters as private fields if and only if they're referenced outside the constructor.
fieldkeyword introduces a compiler-synthesized field. The compiler emits standardget/setIL referencing it.allows ref structis a metadata flag on the type parameter; runtime allows the substitution.- Extension members lower to compiler-generated static classes with
[Extension]-style mechanisms; consumers see them as natural members. Backwards-compatible with C# 3 extension methods.
Code: correct vs wrong
❌ Wrong: var s = [1, 2, 3]
✅ Correct: explicit target type
❌ Wrong: assuming primary ctor parameter is a public property on class
public class Logger(ILogger log) { }
var l = new Logger(myLog);
var x = l.log; // ❌ doesn't exist; primary ctor parameter isn't auto-promoted
✅ Correct: declare property explicitly
❌ Wrong: using new C# version syntax in older project
✅ Correct: set language version
(The compiler that ships with each SDK bundles the new language version. Pin via <LangVersion> if you target older runtimes.)
❌ Wrong: extension members in old syntax
public static class StringExt
{
public static bool IsEmail(this string s) => s.Contains('@');
public static string Format(this string s, params object[] args) => string.Format(s, args);
}
(Still works, but verbose.)
✅ Correct: C# 14 extension block
extension(string s)
{
public bool IsEmail => s.Contains('@');
public string Format(params object[] args) => string.Format(s, args);
}
Design patterns for this topic
Pattern 1 — "Collection expressions everywhere"
- Intent: consistent
[...]syntax for construction. - When to use it: any collection literal in C# 12+.
Pattern 2 — "Primary constructor for DI shapes"
- Intent: terse class declarations.
- Code sketch:
public class OrderService(IRepository repo, IClock clock, ILogger<OrderService> log)
{
public Task ProcessAsync(int id) { /* uses repo, clock, log */ }
}
Pattern 3 — "params Span<T> for variadic alloc-free APIs"
- Intent: zero-allocation vararg.
- Code sketch:
public static int Sum(params ReadOnlySpan<int> nums)
{
int s = 0;
foreach (var n in nums) s += n;
return s;
}
Pattern 4 — "field keyword for property light-validation"
- Intent: terse property setters with side effects.
Pattern 5 — "Extension blocks for type augmentation"
- Intent: group all extensions to a type in one place.
Pros & cons / trade-offs
| Feature | Pros | Cons |
|---|---|---|
| Collection expressions | Concise; sometimes alloc-free | var ambiguity |
| Primary ctors on class | Less boilerplate | Different from record semantics; confusing |
params Span<T> | Alloc-free vararg | Older callers don't see it |
field keyword | Cleaner properties | Newer; need C# 13+ |
| Extension members | One place per type | C# 14+; tooling adoption |
[OverloadResolutionPriority] | Tie-break overloads | Use sparingly |
When to use / when to avoid
- Use collection expressions in all new code where the target type is clear.
- Use primary constructors on class/struct for DI-style shapes.
- Use
fieldkeyword for properties with light validation. - Use extension members (C# 14) for new utility extensions.
- Avoid sprinkling
[OverloadResolutionPriority]— only use to disambiguate when the compiler would pick poorly.
Interview Q&A
Q1. What's a collection expression? A C# 12+ literal [1, 2, 3] whose type is inferred from context. Compiler picks the best construction (array, List
Q2. How are primary constructors on class different from on record? On record, parameters become public init-only properties. On class (and struct), parameters are captured but not auto-promoted; they're accessible inside the class body only.
Q3. What's params Span<T>? C# 13 generalized params. The compiler can pass a stack-allocated Span<T> instead of a heap array — alloc-free vararg.
Q4. What does the field keyword do? Refers to the compiler-synthesized backing field of a property — without forcing you to declare private string _name;. Cleaner setters with side effects.
Q5. What's [OverloadResolutionPriority]? Lets you bias overload resolution among multiple matching overloads. Used by the BCL to prefer Span<T> versions over T[] versions when both apply.
Q6. What's allows ref struct? A C# 13 generic constraint allowing ref struct types as type arguments. Unlocks generic algorithms over Span<T>.
Q7. How do extension members differ from extension methods? Extension methods (C# 3+) are static methods with this parameter. Extension members (C# 14) are a unified syntax extension(T x) { ... } block supporting properties, static extensions, etc.
Q8. What's a partial property? A property declared in one partial class/struct definition and implemented in another. Useful with source generators that emit the implementation.
Q9. What's a [InlineArray]? A C# 12 attribute on a struct that turns it into a fixed-size inline array, accessible via Span<T>. Useful for fixed-capacity buffers.
Q10. Can you use collection expressions in custom types? Yes — implement [CollectionBuilder] on the type pointing at a static factory method. The compiler will use it for collection-expression construction.
Q11. Why is var s = [1,2,3] an error? var requires the compiler to infer the type, but [1,2,3] has no inherent type — could be int[], List<int>, Span<int>, etc. Provide a target type.
Q12. When would you reach for [OverloadResolutionPriority]? When you have multiple overloads that all match for some inputs, and you want the compiler to deterministically pick one without ambiguity errors. Common in BCL evolution to add Span<T> overloads alongside legacy ones.
Gotchas / common mistakes
- ⚠️
varwith collection expressions — won't compile. - ⚠️ Primary ctor parameters on class as if properties — they're not.
- ⚠️
fieldkeyword in older language version — compile error. - ⚠️ Mixing C# 14 and older toolchains — newer projects must compile under newer SDKs.
- ⚠️ Extension member name conflicts — same name on multiple types from different namespaces; resolution depends on
usingorder.