Skip to main content

C# 13 New Features — Complete Guide With Examples (2026)

Learn all C# 13 new features with practical code examples. Params collections, partial properties, Lock type, and more — upgrade your .NET 9 skills today.

C# 13 shipped with .NET 9 in November 2024, and by mid-2026 most teams have either adopted it or are planning their upgrade. Whether you're already using it in production or still evaluating the jump, this guide covers every C# 13 new feature with practical, runnable code examples so you can see exactly what changed and why it matters.

This isn't a changelog copy-paste. We'll walk through each feature, explain the problem it solves, show before-and-after code, and flag the gotchas you'll hit in real projects.

1. params Collections — The Biggest Quality-of-Life Win in C# 13

Before C# 13, the params keyword only worked with arrays. That meant every call allocated a new array on the heap, even if the method just iterated over the values once. C# 13 lifts that restriction: params now works with any collection type — Span<T>, ReadOnlySpan<T>, IEnumerable<T>, List<T>, and more.

// C# 13 — zero-allocation params with Span
public static int Sum(params ReadOnlySpan<int> numbers)
{
    int total = 0;
    foreach (var n in numbers)
        total += n;
    return total;
}

// Calling it — no array allocation
int result = Sum(1, 2, 3, 4, 5);
Console.WriteLine(result); // 15

Why this matters: In hot paths — game loops, financial calculations, logging frameworks — eliminating that hidden array allocation can meaningfully reduce GC pressure. The compiler stack-allocates the span when it can, so you get performance for free without changing call sites.

You can also use params IEnumerable<T> or params List<T> when you need the flexibility:

public static void LogAll(params IEnumerable<string> messages)
{
    foreach (var msg in messages)
        Console.WriteLine($"[LOG] {msg}");
}

// Pass individual args
LogAll("Starting", "Processing", "Done");

// Or pass an existing collection directly
List<string> cached = ["Cached entry 1", "Cached entry 2"];
LogAll(cached);

Pitfall: If you overload a method with both params T[] and params ReadOnlySpan<T>, the span overload wins at the call site. Existing code that relied on receiving an actual array (e.g., storing it in a field) will silently switch to the span overload after recompilation. Audit your overloads before upgrading.

2. The New System.Threading.Lock Type

C# developers have always used lock (someObject) with a plain object as the synchronization target. The problem? Monitor.Enter works on any object, which makes it easy to accidentally lock on a publicly visible reference, and the runtime can't optimize for the dedicated-lock case.

C# 13 introduces System.Threading.Lock — a purpose-built type that the lock statement now recognizes:

using System.Threading;

public class ThreadSafeCounter
{
    private readonly Lock _lock = new();
    private int _count;

    public int Increment()
    {
        lock (_lock) // Compiler uses Lock.EnterScope(), not Monitor.Enter()
        {
            return ++_count;
        }
    }

    public int Current
    {
        get
        {
            lock (_lock)
            {
                return _count;
            }
        }
    }
}

Why switch? When the compiler sees lock on a Lock instance, it emits a call to Lock.EnterScope() which returns a ref struct scope guard. This is both more efficient and safer than the old Monitor-based pattern. The Lock type also makes intent explicit in your code — this field exists for synchronization and nothing else.

Pitfall: If you cast a Lock to object and lock on that, you fall back to the old Monitor path. The compiler warns about this (CS9216), but treat it as an error in your project settings.

3. Partial Properties and Indexers

C# has had partial methods since C# 3. C# 13 extends the partial modifier to properties and indexers, which is a game-changer for source generators.

// In your hand-written file
public partial class UserProfile
{
    public partial string DisplayName { get; set; }
    public partial int this[string key] { get; }
}

// In the source-generator-emitted file
public partial class UserProfile
{
    private string _displayName = string.Empty;

    public partial string DisplayName
    {
        get => _displayName;
        set => _displayName = value ?? throw new ArgumentNullException(nameof(value));
    }

    private readonly Dictionary<string, int> _data = new();

    public partial int this[string key]
    {
        get => _data.TryGetValue(key, out var val) ? val : -1;
    }
}

Why this matters: Source generators like those for MVVM frameworks, ORM mappers, and serialization libraries no longer need to generate entire classes with backing fields. They can implement just the properties that need custom logic while letting you declare the interface in your own code. This makes generated code dramatically easier to understand and debug.

4. The \e Escape Sequence

A small but welcome addition: \e is now a first-class escape sequence for the ESCAPE character (U+001B). Before C# 13, you had to use \x1B or \u001B, both of which are error-prone — \x1B in particular because it's greedy and will consume trailing hex digits.

// Before C# 13 — risky with \x
string oldWay = "\x1B[31mError\x1B[0m"; // Works, but fragile

// C# 13 — clear and safe
string red = "\e[31m";
string reset = "\e[0m";
Console.WriteLine($"{red}Error: something went wrong{reset}");

If you build CLI tools with ANSI color output, adopt \e immediately. It eliminates an entire class of subtle string bugs.

5. Implicit Index Access in Object Initializers

The ^ (index-from-end) operator now works inside object initializers. This lets you set values relative to the end of a collection during initialization:

var countdown = new int[5]
{
    [0] = 5,
    [1] = 4,
    [2] = 3,
    [^2] = 2,  // Same as [3]
    [^1] = 1   // Same as [4]
};

// Result: [5, 4, 3, 2, 1]

This is especially handy with buffer types and fixed-size data structures where you think in terms of "last element", "second to last", etc. It also works with custom types that have an indexer accepting Index.

6. ref and unsafe in Iterators and Async Methods

Before C# 13, you couldn't use ref locals inside async methods or iterators at all — the compiler rejected them outright. C# 13 relaxes this restriction: you can now declare ref locals as long as they don't survive across an await or yield boundary.

public async Task ProcessAsync(int[] data)
{
    // This is now legal in C# 13
    ref int first = ref data[0];
    first = 42; // Mutate before any await

    await Task.Delay(100); // ref local must not be used after this

    // Use data[0] normally after the await
    Console.WriteLine(data[0]); // 42
}

Why this was blocked before: The async state machine hoists locals to fields, and ref fields weren't safe to store on the heap. C# 13 allows it in scopes that the compiler can prove won't cross a suspension point.

7. allows ref struct — The Anti-Constraint for Generics

Ref structs (Span<T>, ReadOnlySpan<T>, etc.) have always been second-class citizens in generics — you simply couldn't use them as type arguments. C# 13 introduces allows ref struct, an anti-constraint that tells the compiler "this type parameter can accept ref struct types":

public static T Transform<T>(T input, Func<T, T> operation)
    where T : allows ref struct
{
    return operation(input);
}

// Now you can pass Span<int> as T
Span<int> data = stackalloc int[] { 1, 2, 3 };
var result = Transform(data, span =>
{
    // Process span here
    return span;
});

The .NET 9 BCL already uses this across Enumerable, Task, and many other APIs. If you author libraries, adding allows ref struct to appropriate type parameters opens your API to zero-allocation usage patterns without breaking existing callers.

8. OverloadResolutionPriorityAttribute

Library authors have long struggled with adding new, better overloads without breaking binary compatibility. C# 13 adds [OverloadResolutionPriority] to let you explicitly rank overloads:

using System.Runtime.CompilerServices;

public class Logger
{
    // Old overload — keep for binary compat
    [OverloadResolutionPriority(-1)]
    public void Log(params string[] messages) { /* ... */ }

    // New, preferred overload
    [OverloadResolutionPriority(1)]
    public void Log(params ReadOnlySpan<string> messages) { /* ... */ }
}

var logger = new Logger();
logger.Log("Hello", "World"); // Calls the Span overload (higher priority)

Higher values win. This is the mechanism the BCL itself uses to prefer span-based overloads introduced in .NET 9 without causing ambiguity errors in existing code.

9. The field Keyword (Preview in C# 13)

One of the most requested C# features is finally taking shape. The field keyword lets you access the compiler-generated backing field inside a property accessor — no need to declare a manual backing field:

// C# 13 Preview — enable with <LangVersion>preview</LangVersion>
public class Product
{
    public string Name
    {
        get => field;
        set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
    }

    public decimal Price
    {
        get => field;
        set
        {
            if (value < 0)
                throw new ArgumentOutOfRangeException(nameof(value));
            field = value;
        }
    }
}

Important: This feature shipped as a preview in C# 13 and became fully supported in C# 14 with .NET 10. If you're on .NET 9, you must opt in with <LangVersion>preview</LangVersion> in your project file. If you have an existing field named field, prefix it with @field to disambiguate.

How to Enable C# 13 in Your Projects

C# 13 is the default language version for .NET 9 projects. To confirm or force it, set your .csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <LangVersion>13</LangVersion> <!-- or "latest" -->
  </PropertyGroup>
</Project>

If you're targeting .NET 8 or earlier, many C# 13 language features still work as long as you set LangVersion to 13 — but runtime-dependent features like System.Threading.Lock require .NET 9.

C# 13 Best Practices and Migration Tips

  • Start with params collections. Search your codebase for params arrays in hot paths and switch to params ReadOnlySpan<T>. It's a mechanical change with measurable performance wins.
  • Replace object locks. Find every private readonly object _lock = new(); and replace with private readonly Lock _lock = new();. Zero risk, immediate benefit.
  • Adopt \e in CLI projects. If you use ANSI escape codes, switch to \e to prevent subtle bugs with \x1B hex greediness.
  • Wait on field for production. Unless you're on .NET 10 already, treat field as a preview feature that may have edge cases. Use it in side projects to build familiarity.
  • Add allows ref struct to library generics carefully. It's a non-breaking addition for consumers, but it changes what your method body can do with the type parameter — test thoroughly.

Conclusion — What C# 13 New Features Mean for Your Code

C# 13 isn't a flashy release with headline syntax changes. Instead, it's a precision upgrade: params collections eliminate allocations, the Lock type fixes a decades-old concurrency pattern, partial properties unblock source generators, and allows ref struct brings ref structs into mainstream generic programming.

The theme across all C# 13 new features is removing friction — things that were tedious, error-prone, or impossible before now just work. If you're on .NET 9, you're already using C# 13 whether you know it or not. Take an afternoon to apply the migration tips above, and your codebase will be cleaner and faster for it.

Key takeaways:

  • params works with spans and collections — stop allocating arrays in hot paths
  • System.Threading.Lock replaces lock (obj) with a safer, faster pattern
  • Partial properties unlock better source generator designs
  • \e escape sequence eliminates ANSI color bugs
  • allows ref struct brings Span<T> into generic APIs
  • The field keyword (preview) is the future of property accessors — try it now, ship it with .NET 10
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

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...

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...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...