DEV Community

Cover image for C14: The Complete Guide to Every New Feature
Vikrant Bagal
Vikrant Bagal

Posted on

C14: The Complete Guide to Every New Feature

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Key Benefits

  1. Add properties to framework types - No more wrapper classes for simple computed properties
  2. Define operators on existing types - Extend types with custom operators without modifying them
  3. Static extension members - Add static methods/properties to types you don't own
  4. 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));
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Important Notes

  1. Naming conflicts: If you have an existing member named field, use @field or this.field to disambiguate
  2. Backward compatible: Existing code continues to work unchanged
  3. 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();
}
Enter fullscreen mode Exit fullscreen mode

With C# 14

customer?.Order = GetCurrentOrder();
customer?.Total += CalculateIncrement();
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

With C# 14

var name = nameof(List<>);  // Returns "List"
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

With C# 14

TryParse<int> parse = (text, out result) => 
    int.TryParse(text, out result);
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

Limitations

  • params modifier 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Rules

  1. Exactly one defining declaration and one implementing declaration
  2. Implementing declaration can include constructor initializers (this() or base())
  3. Only one partial type can include primary constructor syntax
  4. Partial events must include add and remove accessors

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);
Enter fullscreen mode Exit fullscreen mode

With C# 14

string line = ReadLine();
ProcessKey(line[..5]);  // Implicit conversion

int[] buffer = GetBuffer();
Accumulate(buffer[..8]);  // Implicit conversion
Enter fullscreen mode Exit fullscreen mode

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() and Span<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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Update your project file:
   <PropertyGroup>
     <TargetFramework>net10.0</TargetFramework>
     <LangVersion>14</LangVersion>
   </PropertyGroup>
Enter fullscreen mode Exit fullscreen mode
  1. Install .NET 10 SDK

  2. Gradually adopt features:

    • Start with field keyword 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

Breaking Changes

  1. field keyword conflicts: If you have a member named field, use @field or rename it
  2. Generic inference: Some scenarios may need explicit type arguments
  3. 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


Connect with me on LinkedIn: https://www.linkedin.com/in/vikrant-bagal

Top comments (0)