Getting Started: C#
Get started with programming using the C# programming language
Each new .NET release introduces updates to C#, and .NET 10 is no exception. C# 14 includes a set of features designed to boost productivity, improve code readability, and enhance performance. Knowing what these new features are and when to use them will help you write more robust and maintainable code.
This guide covers every new C# 14 feature, explains its benefits, and provides practical code examples to help you navigate where and how you can use each one. These features include:
field keywordref, in, and out parameters in lambdas without explicit parameter typesnameof for unbounded genericsSpan<T>Let’s dive in!
field KeywordC# 14 introduces the field keyword, which allows you to access an auto-implemented property’s hidden backing field directly in your property’s get and set methods. This new feature frees you from creating manual backing fields for your properties.
When working with simple data values in classes, auto-implemented properties simplify configuring read and write functionality for a value:
public string Name { get; set; }
 The syntax is concise and readable. However, its simplicity falls apart as soon as you need additional logic inside the getter or setter. You’re no longer able to take advantage of auto-implemented properties and must manually define a private backing, hindering readability:
private string _name;
public string Name
{
    get => _name;
    set => _name = value ?? throw new ArgumentNullException(nameof(value));
}
 The private _name field in the snippet above stores the property’s underlying value. When setting this property, the value is first validated before being assigned to the field.
With the new field keyword, you can inject additional logic into your getter and setter methods without having to create your own backing field. You retain the readability of auto-implemented properties while customizing their accessors.
public string Name
{
    // Use `field` to access the underlying field directly.
    get => field;
    set => field = value ?? throw new ArgumentNullException(nameof(value));
}
 Notice how the snippet above includes validation in the set method. However, instead of using a manual backing field, it uses the field keyword.
The field keyword is contextual, meaning it's only available inside the get and set methods of your auto-implemented properties. You won’t be able to access a property’s field from other properties, constructors, or methods. Instead, you’ll have to use the property accessor itself.
A small caveat is that if your class already has a member called field, that member will conflict with the new keyword. It’s best to avoid using the keyword as a name altogether, since in such cases, the new field keyword will take precedence and shadow the class member.
Extension blocks are a new feature in C# 14 that group all your extension members for a type into a single block. Besides organizing your extensions neatly, extension blocks also allow you to define more than just extension methods, such as extension properties, methods, static methods, and operator overloads for other types.
Extension methods have been around since C# 3.0. They let you extend a class’s functionality using a static method and the this keyword:
public static class EnumerableExtensions
{
    public static T FirstOrFallback(this IEnumerable<T> enumerable, T fallback)
        => enumerable.FirstOrDefault() ?? fallback;
}
 The extension method can be called on any compatible type like this:
var value = upcomingOrders.FirstOrFallback("No upcoming order");
 Unfortunately, the old extension method syntax only allows you to write methods. Extending classes with other members, such as properties and operators, isn’t possible in C# 13 and earlier.
In C# 14, extension blocks give you the freedom to define any extension property, method, static method, or operator for a type without modifying the type directly. For example, the members in the snippet below extend the IEnumerable<T> type with a new property, method, static method, and operator overload:
public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T> enumerable)
    {
        // Extension property
        public bool IsEmpty => !enumerable.Any();
        // Extension method
        public T FirstOrFallback(T fallback)
            => enumerable.FirstOrDefault() ?? fallback;
        
        // Extension static method
        public static IEnumerable<T> Range(int start, int count, Func<int, T> generator)
            => Enumerable.Range(start, count).Select((_, i) => generator(i));
        
        // Extension operator method
        public static IEnumerable<T> operator +(IEnumerable<T> first, IEnumerable<T> second)
        {
            foreach (var item in first)
                yield return item;
            foreach (var item in second)
                yield return item;
        }
    }
}
 All of these extension members can be used as if they were defined on the underlying class itself.
// Check if list is empty using extension property
var emptyList = new List<string>();
if (emptyList.IsEmpty)
    // ...
// Get the first value or fallback to a default value using the extension method
var upcomingOrders = GetUpcomingOrders();
var value = upcomingOrders.FirstOrFallback("No upcoming order");
// Concatenate two enumerables into a single one using the operator extension
var listA = new[] {1, 2};
var listB = new[] {3, 4};
var combinedList = listA + listB;
// Generate a range of values using the static extension method
var generatedIds = IEnumerable<string>.Range(10000, 11000, i => $"MSC-{i}");
 Be cautious of conflicting signatures when writing extension members. If your extension member matches the signature of a method on the underlying type, the type’s method will take precedence.
Partial members let you split the definition and declaration of a class's members, which is especially useful when using source generators. C# 14 expands support for partial members to constructors and events, reducing manual boilerplate code.
Partial types and members are not new to C#. However, only partial methods, properties, and indexers are supported before C# 14:
// Config.cs
public partial class Config
{
    public partial void ReloadConfig();
}
// Config.g.cs
public partial class Config
{
    public partial void ReloadConfig()
    {
        // Generated code to reload configuration
    }
}
 Unfortunately, because constructors couldn’t be made partial, the only way to execute generated code in them is by calling a generated partial method. This is boilerplate code that you, as the developer, must remember to implement:
// Config.cs
public partial class Config
{
    public Config(string configPath)
    {
        // Developer explicitly calls the partial method to load the configuration
        Initialize(configPath);
    }
    
    // Declare partial method to be generated in Config.g.cs
    private partial void Initialize(string configPath);
}
// Config.g.cs
public partial class Config
{
    private partial void Initialize(string configPath)
    {
        // Generated code
    }
}
 Another limitation in versions of C# before C# 14 prevents you from defining partial events. If an event accessor has to call any generated code, you must define the event accessor methods and call the generated method:
// Config.cs
public partial class Config
{
    private EventHandler? _configReloaded;
    public event EventHandler ConfigReloaded
    {
        add
        {
            // Call generated method
            OnConfigReloadedAdded(value);
            _configReloaded += value;
        }
        remove
        {
            // Call generated method
            OnConfigReloadedRemoved(value);
            _configReloaded -= value;
        }
    }
    
    private partial void OnConfigReloadedAdded(EventHandler handler);
    private partial void OnConfigReloadedRemoved(EventHandler handler);
}
// Config.g.cs
public partial class Config
{
    private partial void OnConfigReloadedAdded(EventHandler handler)
    {
        // Generated code...
    }
    private partial void OnConfigReloadedRemoved(EventHandler handler)
    {
        // Generated code...
    }
}
 Starting with C# 14, partial constructors and events are now supported, reducing the boilerplate required when working with events and constructors in partial classes.
Using this new feature, you can simplify the snippets above to two lines of code:
// Config.cs
public partial class Config
{
    public partial event EventHandler ConfigReloaded;
    public partial Config(string configPath);
}
// Config.g.cs
public partial class Config
{
    // Implementation for partial event
    private EventHandler? _configReloaded;
    public partial event EventHandler ConfigReloaded
    {
        add
        {
            // Generated code
            _configReloaded += value;
        }
        remove
        {
            // Generated code
            _configReloaded -= value;
        }
    }
    
    // Implementation for partial constructor
    public partial Config(string configPath)
    {
        // Generated code
    }
}
 Keep in mind that partial constructors cannot be static and must always have a declaration and an implementation specified. Similarly, partial events don’t support auto-implemented accessors, so you must always include an implementation that has the add and remove accessor.
If your code makes use of source generators, these new partial members can clean up areas in your code.
In C# 14, the null-conditional operator (?.) now works on the left-hand side of value assignments. This frees you from writing verbose conditional checks to ensure the variable you are assigning to is not null.
A common approach in earlier C# versions is first to check whether an object is null before accessing or updating its members:
if (policy is not null)
{
    policy.CustomerId = customerId;
}
 Without the if statement above, a NullReferenceException will be thrown if policy is null. Such a conditional statement is the only solution to this problem in C# 13 and earlier. However, C# 14 brings a welcome change to simplify this pattern.
Because the pattern above is so common in many codebases, C# 14 lets you use the existing null-conditional operator on the left-hand side of an assignment. You can simplify the snippet above to:
policy?.CustomerId = customerId;
 If policy is null, then the assignment is skipped without throwing any exceptions. When skipping the assignment, the right-hand side of the assignment is not executed, preventing any side effects from occurring.
There are some limitations to this syntax. Firstly, it does not support certain unary operators, such as ++ and --. You’re also not able to use this syntax with destructors.
More Readable Null Checks: The syntax simplifies assignments to nested properties by allowing safer assignments without null checks. By keeping the null check and assignment in a single line, the code becomes cleaner and easier to understand.
In C# 14, you can now implement operator overloads for compound assignment operators like +=, -=, etc. These additional operator overloads let you fine-tune the performance of compound assignments on your custom types.
C# already supports overloading operators such as + and - on your own types:
public struct Money(string currency, decimal amount)
{
    public decimal Amount { get; set; } = amount;
    public string Currency { get; set; } = currency;
    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency)
            throw new InvalidOperationException("Cannot add amounts with different currencies.");
        
        return new Money(a.Currency, a.Amount + b.Amount);
    }
    public override string ToString() => $"{Amount} {Currency}";
}
 C# then uses the operator overload for normal operations and compound assignments:
var cheque = new Money("USD", 200);
var savings = new Money("USD", 100);
// Uses the + operator overload
var total = cheque + savings;
// Also uses the + operator overload and creates a new Money instance
total += new Money("USD", 10);
 While sharing the overload for binary and compound operations is convenient, it’s not the most efficient. Compound operations always assign the result back to the first operand. Since the current operator overloads always return new types, these operations cause unnecessary work, such as value copying. In earlier versions of C#, there’s no way to address these performance issues.
You can now provide explicit compound assignment operator overloads:
public struct Money(string currency, decimal amount)
{
    // …
    public void operator += (Money b)
    {
        if (this.Currency != b.Currency)
            throw new InvalidOperationException("Cannot add amounts with different currencies.");
        
        this.Amount += b.Amount;
    }
}
 Notice how the += overload doesn’t create an additional Money object to store the result. Instead, it updates the current instance directly. This is more efficient as it prevents unnecessary memory allocations.
When implementing compound operator overloads for your own types, make sure the final result does not differ from the one produced by the binary operator. Otherwise, the differing results will confuse developers and lead to subtle bugs.
Lambdas now support ref, in, out, scoped, and ref readonly parameter modifiers in C# 14 without needing to specify parameter types. This small yet useful enhancement makes writing high-performance lambdas easier and improves their readability.
When writing lambdas, C# tries to infer the parameter types so that they don’t have to be explicitly stated. However, if you want to use the ref, in, or out parameter modifier for any parameters in C# 13 or earlier, you must also include the parameter type:
delegate void ApplyDiscount(ref decimal money);
// Lambda with explicit parameter type marked as ref
ApplyDiscount apply10PercentDiscount = (ref decimal price) =>
{
    price *= 0.9m;
};
 C# 14 brings a slight quality-of-life improvement that lets you omit the parameter type when using parameter modifiers, provided the type can be inferred:
// Lambda with omitted parameter type
ApplyDiscount apply20PercentDiscount = (ref price) => 
{
    price *= 0.8m;
}
 Notice how you have to define a custom delegate to use these parameter modifiers. This custom delegate is necessary because the standard Action<> and Func<> delegates don’t support these modifiers.
Also, keep in mind that if you use the params modifier, you will still have to specify the parameter type.
Readability: You can now write performance-critical lambda definitions more concisely, improving readability. The syntax for these lambdas is also more consistent with standard lambda functions.
nameof for Unbound GenericsC# 14 now lets you use the nameof operator on unbounded generics. These are generic types that don’t have a specified type argument, like List<>.
In C# 13 and prior, the nameof operator only supports bounded generics. If you want to get the name of a generic type, you have to specify a random type parameter for it to compile and run:
// Using string as an arbitrary type parameter
Console.WriteLine(nameof(List<string>));
// Output: List
 The snippet above illustrates how this approach makes logging and diagnostics cumbersome. You have to specify a type parameter for the generic type, even though it’s never output by nameof.
This quality-of-life improvement in C# 14 now lets you get the name of the type without a type parameter:
// No type parameter (unbounded generic)
Console.WriteLine(nameof(List<>));
// Output: List
 If you want more information about the generic type, use the typeof operator, which provides more detailed information about the type, such as its assembly, namespace, and properties.
Readability: Specifying an arbitrary type parameter to obtain the name of a generic class can be confusing. Being able to retrieve the name of a generic class without specifying a type can greatly enhance readability.
Span<T> Usage with Implicit ConversionsThe Span<T> and ReadOnlySpan<T> were introduced in C# to manage collections in a memory-efficient manner. In a push to improve the developer experience when working with these types in C#, the latest version comes with additional implicit operators that can convert various types into Span<T> and ReadOnlySpan<T>.
To demonstrate how some of these new implicit conversions have been implemented, consider the method below, which accepts a ReadOnlySpan<decimal> and calculates the standard deviation:
public T CalculateStandardDeviation<T>(ReadOnlySpan<T> values) where T : INumber<T>
{
    // Logic for calculation
}
 In versions before C# 14, you need to explicitly specify the type of the elements in the array of values when calling the method, like this:
// Array is converted to ReadOnlySpan using explicit type
var sd = CalculateStandardDeviation<int>(new[] { 3, 5, 3 });
 The int type hint tells the compiler to convert this array of integers into a span of them. Failure to do so results in a compile-time error:

With C# 14, new implicit converters make it possible to infer the type to use when converting to ReadOnlySpan<T>, meaning you can omit the explicit type completely:
// Array is implicitly converted to ReadOnlySpan<int>
var sd = CalculateStandardDeviation(new[] { 3, 5, 3 });
 There may still be scenarios where the implicit conversions won’t pick up the correct underlying type. However, you’re far less likely to run into these issues in C# 14.
Span<> and its related types in C#, allowing developers to use them more naturally without needing explicit conversions.C# 14 introduces a range of quality-of-life updates and performance improvements. Most of the features aim to improve developer productivity by reducing boilerplate code. However, adopting a feature like user-defined compound assignments can also lead to real performance gains.
Implementing these new features in your codebase isn’t hard. First, update your projects to use .NET 10. This step might involve a few breaking changes, but they’re generally easy to fix. Once that’s done, you can start using these new features in your code.
If you’re interested in learning more about C# and some of the features highlighted in this article, be sure to check out the courses below.
© 2025 Dometrain. All rights reserved.