Learnixo

.NET & C# Development · Lesson 164 of 229

C# 12, 13, and 14 Features — What's New and When to Use It

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

C#
// 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

C#
// 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

C#
// 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

C#
// 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

C#
// 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

C#
// 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 span

New Lock Object

C#
// 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

C#
// 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

C#
// 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

C#
// \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)27

C# 14 (.NET 10) Features

Implicit Span Conversions

C#
// 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 conversion

Extension Members (Preview in C# 14)

C#
// 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 method

Features to Know from C# 10–11 (Still Widely Used)

C#
// 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."