Skip to main content

C# 13 New Features: Complete Guide for Developers (2026)

Learn every C# 13 new feature with runnable examples: params collections, Lock type, escape sequences, partial properties and more. Master C# 13 today.

C# 13 shipped alongside .NET 9 and, while it's not the biggest release in the language's history, it fixes several long-standing pain points that developers hit every single day. If you've been searching for C# 13 new features and want more than a bullet list, this guide walks through every feature with runnable code, explains why each change was made, and calls out the pitfalls that catch teams migrating from C# 12.

Whether you're a beginner wondering what params Span<T> means, an intermediate developer wanting best practices for the new Lock type, or a senior engineer evaluating an upgrade to .NET 9 or .NET 10, you'll find what you need here.

C# 13 New Features at a Glance

  • params collectionsparams now works with Span<T>, IEnumerable<T>, List<T> and more, not just arrays
  • System.Threading.Lock – a dedicated lock type that's faster and safer than lock(object)
  • New escape sequence \e – for the ESC character (ANSI terminal colours)
  • Implicit index access (^) in object initializers
  • ref and unsafe in iterators and async methods
  • ref struct types can implement interfaces and use allows ref struct generic constraints
  • Partial properties and partial indexers – huge for source generators
  • Overload resolution priority attribute for library authors
  • Improved method group natural type (a quiet performance win)
  • Preview: the field keyword for semi-auto properties

To use these features, target net9.0 or later in your project file, or set <LangVersion>13</LangVersion> explicitly. Let's go through them one by one.

1. params Collections – The Headline C# 13 Feature

Since C# 1.0, params only worked with arrays. That meant every call to a params method allocated a new array on the heap — even for two or three arguments. In performance-sensitive code (logging, string formatting, hot loops) that's a real cost. C# 13 lets you use params with spans, interfaces and concrete collection types.

using System;
using System.Collections.Generic;

public static class Printer
{
    // C# 12 and earlier: always allocates a string[]
    public static void PrintArray(params string[] items)
    {
        foreach (var item in items) Console.WriteLine(item);
    }

    // C# 13: no heap allocation for the arguments
    public static void PrintSpan(params ReadOnlySpan<string> items)
    {
        foreach (var item in items) Console.WriteLine(item);
    }

    // C# 13: also works with interfaces and concrete collections
    public static int Sum(params IEnumerable<int> numbers)
    {
        int total = 0;
        foreach (var n in numbers) total += n;
        return total;
    }
}

class Program
{
    static void Main()
    {
        Printer.PrintSpan("alpha", "beta", "gamma");   // stack-allocated span
        Console.WriteLine(Printer.Sum(1, 2, 3, 4, 5)); // 15

        var list = new List<int> { 10, 20 };
        Console.WriteLine(Printer.Sum(list));          // 30 – existing collections still work
    }
}

Why it matters

The compiler can now emit an inline array (a stack-allocated buffer) for params ReadOnlySpan<T>, eliminating the allocation entirely. Microsoft has already updated overloads such as string.Join, string.Concat and Path.Combine in .NET 9 to take advantage of this, so simply recompiling can reduce garbage collection pressure.

Best practices and pitfalls

  • Prefer params ReadOnlySpan<T> for hot paths where you only read the arguments.
  • When both an array and a span overload exist, C# 13 prefers the span overload. This is normally what you want, but be aware that adding a span overload to a public library changes which method callers bind to after recompilation.
  • Don't capture a params Span<T> in a lambda or store it in a field — spans cannot escape the stack, and the compiler will stop you.

2. The New System.Threading.Lock Type

For two decades, C# developers have written private readonly object _sync = new(); and then lock (_sync). It works, but locking on a plain object relies on the runtime's monitor table and offers no compile-time protection against locking the wrong thing. C# 13 recognises the new System.Threading.Lock type and generates more efficient code for it.

using System.Threading;

public class BankAccount
{
    private readonly Lock _lock = new();   // new in .NET 9
    private decimal _balance;

    public void Deposit(decimal amount)
    {
        lock (_lock)   // compiler emits Lock.EnterScope() instead of Monitor.Enter
        {
            _balance += amount;
        }
    }

    public decimal Withdraw(decimal amount)
    {
        // The explicit form – useful when you want the scope in a using statement
        using (_lock.EnterScope())
        {
            if (amount > _balance) throw new InvalidOperationException("Insufficient funds");
            _balance -= amount;
            return _balance;
        }
    }
}

Why it matters

The Lock type is a purpose-built mutex with lower overhead than Monitor, and because it's a distinct type you get clear intent in code reviews. The EnterScope() method returns a ref struct that releases the lock when disposed, so the pattern is exception-safe.

Pitfalls

  • If you accidentally cast a Lock to object and lock on it, the compiler emits a warning (CS9216) because you'd fall back to the slower Monitor path — and worse, the two mechanisms don't synchronise with each other.
  • Lock is not a drop-in replacement for Monitor.Wait/Pulse. If you use condition-variable semantics, keep the classic object lock or move to SemaphoreSlim.
  • It's still a synchronous lock. For async code, continue using SemaphoreSlim.WaitAsync.

3. The \e Escape Sequence

A small but welcome addition: \e represents the ESC character (Unicode U+001B). Previously you had to write \u001b or \x1b, and \x is notoriously error-prone because it greedily consumes following hex digits.

// Before C# 13 – "\x1b[31m" is ambiguous if followed by hex-like characters
Console.WriteLine("\u001b[32mSuccess!\u001b[0m");

// C# 13 – clean and readable ANSI colour output
Console.WriteLine("\e[32mSuccess!\e[0m");
Console.WriteLine("\e[1;31mError:\e[0m something went wrong");

If you build CLI tools, this is a genuine quality-of-life improvement for terminal colours and cursor control.

4. Implicit Index Access in Object Initializers

The "from the end" index operator ^ can now be used inside object initializers. This closes an odd gap where ^1 worked everywhere except initializers.

public class Countdown
{
    public int[] Values { get; set; } = new int[5];
}

var countdown = new Countdown
{
    Values =
    {
        [^1] = 1,   // last element
        [^2] = 2,
        [^3] = 3,
        [^4] = 4,
        [^5] = 5
    }
};

Console.WriteLine(string.Join(", ", countdown.Values)); // 5, 4, 3, 2, 1

5. ref Locals and unsafe Code in Iterators and Async Methods

Before C# 13, you could not declare a ref local or use a ref struct like Span<T> anywhere inside an async method or an iterator (a method using yield). That forced developers to split methods awkwardly. C# 13 allows them, with one rule: the ref or ref struct value must not be alive across an await or yield return.

using System;
using System.Threading.Tasks;

public static class Parser
{
    public static async Task<int> CountDigitsAsync(string input)
    {
        await Task.Delay(10);   // simulate I/O

        // Allowed in C# 13: a Span in an async method
        ReadOnlySpan<char> chars = input.AsSpan();
        int count = 0;
        foreach (var c in chars)
        {
            if (char.IsDigit(c)) count++;
        }
        // 'chars' is not used after this point, so no await crosses it

        await Task.Delay(10);
        return count;
    }
}

Why it matters

Async methods are compiled into state machines whose locals become heap fields; a ref struct can't live on the heap. The C# 13 compiler performs flow analysis so you can use spans in the synchronous portions of an async method safely. The practical result: less boilerplate and fewer helper methods purely to work around the old restriction.

6. ref struct Interfaces and the allows ref struct Constraint

ref struct types (like Span<T>) can now implement interfaces, and generic type parameters can opt in to accepting them with the allows ref struct anti-constraint.

public interface IWriter
{
    void Write(ReadOnlySpan<char> text);
}

// A ref struct implementing an interface – new in C# 13
public ref struct BufferWriter : IWriter
{
    private Span<char> _buffer;
    private int _position;

    public BufferWriter(Span<char> buffer) { _buffer = buffer; _position = 0; }

    public void Write(ReadOnlySpan<char> text)
    {
        text.CopyTo(_buffer.Slice(_position));
        _position += text.Length;
    }

    public override string ToString() => _buffer.Slice(0, _position).ToString();
}

public static class Logger
{
    // 'allows ref struct' lets T be a ref struct; T is used by value, never boxed
    public static void Log<T>(ref T writer, string message) where T : IWriter, allows ref struct
    {
        writer.Write(message);
    }
}

// Usage
Span<char> stack = stackalloc char[64];
var writer = new BufferWriter(stack);
Logger.Log(ref writer, "Hello, C# 13!");
Console.WriteLine(writer.ToString());

Pitfall

You still cannot box a ref struct. Calling an interface method through the interface type (IWriter w = writer;) is a compile error. The interface is only usable through generics with the allows ref struct constraint. This is by design — it preserves the zero-allocation guarantee.

7. Partial Properties and Partial Indexers

C# 9 introduced partial methods for source generators. C# 13 extends the idea to properties and indexers, which lets generators such as the MVVM Community Toolkit produce far cleaner code.

// Your file – declare the property, no body
public partial class ViewModel
{
    public partial string Name { get; set; }
}

// Generated file (or another partial file) – the implementation
public partial class ViewModel
{
    private string _name = string.Empty;

    public partial string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged(nameof(Name));
            }
        }
    }

    private void OnPropertyChanged(string propertyName) =>
        Console.WriteLine($"{propertyName} changed");
}

Previously, generators had to invent a backing field and a differently-named property, which broke IntelliSense discoverability and made the code harder to read. With partial properties, you write the shape and the generator provides the behaviour.

8. OverloadResolutionPriority Attribute

Library authors sometimes add a better overload (say, a span-based version) but can't make it the preferred choice without breaking existing callers. The [OverloadResolutionPriority] attribute lets you nudge the compiler.

using System.Runtime.CompilerServices;

public static class Text
{
    [OverloadResolutionPriority(1)]   // higher number wins when otherwise ambiguous
    public static string Join(params ReadOnlySpan<string> parts) => string.Join(" ", parts);

    public static string Join(params string[] parts) => string.Join(" ", parts);
}

Use this sparingly. It's a tool for API evolution, not something to sprinkle across application code — it makes overload behaviour harder to reason about for anyone reading the code later.

9. Method Group Natural Type Improvements

This one is invisible in source but real at runtime. When you pass a method group (e.g. list.Select(Transform)), the compiler now prunes inapplicable candidate methods earlier, which produces more accurate natural types and, in many cases, lets the compiler cache the delegate instead of allocating a new one on each call. No code changes needed — just recompile.

10. Preview Feature: The field Keyword

Available in C# 13 behind <LangVersion>preview</LangVersion> (and fully released in C# 14), the field keyword gives you access to the compiler-generated backing field of an auto-property. This eliminates the boilerplate of declaring a private field just to add validation.

public class Product
{
    // No explicit backing field needed
    public decimal Price
    {
        get;
        set => field = value < 0
            ? throw new ArgumentOutOfRangeException(nameof(value), "Price cannot be negative")
            : value;
    }
}

Pitfall

If your class already has a member named field, the keyword takes precedence inside property accessors. Rename the member or reference it as @field / this.field. Since this is a preview feature in C# 13, avoid it in production libraries until you're on C# 14.

Upgrading to C# 13: A Practical Checklist

  • Target framework: Set <TargetFramework>net9.0</TargetFramework> (or net10.0). C# 13 features like Lock and allows ref struct depend on runtime support, so simply setting LangVersion on .NET 8 won't give you everything.
  • Recompile and measure: params spans and method-group caching can reduce allocations with zero code changes. Use BenchmarkDotNet to confirm gains before and after.
  • Audit public APIs: If you maintain a NuGet package, adding span-based params overloads changes binding for recompiled callers. Document it in release notes.
  • Migrate locks gradually: Replace object locks with Lock in new code and hot paths first. Don't mix both mechanisms on the same critical section.
  • Enable analyzers: The .NET 9 SDK ships analyzers that flag the CS9216 lock-conversion warning and other C# 13 hazards. Treat them as errors in CI.

Conclusion: Key Takeaways on C# 13 New Features

The C# 13 new features are less about flashy syntax and more about removing friction and allocations from everyday code. Here's what to remember:

  • params collections are the most impactful change — reach for params ReadOnlySpan<T> in hot paths.
  • System.Threading.Lock is the new default for synchronous locking; it's faster and communicates intent.
  • ref locals in async and iterator methods remove a long-standing annoyance, as long as nothing crosses an await or yield.
  • ref struct interfaces and allows ref struct unlock zero-allocation generic APIs.
  • Partial properties make source generators dramatically cleaner — expect your MVVM and serialization tooling to take advantage of them.
  • Small wins like \e and ^ in initializers add up to more readable code.

If you're still on C# 12, upgrading to .NET 9 and C# 13 is low-risk and delivers measurable performance gains just from recompiling. Start with the checklist above, run your benchmarks, and adopt the new Lock and params patterns in your most performance-sensitive code first. Once you're comfortable, C# 14 and .NET 10 build directly on these foundations with the field keyword, extension members and more.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...