- Published on
- //
Explore C# 14 in .NET 10
- Authors

- Name
- Martin Staael
- @staael

Explore C# 14 in .NET 10
C# 14 brings a host of new language features to .NET 10, focusing on enhanced extensibility, performance, and developer productivity. These updates build on C#'s evolution, making it easier to write clean, efficient code. Available with the .NET 10 SDK and Visual Studio 2026, let's explore these key updates.
1. Extension Members
C# 14 introduces extension members, allowing you to define extension properties and methods that extend existing types. These can be instance-style or static-style extensions, providing more flexibility in code organization.
public static class Enumerable
{
// Instance-style extension block
extension<TSource>(IEnumerable<TSource> source)
{
// Extension property
public bool IsEmpty => !source.Any();
// Extension method
public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
}
// Static-style extension block
extension<TSource>(IEnumerable<TSource>)
{
// Static extension method
public static IEnumerable<TSource> Combine(
IEnumerable<TSource> first, IEnumerable<TSource> second) { ... }
// Static extension property
public static IEnumerable<TSource> Identity => Enumerable.Empty<TSource>();
// User-defined operator as static extension method
public static IEnumerable<TSource> operator +(
IEnumerable<TSource> left, IEnumerable<TSource> right) => left.Concat(right);
}
}
Members in instance-style blocks are accessed as instance members, while static-style blocks treat them as static members of the type.
Here's another example of using extension members to add functionality to a string type:
public static class StringExtensions
{
extension(string str)
{
public bool IsPalindrome => str.SequenceEqual(str.Reverse());
public string Reverse() => new string(str.Reverse().ToArray());
}
}
// Usage:
string test = "radar";
Console.WriteLine(test.IsPalindrome); // True
2. The field Keyword (Field-Backed Properties)
The field keyword simplifies property declarations by letting the compiler generate backing fields automatically, reducing boilerplate code.
// Before C# 14:
private string _msg;
public string Message
{
get => _msg;
set => _msg = value ?? throw new ArgumentNullException(nameof(value));
}
// C# 14:
public string Message
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
Be mindful of potential naming conflicts with members named field; use @field or this.field to resolve them.
For a more complex scenario, consider a property with custom logic in both getter and setter:
public int Age
{
get => field;
set
{
if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
field = value;
}
}
3. Implicit Span Conversions
C# 14 adds first-class support for System.Span<T> and System.ReadOnlySpan<T>, including implicit conversions to and from arrays, improving performance in high-throughput scenarios.
This feature enhances generics, conversions, and extension methods, making spans more natural to use.
Example of implicit conversion in action:
void ProcessSpan(Span<int> span)
{
// Process the span
foreach (var item in span)
{
Console.Write(item + " ");
}
}
// Usage with array (implicit conversion)
int[] array = {1, 2, 3};
ProcessSpan(array); // Works directly
Another example with string to ReadOnlySpan<char>:
ReadOnlySpan<char> GetSpan(string text) => text; // Implicit conversion
// Usage:
var span = GetSpan("Hello");
Console.WriteLine(span.Length); // 5
4. Unbound Generic Types and nameof
The nameof operator now works with unbound generic types, returning the name of the generic type definition.
nameof(List<>) // Returns "List"
This is useful for reflection and diagnostics without needing closed types.
In a practical scenario, such as logging type names:
public void LogType<T>()
{
Console.WriteLine(nameof(T<>)); // For generic T, logs the open type name
}
// If called as LogType<List>(), it would log "List"
5. Simple Lambda Parameters with Modifiers
Lambda parameters can include modifiers like ref, out, in, scoped, and ref readonly without explicit types, relying on type inference.
delegate bool TryParse<T>(string text, out T result);
// C# 14:
TryParse<int> parse1 = (text, out result) => Int32.TryParse(text, out result);
The params modifier still requires explicit typing.
Example with ref modifier:
Action<string, ref int> increment = (text, ref count) => count += text.Length;
// Usage:
int total = 0;
increment("Hello", ref total); // total = 5
6. More Partial Members
C# 14 supports partial constructors and partial events.
- Partial constructors require one defining and one implementing declaration.
- Partial events have a field-like defining declaration and accessors in the implementing one.
Only the implementing partial constructor can include this() or base() initializers.
Example of partial constructor:
// File1.cs
public partial class Person
{
public partial Person(string name);
}
// File2.cs
public partial class Person
{
private string _name;
public partial Person(string name)
{
_name = name;
}
}
Example of partial event:
// File1.cs
public partial class SampleClass
{
public partial event EventHandler MyEvent;
}
// File2.cs
public partial class SampleClass
{
public partial event EventHandler MyEvent
{
add { /* implementation */ }
remove { /* implementation */ }
}
}
7. User-Defined Compound Assignment Operators
Developers can now define custom compound assignment operators such as += and -=, extending operator overloading capabilities.
Example for a custom Vector class:
public struct Vector2D
{
public double X { get; set; }
public double Y { get; set; }
public static Vector2D operator +(Vector2D a, Vector2D b) =>
new Vector2D { X = a.X + b.X, Y = a.Y + b.Y };
public static Vector2D operator checked +(Vector2D a, Vector2D b) => a + b; // For checked context
// User-defined += operator
public static Vector2D operator +=(Vector2D a, Vector2D b) => a + b;
}
// Usage:
Vector2D v1 = new() { X = 1, Y = 2 };
Vector2D v2 = new() { X = 3, Y = 4 };
v1 += v2; // v1 now {4, 6}
8. Null-Conditional Assignment
Null-conditional operators ?. and ?[] can appear on the left side of assignments or compound assignments.
customer?.Order = GetCurrentOrder();
The right-hand side evaluates only if the left is non-null. This supports compound assignments but not ++ or --.
Another example with arrays:
int[]? array = null;
array?[0] = 42; // No assignment, since array is null
array = new int[5];
array?[0] = 42; // Assigns 42 to array[0]
Example with compound assignment:
customer?.Balance += 100; // Adds only if customer != null
9. New Preprocessor Directives for File-Based Apps
New directives support file-based application models, aiding in conditional compilation and organization.
Example using #if for file-based scenarios:
#if FILE_BASED_APP
Console.WriteLine("Running in file-based mode");
#else
Console.WriteLine("Standard mode");
#endif
Another directive example with #define:
#define DEBUG_LOGGING
#if DEBUG_LOGGING
public void Log(string message) => Console.WriteLine(message);
#else
public void Log(string message) { } // No-op
#endif
These features in C# 14 refine the language for modern development needs, blending extensibility with safety and performance. I encourage you to explore these new features and discover how they can elevate your current and future projects.
For more details, check the official documentation: What's new in C# 14.
