C# 13 shipped with .NET 9 in November 2024, continuing the language team’s pattern of targeted, pragmatic improvements rather than sweeping reinventions. Some releases are splashy. C# 13 is surgical — each feature solves a specific frustration that experienced C# developers have hit repeatedly.
This post covers the features that will actually show up in your daily code. Not the edge cases, not the spec curiosities — the changes that make you write less boilerplate and run faster code.
Table of Contents
- params Collections Generalization
- New Lock Object and Lock Statement
- ref and unsafe in Iterators and async
- New Escape Sequences
- Extension Members (Preview)
- Implicit Index Access in Object Initializers
- Method Group Natural Type Improvements
- FAQ
params Collections Generalization
This one changes how you’ll design APIs. Before C# 13, params only worked with arrays:
// C# 12 and earlier
void Log(string message, params string[] tags) { }
Log("error", "auth", "database"); // fine
Log("error", myList.ToArray()); // annoying — had to call ToArray()
In C# 13, params works with any collection type that supports collection expressions:
// C# 13
void Log(string message, params IEnumerable<string> tags) { }
void Log(string message, params ReadOnlySpan<string> tags) { } // zero allocation
void Log(string message, params List<string> tags) { }
void Log(string message, params HashSet<string> tags) { }
// Now callers can pass lists, spans, arrays — no conversion needed
Log("error", myList);
Log("error", mySpan);
Log("error", "auth", "database"); // still works with inline values
Performance Impact: params ReadOnlySpan
The most important new variant is params ReadOnlySpan<T>. When you pass inline values, they’re stack-allocated — no heap allocation at all:
// Zero heap allocations for inline calls
void ProcessValues(params ReadOnlySpan<int> values)
{
foreach (var v in values)
Console.WriteLine(v);
}
ProcessValues(1, 2, 3, 4); // stack-allocated int[4], no GC pressure
For hot paths — logging, metric recording, batch processing — this is a meaningful performance win. The BCL itself has been updated to use params ReadOnlySpan<T> overloads throughout.
New Lock Object and Lock Statement
Threading code just got safer. C# 13 introduces System.Threading.Lock, a dedicated lock type that’s more explicit and correct than using arbitrary objects:
// Before C# 13 — using an object as a lock (error-prone)
private readonly object _lock = new object();
void DoWork()
{
lock (_lock)
{
// critical section
}
}
// C# 13 — dedicated Lock type
using System.Threading;
private readonly Lock _lock = new Lock();
void DoWork()
{
lock (_lock) // compiler emits optimized code for Lock type
{
// critical section
}
}
Why the Dedicated Type Matters
The Lock type uses optimized OS primitives instead of the heavyweight Monitor-based locking used by lock (object). Microsoft reports 10–20% lower overhead in contended scenarios. It also exposes a Lock.Scope struct for using-statement patterns:
// Using-statement pattern with Lock.Scope
private readonly Lock _lock = new Lock();
void DoWork()
{
using (_lock.EnterScope())
{
// critical section — released when scope exits
}
}
Additionally, the type system can now catch mistakes — you can’t accidentally pass a Lock instance where an object lock is expected in the wrong context, reducing the class of bugs where you lock on the wrong thing.
ref and unsafe in Iterators and async Methods
This is a niche but important capability unlock. Previously, you couldn’t use ref locals or unsafe blocks inside iterator methods (yield return) or async methods. C# 13 relaxes this restriction.
ref locals in Iterators
// C# 13 — ref locals now work in iterators
IEnumerable<int> ProcessBuffer(int[] buffer)
{
ref int first = ref buffer[0]; // ref local — previously illegal in iterators
for (int i = 0; i < buffer.Length; i++)
{
yield return buffer[i] * first;
}
}
unsafe in async Methods
// C# 13 — unsafe blocks in async methods
async Task ProcessDataAsync(nint ptr, int length)
{
unsafe
{
// Can now use pointer arithmetic inside async methods
byte* bytes = (byte*)ptr;
for (int i = 0; i < length; i++)
{
bytes[i] = Transform(bytes[i]);
}
}
await SaveResultsAsync();
}
The restriction still exists for ref variables across await points — a ref can’t be held across a suspension point. But you can use refs within the synchronous sections of async methods.
New Escape Sequences
A small quality-of-life addition: C# 13 adds \e as an escape sequence for the ESC character (Unicode U+001B). This is the character used in ANSI terminal escape codes for color output:
// Before C# 13 — awkward workarounds
string redText = "\u001B[31mError\u001B[0m";
string redText2 = $"{(char)27}[31mError{(char)27}[0m";
// C# 13 — clean and readable
string redText = "\e[31mError\e[0m";
Console.WriteLine("\e[32mSuccess!\e[0m"); // green text
Console.WriteLine("\e[31mFailed!\e[0m"); // red text
Tiny change, but if you write CLI tools or structured console output, you’ll appreciate it immediately.
Extension Members (Preview)
This is the big one in preview — a complete redesign of how extensions work in C#. The old extension method syntax via static classes works, but it’s limited: you can only add methods, not properties, events, or operators.
C# 13 introduces extension blocks as a preview feature that unifies all extension member types:
// C# 13 preview — extension block syntax
extension StringExtensions for string
{
// Extension property
public bool IsPalindrome
{
get
{
var reversed = new string(this.Reverse().ToArray());
return this == reversed;
}
}
// Extension method
public string Truncate(int maxLength) =>
this.Length <= maxLength ? this : this[..maxLength] + "...";
// Static extension method
public static string Empty => "";
}
// Usage
"racecar".IsPalindrome // true
"Hello World".Truncate(5) // "Hello..."
Extension Operators (Preview)
extension VectorMath for Vector2D
{
public static Vector2D operator +(Vector2D a, Vector2D b) =>
new Vector2D(a.X + b.X, a.Y + b.Y);
}
This is preview and the syntax may change. Enable it with <LangVersion>preview</LangVersion> in your csproj. Don’t ship preview features in production libraries.
[INTERNAL_LINK: C# 12 features recap and comparison]
Implicit Index Access in Object Initializers
A small but useful improvement to collection initialization. The ^ (from-end) index operator now works in object initializers:
// C# 13
var config = new AppConfig
{
Items =
{
[^1] = "last item", // previously required a variable, now works inline
[0] = "first item"
}
};
// Also works with nested initializers
var matrix = new int[3, 3]
{
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
// Access from end in initializer expressions
var last = matrix[^1, ^1]; // 9
Method Group Natural Type and Overload Priority
C# 13 adds the [OverloadResolutionPriority] attribute, which lets library authors guide the compiler toward preferred overloads without breaking existing code:
using System.Runtime.CompilerServices;
public class Logger
{
// Old overload — still works, but compiler prefers the new one
public void Log(string message) { }
// New high-performance overload — compiler prefers this
[OverloadResolutionPriority(1)]
public void Log(ReadOnlySpan<char> message) { }
}
var logger = new Logger();
logger.Log("test"); // compiler chooses Log(ReadOnlySpan<char>) — no breaking change
The BCL uses this to prefer Span-based overloads over string overloads across many APIs in .NET 9, giving you automatic performance improvements without any code changes.
FAQ
Do I need .NET 9 to use C# 13?
For most C# 13 features, yes — C# 13 ships with .NET 9. However, some language features don’t depend on new runtime types and can be used with <LangVersion>13</LangVersion> on older TFMs. The new Lock type and System.Threading updates do require .NET 9.
Is C# 13 a Long-Term Support release?
.NET 9 is a Standard Term Support (STS) release — 18 months of support. If you need long-term support, stay on .NET 8 (LTS) until .NET 10 LTS ships in November 2025. C# 12 with .NET 8 is still an excellent, fully-supported choice.
When will Extension Members leave preview?
The C# team has indicated extension members will continue maturing through 2025. Expect a stable release in C# 14 with .NET 10. The preview in C# 13 is for community feedback — expect breaking syntax changes before stabilization.
Is params ReadOnlySpan actually zero allocation?
For small inline calls (under the stack allocation threshold), yes — the compiler emits stack-allocated arrays. For very large param arrays (typically over 8 elements, implementation-defined), the compiler may fall back to heap allocation. Profile before assuming zero allocation in all cases.
How does the new Lock type interact with existing Monitor-based code?
System.Threading.Lock is a new type and doesn’t share state with Monitor.Enter/Exit. You can’t mix them on the same lock object. Migrate consistently — if you switch a field from object to Lock, update all locking sites.
C# 13 is a focused release that rewards developers who care about performance and clean APIs. The params generalization and new Lock type alone are worth upgrading for in performance-sensitive applications. Keep an eye on extension members — when they stabilize, they’ll be one of the most impactful C# language additions in years.
[INTERNAL_LINK: .NET 9 performance improvements benchmark results]

Leave a Reply