What’s New in C# 13 – Explore the Latest Features

C# continues to evolve with every release, bringing developers new language improvements to write cleaner, safer, and more expressive code. With C# 13 (part of .NET 9), we get a mix of features that improve developer productivity, add flexibility, and enhance existing capabilities.

In this article, we’ll explore all the new features in C# 13, explain them in detail, and walk through code examples to understand how they work.

Overview of C# 13 Features

Here’s a quick list of what’s new in C# 13:

  • params Collections
  • New Lock Object
  • Partial Properties and Indexers
  • New Escape Sequence
  • Implicit Index Access
  • ref and unsafe in Iterators and async Methods
  • allows ref struct in Generics
  • ref struct Interfaces

Now let’s dive into each feature one by one.

params Collections

In previous versions of C#, params allowed passing a variable number of arguments, but only as arrays. With C# 13, params now supports collections like List<T> and Span<T>.

This means we can write APIs that are more efficient and flexible:

C#
DisplayItems(1, 2, 3, 4, 5);
DisplayItems("Apple", "Banana", "Orange");

void DisplayItems<T>(params List<T> items)
{
    foreach (var item in items)
    {
        Console.WriteLine(item);
    }
}

No need to manually create a list, the compiler handles it for us. This makes method signatures more natural and expressive.

New Lock Object

The .NET 9 runtime introduces a new synchronization type called System.Threading.Lock. This type provides a more modern and efficient way to handle thread synchronization compared to the traditional Monitor-based approach.

When you call Lock.EnterScope(), it creates an exclusive scope, and the returned ref struct follows the Dispose pattern, meaning the lock is automatically released at the end of the scope.

In C# 13, the lock statement is smart enough to detect when you’re working with a Lock object. In that case, it uses the new System.Threading.Lock APIs behind the scenes instead of falling back to Monitor. If you convert the Lock to another type, then the compiler switches back to the old Monitor implementation.

Before C# 13:
C#
public class MyClass
{
    private object _lockObj = new();

    public void DoWork()
    {
        lock (_lockObj)
        {
            // Critical section
        }
    }
}
With C# 13:
C#
using System.Threading;

public class MyClass
{
    // Just change the type from object to Lock
    private Lock _lockObj = new();

    public void DoWork()
    {
        lock (_lockObj)
        {
            // Critical section
        }
    }
}

The only change is the type of the lock object. The rest of your code remains the same, but now it benefits from the modern synchronization improvements provided by System.Threading.Lock.

Partial Properties and Indexers

We’re already familiar with partial classes and methods, which allow splitting implementation across multiple files.

With C# 13, this flexibility is extended to partial properties and partial indexers. This means you can split the declaration and implementation of a property or indexer across different parts of a class, much like partial methods.

Here’s what you need to know:

  • A declaring declaration defines the property or indexer without providing a body.
  • An implementing declaration provides the actual logic.
  • The signatures of both declarations must match exactly.
  • Auto-properties can’t be used as the implementing declaration, you must provide a body.
  • Properties or indexers that don’t declare a body are treated as the declaring declaration
C#
public partial class Customer
{
    // Declaring declaration
    public partial string FullName { get; }
}

public partial class Customer
{
    private string _firstName = "John";
    private string _lastName = "Doe";

    // Implementing declaration
    public partial string FullName => $"{_firstName} {_lastName}";
}

New Escape Sequence

C# 13 adds a dedicated escape sequence for the ESC (Escape) character. Until now, developers had to write it using the Unicode form \u001b, which wasn’t very readable.

With the new \e escape sequence, writing console applications that interact with VT100/ANSI terminal codes becomes simpler and more intuitive.

For example:

C#
// Before C# 13
Console.WriteLine("\u001b[31mHello, World!\u001b[0m");

// With C# 13
Console.WriteLine("\e[31mHello, World!\e[0m");

Implicit Index Access

C# 13 now allows the “from the end” index operator (^) inside object initializers for single-dimension arrays or collections.

This is especially useful when you want to set values starting from the end of a collection, such as preparing buffers, countdowns, or recent logs.

Before C# 13

Previously, you couldn’t use ^ in an object initializer, so you had to assign by forward indexes:

C#
public class LogBuffer
{
    public string[] Messages { get; set; } = new string[5];
}

LogBuffer logs = new()
{
    Messages =
    {
        [0] = "Trace: App started",
        [1] = "Debug: Cleanup started",
        [2] = "Info: Scheduled backup completed",
        [3] = "Warning: High memory usage",
        [4] = "Critical: Disk full"
    }
};
With C# 13
C#
LogBuffer logs = new()
{
    Messages =
    {
        [^1] = "Critical: Disk full",
        [^2] = "Warning: High memory usage",
        [^3] = "Info: Scheduled backup completed",
        [^4] = "Debug: Cleanup started",
        [^5] = "Trace: App started"
    }
};

Here, the most recent log (Critical: Disk full) is placed at the end of the array, while older logs are positioned relative to it.

With C# 13, the intent is clearer, you’re directly saying “this goes at the end, that goes before it,” which is much more natural for certain data patterns.

ref and unsafe in Iterators and async Methods

Before C# 13, developers faced strict limitations when working with ref locals and ref struct types inside async or iterator methods. Declaring them simply wasn’t allowed, and iterator methods couldn’t contain unsafe code at all.

With C# 13, these restrictions are relaxed. Async methods can now declare ref locals and ref struct locals, provided they don’t cross an await boundary. Similarly, iterator methods can take advantage of unsafe contexts, though all yield return and yield break statements must still remain in safe code.

These changes make it possible to use performance-focused types like Span<T> and ReadOnlySpan<T> in more scenarios, without compromising compiler-enforced safety.

allows ref struct in Generics

Before C# 13, ref struct types couldn’t be used as type parameters in generic types or methods. This was a limitation that made it harder to write generic algorithms that work with types like Span<T> and ReadOnlySpan<T>.

C# 13 solves this problem with a new anti-constraint called allows ref struct. This constraint explicitly states that a type parameter can be a ref struct.

The compiler still enforces all ref-safety rules, ensuring scoped lifetime checks remain valid.

C#
public class Processor<T> where T : allows ref struct
{
    // Use T as a ref struct:
    public void Process(scoped T item)
    {
        // item must follow ref safety rules
    }
}

Now, you can safely use ref struct types in generic algorithms:

C#
Span<int> span = [ 1, 2, 3 ];
Processor<Span<int>> processor = new();
processor.Process(span);

This feature enables more reusable and type-safe generic code, particularly when working with performance-oriented APIs that rely on Span<T> or similar ref struct types.

ref struct Interfaces

In earlier versions of C#, ref struct types couldn’t implement interfaces. That limitation is lifted in C# 13, allowing ref struct types to declare that they implement an interface.

However, to maintain ref safety rules, there are a few important restrictions:

  • A ref struct can’t be converted to an interface type, because such a conversion would require boxing, which could break ref safety.
  • Explicit interface implementations inside a ref struct can only be accessed through a type parameter that’s declared with allows ref struct.
  • As with normal structs and classes, all interface members must be implemented — including those with default implementations.
C#
public interface IFormatter
{
    void Format();
}

public ref struct LogFormatter : IFormatter
{
    public void Format()
    {
        Console.WriteLine("Formatting log output...");
    }
}
C#
LogFormatter formatter = new();
formatter.Format();

Summary

C# 13 isn’t a revolutionary update, but it delivers meaningful improvements that:

  • Simplify everyday coding (params collections, implicit index access).
  • Modernize synchronization (lock object).
  • Improve code generation scenarios (partial properties).
  • Expand ref struct usability across generics, async/iterators, and interfaces.
  • Add polish to language syntax (escape sequence).

C# continues to move forward step by step, and this release is no exception. Stay tuned — C# 14 and .NET 10 are already on the horizon with even more exciting updates.

Takeaways

C# 13 shows how the language continues to balance productivity with safety. Developers get less boilerplate through params collections, implicit index access, and the new \e escape sequence. At the same time, partial properties and indexers give more flexibility in designing and evolving APIs. Perhaps most importantly, ref struct support has grown significantly from generics to async and iterator methods, and now even interfaces, enabling advanced performance-oriented scenarios while maintaining compiler-enforced safety rules.

Overall, this release may feel incremental, but these refinements add up to a more powerful, developer-friendly language. And with C# 14 and .NET 10 on the way, it’s clear that the evolution of the language is far from slowing down.

Found this article useful? Share it with your network and spark a conversation.