C# 14 is here, and it's packed with features that will make your code cleaner, more expressive, and more performant. Whether you're a seasoned .NET developer or just getting started, this complete guide covers every new feature in C# 14.
Introduction
C# 14 ships with .NET 10, and it's one of the most significant releases in recent years. From extension members that completely change how we extend types, to practical quality-of-life improvements that eliminate boilerplate code, C# 14 has something for everyone.
In this guide, we'll explore each new feature in detail, with code examples and practical use cases. Let's dive in!
1. Extension Members: The Headline Feature
Extension members are the most significant addition to C# 14. They extend the concept of extension methods to include properties, operators, and static members.
What Problem Does It Solve?
For over a decade, C# developers have used extension methods to add functionality to existing types. But extension methods were limited - you couldn't add properties, operators, or static members. This limitation often forced developers to create wrapper types or duplicate code.
Extension members solve this by allowing you to add instance properties, operators, and static members to any existing type.
Syntax
Extension members use a new extension keyword with a receiver syntax:
public static class EnumerableExtensions
{
// Instance extension block - receiver 'source' is the instance
extension<TSource>(IEnumerable<TSource> source)
{
// Extension property
public bool IsEmpty => !source.Any();
// Extension method (works like before)
public IEnumerable<TSource> Where(Func<TSource, bool> predicate)
{
foreach (var item in source)
if (predicate(item))
yield return item;
}
}
// Static extension block - no receiver needed for static members
extension<TSource>(IEnumerable<TSource>)
{
// Static extension property
public static IEnumerable<TSource> Identity => Enumerable.Empty<TSource>();
// Static extension operator
public static IEnumerable<TSource> operator +(
IEnumerable<TSource> left,
IEnumerable<TSource> right)
=> left.Concat(right);
}
}
Real-World Usage
var numbers = new[] { 1, 2, 3, 4, 5 };
// Use extension property
if (numbers.IsEmpty)
{
Console.WriteLine("No numbers!");
}
// Use extension operator
var combined = numbers + new[] { 6, 7, 8 };
// [1, 2, 3, 4, 5, 6, 7, 8]
// Use static extension property
var empty = IEnumerable<int>.Identity;
Key Benefits
- Add properties to framework types - No more wrapper classes for simple computed properties
- Define operators on existing types - Extend types with custom operators without modifying them
- Static extension members - Add static methods/properties to types you don't own
- Backward compatible - Works alongside existing extension methods
2. The field Keyword: Auto Properties With Logic
The field keyword is a contextual keyword that provides a middle ground between auto-implemented properties and fully hand-written backing fields.
Before C# 14
If you needed to add validation logic to a property, you had to declare a backing field:
private string _name;
public string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException(nameof(value));
}
With C# 14
Now you can use the field keyword, which represents a compiler-generated backing field:
public string Name
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
More Examples
public class Product
{
private decimal _price;
// Traditional approach
public decimal Price
{
get => _price;
set => _price = value >= 0 ? value : throw new ArgumentException("Price cannot be negative");
}
// With field keyword
public decimal DiscountPrice
{
get;
set => field = value >= 0 ? value : throw new ArgumentException("Discount price cannot be negative");
}
// Multiple accessors with field
public string Description
{
get => field ?? "No description";
set => field = value?.Trim();
}
}
Important Notes
-
Naming conflicts: If you have an existing member named
field, use@fieldorthis.fieldto disambiguate - Backward compatible: Existing code continues to work unchanged
- Encourages property-only access: All code within the type must use the property
3. Null-Conditional Assignment: Eliminate Boilerplate
C# 14 allows you to use null-conditional operators (?.) on the left-hand side of assignments.
Before C# 14
if (customer is not null)
{
customer.Order = GetCurrentOrder();
customer.Total += CalculateIncrement();
}
With C# 14
customer?.Order = GetCurrentOrder();
customer?.Total += CalculateIncrement();
Behavior
- The right-hand side is evaluated only when the left side is not null
- Works with
?.and?[]operators - Supports compound assignment operators (
+=,-=,*=, etc.) - Does NOT support increment/decrement operators (
++,--)
Practical Example
public class ShoppingCart
{
public Order? CurrentOrder { get; set; }
public decimal Total { get; set; }
}
public void ProcessOrder(ShoppingCart? cart)
{
// Before: Multiple null checks
if (cart is not null && cart.CurrentOrder is not null)
{
cart.CurrentOrder.Status = OrderStatus.Processing;
cart.Total += cart.CurrentOrder.Subtotal;
}
// After: Clean null-conditional assignment
cart?.CurrentOrder?.Status = OrderStatus.Processing;
cart?.Total += cart?.CurrentOrder?.Subtotal ?? 0;
}
4. Unbound Generic Types with nameof
The nameof operator now accepts unbound generic types (types without type arguments).
Before C# 14
var name = nameof(List<int>); // Returns "List"
With C# 14
var name = nameof(List<>); // Returns "List"
Practical Use Cases
// Logging generic type names
public void LogType<T>()
{
_logger.LogInformation($"Processing type: {nameof(T)}");
_logger.LogInformation($"Generic type name: {nameof(List<>)}");
}
// Exception messages
public void ValidateType(Type type)
{
if (!type.IsGenericType)
throw new InvalidOperationException(
$"Expected generic type, got {nameof(type)}");
}
// Reflection scenarios
public class Repository<T>
{
public static string GetTypeName()
{
return nameof(Repository<>); // Returns "Repository"
}
}
Benefits
- No need to specify arbitrary type arguments
- Cleaner code for generic type logging/throwing
- Useful in reflection and code generation scenarios
5. Simple Lambda Parameters with Modifiers
Lambda expressions can now use parameter modifiers (ref, in, out, scoped) without explicit type annotations.
Before C# 14
delegate bool TryParse<T>(string text, out T result);
TryParse<int> parse = (string text, out int result) =>
int.TryParse(text, out result);
With C# 14
TryParse<int> parse = (text, out result) =>
int.TryParse(text, out result);
More Examples
// Ref parameters
var process = (ref int x, int y) => x += y;
// In parameters
var compare = (in int x, in int y) => x.CompareTo(y);
// Out parameters
var tryGetValue = (string key, out string value) =>
dictionary.TryGetValue(key, out value);
// Mixed modifiers
var complexParse = (string text, out int number, out bool isValid) =>
{
isValid = int.TryParse(text, out number);
return isValid;
};
Limitations
-
paramsmodifier still requires explicit type annotations - Parameter types are still inferred from delegate context
6. Partial Events and Constructors
C# 14 allows instance constructors and events to be declared as partial members.
Why This Matters
This is particularly useful for source generators and partial type scenarios where different files or generators contribute to a type's definition.
Partial Events
public partial class Widget
{
public partial event EventHandler Changed;
}
public partial class Widget
{
private EventHandler? _changed;
public partial event EventHandler Changed
{
add => _changed += value;
remove => _changed -= value;
}
}
Partial Constructors
public partial class Widget(int size, string name)
{
public int Size { get; } = size;
public string Name { get; } = name;
}
public partial class Widget
{
public Widget() : this(0, "Default") { }
public Widget(string name) : this(10, name) { }
// Additional constructor body logic
public Widget
{
Initialize(); // Additional initialization
}
}
Rules
- Exactly one defining declaration and one implementing declaration
- Implementing declaration can include constructor initializers (
this()orbase()) - Only one partial type can include primary constructor syntax
- Partial events must include
addandremoveaccessors
7. Implicit Span Conversions
C# 14 introduces first-class language support for Span<T> and ReadOnlySpan<T> with implicit conversions.
Before C# 14
string line = ReadLine();
ReadOnlySpan<char> key = line.AsSpan(0, 5); // Explicit call
ProcessKey(key);
int[] buffer = GetBuffer();
Span<int> head = new(buffer, 0, 8); // Explicit constructor
Accumulate(head);
With C# 14
string line = ReadLine();
ProcessKey(line[..5]); // Implicit conversion
int[] buffer = GetBuffer();
Accumulate(buffer[..8]); // Implicit conversion
Conversions Supported
| From | To |
|---|---|
T[] |
Span<T> |
T[] |
ReadOnlySpan<T> |
string |
ReadOnlySpan<char> |
Span<T> |
ReadOnlySpan<T> |
Benefits
- Zero-allocation conversions where possible
- Enables better JIT optimizations
- Reduces explicit
AsSpan()andSpan<T>()constructor calls - More natural string slicing syntax
8. User-defined Compound Assignment
C# 14 allows you to define custom compound assignment operators (+=, -=, etc.).
Before C# 14
public struct BigVector(float x, float y, float z)
{
public float X { get; private set => value = field; } = x;
public float Y { get; private set => value = field; } = y;
public float Z { get; private set => value = field; } = z;
public static BigVector operator +(BigVector l, BigVector r)
=> new(l.X + r.X, l.Y + r.Y, l.Z + r.Z);
}
// Usage - creates intermediate temporaries
BigVector sum = BigVector.Zero;
foreach (var v in values)
{
sum = sum + v; // Creates new temporary each iteration
}
With C# 14
public struct BigVector(float x, float y, float z)
{
public float X { get; private set => value = field; } = x;
public float Y { get; private set => value = field; } = y;
public float Z { get; private set => value = field; } = z;
public static BigVector operator +(BigVector l, BigVector r)
=> new(l.X + r.X, l.Y + r.Y, l.Z + r.Z);
public void operator +=(BigVector r)
{
X += r.X;
Y += r.Y;
Z += r.Z;
}
}
// Usage - no intermediate temporaries
BigVector sum = BigVector.Zero;
foreach (var v in values)
{
sum += v; // Calls user-defined operator += directly
}
Performance Benefits
- Avoids creating intermediate temporaries
- Enables better JIT optimizations in tight loops
- Critical for high-performance numeric and SIMD types
- More idiomatic API design
Summary Table
| Feature | Problem Solved | Key Benefit |
|---|---|---|
| Extension Members | Can't add properties/operators to existing types | Add properties, operators, static members to any type |
| field Keyword | Manual backing fields for properties with logic | Cleaner property code, no manual fields needed |
| Null-conditional Assignment | Explicit null checks before assignment | Eliminates boilerplate null-checking code |
| Unbound Generics with nameof | Can't get generic type name without type args | Cleaner generic type logging/throwing |
| Lambda Parameter Modifiers | Need types for modifiers in lambdas | Keep lambdas concise with ref/out/in |
| Partial Events & Constructors | Can't split event/constructor logic across files | Better source generator support |
| Implicit Span Conversions | Explicit AsSpan() calls required | Zero-allocation conversions, better performance |
| Compound Assignment Operators | No custom compound operators | Avoid temporaries, better JIT optimization |
Migration Guide
Upgrade to C# 14
- Update your project file:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14</LangVersion>
</PropertyGroup>
Install .NET 10 SDK
-
Gradually adopt features:
- Start with
fieldkeyword for simple property validation - Use null-conditional assignment to reduce null checks
- Adopt extension members for utility properties on framework types
- Use implicit span conversions for better performance
- Start with
Breaking Changes
-
fieldkeyword conflicts: If you have a member namedfield, use@fieldor rename it - Generic inference: Some scenarios may need explicit type arguments
- Extension member resolution: Participates in overload resolution like regular members
Conclusion
C# 14 represents a significant step forward for the language. The extension members feature alone changes how we think about extending types, while the quality-of-life improvements like field keyword and null-conditional assignment eliminate common sources of boilerplate code.
Whether you're building high-performance applications with spans and compound operators, or just want cleaner property code with the field keyword, C# 14 has something valuable to offer.
Get started today by installing .NET 10 SDK and trying out these features in your next project!
Further Reading
- What's new in C# 14 - Microsoft Learn
- Introducing C# 14 - .NET Blog
- Extension Members Specification
- .NET 10 Overview
Connect with me on LinkedIn: https://www.linkedin.com/in/vikrant-bagal
Top comments (0)