Skip to content

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, using aliases for any type.
  • C# 13 (.NET 9): params Span<T>/IEnumerable<T> (not just arrays), [OverloadResolutionPriority], partial properties, allows ref struct constraint, \e escape, field keyword preview.
  • C# 14 (.NET 10): field keyword stable, extension members (extension properties + static extensions), partial constructors, lambda parameter modifiers (ref/out/in), first-class Span<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 record primary 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)

Console.Write("\e[31mred\e[0m");   // ANSI 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)

Func<int, int, int> add = (int a, int b = 1) => a + b;
add(5);    // 6
add(5, 2); // 7

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], just new int[] {1,2,3}. For Span<int> s = [1,2,3], an inline array + Span constructor — 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.
  • field keyword introduces a compiler-synthesized field. The compiler emits standard get/set IL referencing it.
  • allows ref struct is 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]

var s = [1, 2, 3];   // ❌ ambiguous — int[]? List<int>? Span<int>?

✅ Correct: explicit target type

int[] s = [1, 2, 3];
List<int> l = [1, 2, 3];
Span<int> sp = [1, 2, 3];

❌ 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

public class Logger(ILogger log)
{
    public ILogger Log => log;   // explicit
}

❌ Wrong: using new C# version syntax in older project

<TargetFramework>net8.0</TargetFramework>
public string Name
{
    get => field;   // ❌ field keyword needs C# 13+ / .NET 9+ language version
}

✅ Correct: set language version

<LangVersion>latest</LangVersion>

(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 field keyword 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, Span, etc.).

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

  • ⚠️ var with collection expressions — won't compile.
  • ⚠️ Primary ctor parameters on class as if properties — they're not.
  • ⚠️ field keyword 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 using order.

Further reading