Exploring C# 13 – Key Features of Microsoft's Latest Release with .NET 9
- Harsh Gupta
- 11 Jul, 2025
- 13 Mins read
- Development
On November 12, 2024, Microsoft launched .NET 9 and C# 13, bringing exciting updates for developers. The new features in C# 13 are all about making coding faster, smoother, and more efficient. Whether you’re an experienced coder or just starting out, these updates are designed to help you write better code with less hassle. Let’s take a closer look at what’s new and how it can make a difference in your projects.
1. Params
Params Keyword
The params keyword in C# allows passing a variable number of arguments to a method without needing to create an array. This is helpful when the number of arguments is not fixed.
public void PrintNumbers(params int[] numbers)
{
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
// Usage PrintNumbers(1, 2, 3, 4, 5); // Output: 1 2 3 4 5
Collections
You can use collections (like List
public void PrintNames(List<string> names)
{
foreach (var name in names)
{
Console.WriteLine(name);
}
}
// Usage
PrintNames(new List
Tuples
Tuples allow grouping multiple values into a single object.
public void DisplayInfo((string Name, int Age) person)
{
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
}
// Usage DisplayInfo((“Alice”, 30));
Custom Classes
You can create custom classes to encapsulate multiple parameters.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public void DisplayPerson(Person person)
{
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
}
// Usage DisplayPerson(new Person { Name = “Alice”, Age = 30 });
Span and ReadOnlySpan in C#
Span
Key Features:
- Memory Efficiency: They provide a view over existing data, reducing memory usage.
- Performance: They allow fast, efficient memory access without creating new arrays.
- Safety: They prevent accessing out-of-bound elements, reducing errors.
Differences Between Span
- Span
: Mutable (you can modify data). - ReadOnlySpan
: Immutable (data cannot be changed).
Example: Using Span
public void ModifyArray(Span<int> numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] *= 2;
}
}
// Usage
int[] array = { 1, 2, 3 };
ModifyArray(array); // Modifies array elements
Example: Using ReadOnlySpan
public void PrintArray(ReadOnlySpan<int> numbers)
{
foreach (var number in numbers)
{
Console.Write(number + " ");
}
Console.WriteLine();
}
// Usage
int[] array = { 1, 2, 3 };
PrintArray(array); // Reads array elements
Creating Spans:
- From Arrays: Span
span = array; - Slicing: Span
slice = span.Slice(1, 2); - Stack Allocation: Span
stackSpan = stackalloc int[5];
Key Differences: Span, ReadOnlySpan, and Arrays
| Aspect | Arrays | Span |
|---|---|---|
| Memory Ownership | Own memory (allocated on the heap) | Don’t own memory, just provide a view of existing data |
| Mutability | Mutable | Span |
| Performance | Overhead with copying and allocating memory | Lightweight and faster, especially for temporary data and slices |
| Flexibility | Fixed size | Flexible slices from existing data |
| Stack Allocation | Allocated on the heap | Span |
2. New Lock Object
What Is the Lock Object in .NET 9?
Introduced in .NET 9, the Lock object simplifies thread synchronization. It provides a cleaner, more intuitive API for locking. It uses the EnterScope() method and automatically handles lock release using the Dispose() pattern.
With this, you don’t need to manually release the lock. You can just use the lock keyword as usual, and the system ensures proper lock management.
Lock lockObj = new Lock();
lock (lockObj) // Automatically handles locking
{
// Critical section
}
Switching to the Lock object in .NET 9 simplifies your code while providing better synchronization performance.
3. New Escape Sequence
In .NET, escape sequences are used to represent special characters in strings (like newlines, tabs, or backslashes). With .NET 9, a new escape sequence for the ESCAPE character (Unicode U+001B) has been introduced, which is often used for terminal control (like text formatting or color codes).
Previous Escape Sequences
Before .NET 9, you represented the ESCAPE character using either of these:
- Unicode Escape: \u001b
- Hexadecimal Escape: \x1b — this could be confusing if followed by more characters, like [31m (which represents red text in terminal systems).
Problem with \x1b: If you used \x1b[31m, the parser might interpret [31m as part of the escape sequence, leading to confusion.
New Escape Sequence in .NET 9: \e
.NET 9 introduces a new, clearer escape sequence: \e.
- \e represents the ESCAPE character (U+001B) directly.
- It avoids confusion with subsequent characters and is easier to read.
string str = "Hello \e[31mWorld\e[0m!";
[31m sets the text color to red, and [0m resets the formatting.
Examples Before and After .NET 9:
// Before .NET 9 (C# 13 and earlier):
string escapeWithUnicode = "\u001b[31mThis is red text (Before .NET 9)\u001b[0m";
string escapeWithHex = "\x1b[32mThis is green text (Before .NET 9)\x1b[0m";
// After .NET 9:
string escapeWithNewSyntax = "\e[34mThis is blue text (After .NET 9)\e[m";
4. Method Group Resolution
What Is a Method Group?
A method group in C# is a collection of methods with the same name but different parameter types. For example:
class Example
{
public void Foo(int x) { }
public void Foo(string x) { }
public void Foo(double x) { }
public void Foo<T>(T x) { } // Generic method
}
Here, Foo is a method group consisting of Foo(int x),Foo(string x), Foo(double x), and Foo
What Is Overload Resolution?
Overload resolution is the process by which the compiler selects the correct method from a method group based on the arguments you pass. Before .NET 9, overload resolution involved examining all methods in the method group, which could be inefficient and problematic, especially with generics or methods with constraints.
Old Behavior: Full Candidate Set Construction
Before .NET 9, the compiler would consider every method in the group, including those that didn’t match the arguments — generic methods would be considered even if the argument type didn’t match, and methods with constraints (e.g., where T : struct) would be considered even if the argument didn’t satisfy the constraint. This resulted in unnecessary checks, leading to slower compilation and increased memory usage.
New Behavior in .NET 9: Pruned Candidate Set
.NET 9 optimizes this by pruning irrelevant methods early in the process. The compiler now only considers methods that could actually match the arguments.
Key Differences Between Old and New Behavior
| Aspect | Old Behavior (Pre-.NET 9) | New Behavior (.NET 9 and onward) |
|---|---|---|
| Candidate Set | Builds a full set of candidate methods, including irrelevant ones | Prunes irrelevant methods early |
| Generic Methods | Considers all generic methods, even if parameters don’t match | Prunes non-matching generic methods immediately |
| Performance | Slower due to unnecessary checks | Faster as irrelevant methods are removed early |
| Scope Checking | Checks all methods globally | Prunes non-matching methods at each scope |
| Error Handling | Potential for more errors due to incorrect method matching | Fewer errors due to more accurate matching |
Example of the Difference
public void Foo(int x) { }
public void Foo<T>(T x) where T : struct { }
Old Behavior: Calling Foo(10) would consider both methods. The compiler would first try the generic method, but fail when it checks the constraint (T : struct), leading to unnecessary checks.
New Behavior: The compiler immediately prunes the generic method, as T must be a struct and 10 is an int, which fits the non-generic method. Only Foo(int x) is considered, making the process faster.
Why This Change Matters
- Efficiency: By removing irrelevant methods early, the compiler spends less time checking methods that cannot match.
- Accuracy: The compiler only considers valid methods, reducing the chances of errors.
- Consistency: The new approach aligns with the general overload resolution process, making the compiler’s behavior more predictable.
5. Implicit Index Access with the ^ Operator in C#
What Is the ^ Operator (From-the-End Indexing)?
The ^ operator in C# allows you to access elements from the end of a collection (like arrays or lists). Introduced in C# 8.0, it simplifies indexing when you want to reference the last few elements without manually calculating their indices.
- arr[^1] gives the last element.
- arr[^2] gives the second-to-last element.
- arr[^3] gives the third-to-last element, and so on.
Traditional Indexing
int[] arr = { 1, 2, 3, 4, 5 };
Console.WriteLine(arr[0]); // Prints 1 (first element)
Console.WriteLine(arr[4]); // Prints 5 (last element)
To access the last element, you’d need to use arr.Length - 1.
Using the ^ Operator (From the End Indexing)
int[] arr = { 1, 2, 3, 4, 5 };
Console.WriteLine(arr[^1]); // Prints 5 (last element)
Console.WriteLine(arr[^2]); // Prints 4 (second-to-last element)
Console.WriteLine(arr[^3]); // Prints 3 (third-to-last element)
New in C# 13: Using ^ in Object Initializers
C# 13 introduces the ability to use the ^ operator in object initializers. This allows you to directly reference and modify array elements from the end while initializing objects, making your code more intuitive and readable.
What Is an Object Initializer?
An object initializer lets you set the properties of an object when it’s created, without needing to call a constructor for each property.
public class TimerRemaining
{
public int[] buffer { get; set; } = new int[10];
}
Before C# 13 (Traditional Initialization):
var countdown = new TimerRemaining()
{
buffer = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }
};
After C# 13 (Using ^ in Initializers):
var countdown = new TimerRemaining()
{
buffer =
{
[^1] = 0,
[^2] = 1,
[^3] = 2,
[^4] = 3,
[^5] = 4,
[^6] = 5,
[^7] = 6,
[^8] = 7,
[^9] = 8,
[^10] = 9
}
};
This sets the array elements from the last index and counts backwards, improving readability.
Why Is This Useful?
- Simplified Syntax: The ^ operator allows easier access to elements from the end of an array, avoiding the need to manually calculate the index (e.g., arr.Length - 1).
- Intuitive Initialization: When initializing arrays in reverse order or modifying the last few elements,
^makes the code cleaner and more understandable. - Cleaner Code: You no longer need complex logic to calculate indices when referencing elements from the end.
6. Using ref and unsafe in Async and Iterator Methods
C# 13 introduces significant updates for working with ref variables, ref struct types, and unsafe code in async and iterator methods. These changes make it easier to handle low-level memory management while ensuring safety.
Key Concepts
- ref Variables and ref struct Types: A ref variable holds a reference to another variable, allowing direct modifications without copying. A ref struct is a type like Span
or ReadOnlySpan , designed for memory safety and performance — it must reside on the stack and cannot be boxed or stored on the heap. - Iterator Methods: Methods using yield return and yield break return values lazily, generating elements one at a time, which saves memory.
- Async Methods: async methods enable asynchronous programming using async and await. They return a Task or Task
and allow non-blocking operations. - Unsafe Code: unsafe code allows direct memory manipulation, using pointers and bypassing runtime safety checks.
Before C# 13: Limitations
Prior to C# 13, async methods couldn’t use ref variables or ref struct types (like Span
New Features in C# 13
C# 13 relaxes these restrictions, allowing more flexibility while maintaining memory safety.
1. Async Methods with ref Variables and ref struct Types
You can now declare ref variables and use ref struct types like Span
public async Task ExampleAsync()
{
Span<int> span = new Span<int>(new int[] { 1, 2, 3, 4 });
ref int value = ref span[2]; // Declaring a ref variable
value = 10; // Modify the value
await Task.Delay(1000); // Simulate async operation
}
2. Iterator Methods with unsafe Code
Iterator methods can now include unsafe code, enabling direct memory manipulation with pointers. However, yield return and yield break must stay within a safe context.
public unsafe IEnumerable<int> GetNumbers()
{
int* ptr = stackalloc int[10]; // Unsafe code in iterator method
for (int i = 0; i < 10; i++)
{
ptr[i] = i;
yield return ptr[i]; // Yield return is safe
}
}
Benefits of the New Features
- Improved Performance: You can now use ref struct types like Span
and ReadOnlySpan in async methods, allowing high-performance memory operations without heap allocations. - Flexible unsafe Code in Iterators: Iterator methods can now safely use pointers, which is useful for tasks requiring direct memory access.
- Safety Enforcement: The compiler ensures that ref types aren’t used across await or yield return boundaries, preserving memory safety.
7. The field Keyword in C# 13: A Simplified Approach to Property Backing Fields
This allows you to access the compiler-generated backing field of a property directly in its get and set accessors, eliminating the need to manually declare the backing field.
What Is a Backing Field?
public class Person
{
private string _name; // Backing field
public string Name
{
get { return _name; } // Access the backing field
set { _name = value; } // Modify the backing field
}
}
How the field Keyword Works
With C# 13, you can use the field keyword to refer to this automatically generated backing field without explicitly declaring it.
public class Person
{
public string Name
{
get => field; // Access the backing field
set => field = value; // Modify the backing field
}
}
What Happens Behind the Scenes?
The compiler generates a backing field with a name like
private string <Name>k__BackingField;
public string Name
{
get => <Name>k__BackingField;
set => <Name>k__BackingField = value;
}
Benefits of Using field
- Cleaner Code: No need to manually declare backing fields.
- Less Boilerplate: Reduces the amount of code, making property definitions more concise.
- Focus on Logic: You can focus on the logic of the property itself, without worrying about the underlying implementation.
Potential Issues to Watch Out For
If you already have a field or parameter named field, it will cause ambiguity. Resolve this with @field or this.field:
public class Person
{
private string field; // A regular field
public string Name
{
get => @field; // Disambiguates with the @ symbol
set => @field = value;
}
}
8. Overload Resolution Priority
C# 13 introduces the OverloadResolutionPriorityAttribute, a feature designed primarily for library authors. It allows developers to specify which method overload should be preferred when there are multiple options.
The Problem It Solves
As libraries evolve, new overloads may be added to improve performance or provide better functionality. When multiple overloads match the same method call, it can cause ambiguity.
public class Calculator
{
public int Add(int a, int b) { return a + b; }
public double Add(double a, double b) { return a + b; }
public int Add(int a, int b, int c) { return a + b + c; }
}
If a more efficient overload like Add(long a, long b) is added, the compiler might still prefer the older Add(int, int) method.
What Is the OverloadResolutionPriority Attribute?
You apply the attribute to method overloads, specifying a numeric priority. Overloads with higher values are selected over those with lower values.
public class Calculator
{
// Default Priority – 0 (Least)
public int Add(int a, int b) { return a + b; }
// New, more efficient overload
[OverloadResolutionPriority(2)]
public int Add(long a, long b) { return (int)(a + b); }
// Another overload
[OverloadResolutionPriority(1)]
public int Add(int a, int b, int c) { return a + b + c; }
}
If there’s ambiguity (e.g., when calling Add(5L, 10L)), the compiler will prefer Add(long, long) because it has a higher priority.
Key Benefits
- Preserve Backward Compatibility: New, optimized overloads can be added without breaking existing code.
- No Breaking Changes: Users don’t need to update their code unless they want to explicitly use a new overload.
- Disambiguation: In complex scenarios, you can guide the compiler to select the best overload.
Example in a Library
public class Library
{
// Older overload
public string FormatMessage(string message) => "Message: " + message;
// New, more efficient overload with higher priority
[OverloadResolutionPriority(2)]
public string FormatMessage(StringBuilder message) => "Message: " + message.ToString();
// Another overload
[OverloadResolutionPriority(1)]
public string FormatMessage(int count) => "Message repeated " + count + " times.";
}
For FormatMessage(“Hello”), the compiler will prefer the FormatMessage(string) overload. For FormatMessage(new StringBuilder(“Hello”)), the FormatMessage(StringBuilder) overload will be preferred. For FormatMessage(3), the FormatMessage(int) overload will be used.
Potential Pitfalls
- Overuse of Priority: Too many overloads with priorities can create confusion. Use this feature sparingly.
- Backward Compatibility: Raising the priority of an existing overload too much could cause unexpected behavior for users.
- Ambiguities: This attribute doesn’t resolve all overload conflicts, especially when overloads are incompatible with the arguments passed.
9. What Is a Ref Struct?
A ref struct is a special type in C# that is allocated on the stack (not the heap). Span
- They can’t be used with operations that require heap allocation, like in asynchronous methods or boxed into an object.
- They are strictly tied to the memory they’re allocated in, meaning their lifetime is very specific to where they are used.
The Problem Before C# 13
Before C# 13, you couldn’t use ref struct types like Span
public class MyClass<T>
{
T value;
}
You couldn’t use a ref struct (like Span
The New “allows ref struct” Feature in C# 13
With C# 13, a new feature called allows ref struct was introduced. This feature allows generics to accept ref struct types as type parameters while still ensuring that memory safety rules are followed.
public class MyClass<T> where T : allows ref struct
{
public void SomeMethod(scoped T p)
{
// Do something with p, which is a ref struct
}
}
Key Points:
- where T : allows ref struct: This line tells the compiler that T can be a ref struct.
- scoped T p: The scoped keyword ensures that the ref struct is only valid within a limited scope.
Example Usage
public class BufferProcessor<T> where T : allows ref struct
{
public void ProcessBuffer(scoped T buffer)
{
// Safely work with the buffer (e.g., Span<T> or ReadOnlySpan<T>)
}
}
Benefits of “allows ref struct”
- Memory Safety: The allows ref struct feature ensures stack-allocation rules are still followed even in generics.
- Flexibility with Generics: You can now write more flexible, reusable code that works with stack-allocated types like Span
. - Compiler Enforcement: The compiler ensures that any generic code using ref struct types follows all memory safety rules.
10. Introduction to Partial Members in C# 13
In C# 13, two new features called partial properties and partial indexers were introduced. These features build on the idea of partial methods and allow developers to split the implementation of properties or indexers into different parts of a class.
What Are Partial Properties and Indexers?
A partial property allows you to separate its declaration (just the signature) from its implementation (the actual code that defines how the property works).
Declaring a Partial Property
public partial class C
{
public partial string Name { get; set; }
}
Implementing a Partial Property
public partial class C
{
private string _name;
public partial string Name
{
get => _name;
set => _name = value;
}
}
Restrictions on Partial Properties
- No Auto-Properties in Implementation: In the implementation part, you cannot use an auto-property (like get; set;).
- Signature Matching: The declaration and implementation of the property must have the same signature (name, type, accessors).
- Private Fields: The implementation part often uses a private backing field, but this is not required in the declaration.
Example of Full Partial Property
File 1: C.Declaring.cs
public partial class C
{
public partial string Name { get; set; }
}
File 2: C.Implementing.cs
public partial class C
{
private string _name;
public partial string Name
{
get => _name;
set => _name = value;
}
}
Partial Indexers
Partial indexers work in the same way as partial properties.
// Declaring a Partial Indexer
public partial class C
{
public partial string this[int index] { get; set; }
}
// Implementing a Partial Indexer
public partial class C
{
private string[] _values = new string[10];
public partial string this[int index]
{
get => _values[index];
set => _values[index] = value;
}
}
Advantages of Partial Properties and Indexers
- Separation of Concerns: By splitting the code into multiple files, you can keep things organized and modular.
- Collaboration: Different developers can work on different parts of the class without conflicts.
- Auto-Generated Code: If part of your code is generated automatically, you can have the tool generate the declarations, while you manually implement the logic.
11. What Changed in C# 13: Ref Struct Types Can Now Implement Interfaces
In C# 13, a significant change was introduced that allows ref struct types to implement interfaces. Before this version, ref struct types (like Span
What Is a ref struct?
A ref struct is a type that is specifically designed to be allocated on the stack rather than the heap.
Key points about ref structs:
- No boxing: They cannot be converted to object, which would involve heap allocation.
- No async methods: They can’t be used in async methods because async methods require heap allocation.
- No class or struct fields: They can’t be fields in a regular class unless that class is also a ref struct.
What Changed in C# 13?
In C# 13, ref structs can now implement interfaces. However, to maintain their strict memory safety, there are still some important restrictions.
1. No Boxing to Interface Type
public ref struct MySpan
{
public int[] Data;
public MySpan(int[] data) { Data = data; }
}
public interface IMyInterface
{
void DoSomething();
}
public class Test
{
public void Example()
{
MySpan span = new MySpan(new int[] { 1, 2, 3 });
IMyInterface myInterface = span; // Error: Cannot box a ref struct to an interface
}
}
2. No Explicit Interface Implementation
public ref struct MyRefStruct : IMyInterface
{
void IMyInterface.DoSomething() // Invalid for ref structs
{
Console.WriteLine("Doing something!");
}
}
3. Implementing All Interface Methods
If a ref struct implements an interface, it must implement all the methods defined in that interface, even those with default implementations.
public interface IMyInterface
{
void DoSomething() // Default implementation
{
Console.WriteLine("Doing something in the interface!");
}
void DoSomethingElse();
}
public ref struct MyRefStruct : IMyInterface
{
public void DoSomething()
{
Console.WriteLine("MyRefStruct does something!");
}
public void DoSomethingElse()
{
Console.WriteLine("MyRefStruct does something else!");
}
}
4. No Virtual Methods in ref struct
public ref struct MyRefStruct
{
// This is invalid: ref structs cannot have virtual methods
public virtual void MyMethod()
{
Console.WriteLine("MyMethod");
}
}
Example of a ref struct Implementing an Interface
public interface IShape
{
void Draw();
}
public ref struct Circle : IShape
{
private double radius;
public Circle(double radius)
{
this.radius = radius;
}
public void Draw()
{
Console.WriteLine($"Drawing a circle with radius {radius}");
}
}
public class Test
{
public void Run()
{
Circle circle = new Circle(5.0);
IShape shape = circle; // Valid: ref struct can implement an interface
shape.Draw(); // Output: Drawing a circle with radius 5
}
}
In this example:
- Circle is a ref struct that implements the IShape interface.
- The Draw method is implemented in the ref struct and used through the interface (IShape)
- — no boxing or heap allocation happens, maintaining the ref struct’s stack-based memory model.