Introducing C# 14

Wait 5 sec.

C# 14 ships with .NET 10. The highlight is new extension members, but there's a lot more features that make your life as a developer more productive. And, we've added new features that enable some of the performance improvements you can experience in .NET 10. Read on for a tour of all the new features, and find links to dive deeper and start using these features today.[iframe width="800" height="450" src="https://www.youtube.com/embed/xy-HzFp0pbA?si=XjEAI7q2qJm8yT5K" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen]Extension membersExtension members are the headline feature of C# 14. The new syntax is fully compatible with existing extension methods. Extension members enables extension properties, extension operators, and static extension members.The following code shows an example extension block. The extension block contains two instance extensions followed by two static extensions for the same type. The receiver name, source, is optional if the extension only contains static extensions.public static class EnumerableExtensions{ // Instance-style extension members: 'source' is the receiver variable extension(IEnumerable source) { // Extension property public bool IsEmpty => !source.Any(); // Extension method (body elided for brevity) public IEnumerable Where(Func predicate) { // Implementation would filter 'source' throw new NotImplementedException(); } // Static extension property public static IEnumerable Identity => Enumerable.Empty(); // Static user-defined operator provided as an extension public static IEnumerable operator +( IEnumerable left, IEnumerable right) => left.Concat(right); }}Usage examples:int[] data = ...;// access instance extension property:if (data.IsEmpty) { /* ... */ }// Access static extension operator +var combined = data + [ 4, 5 ];// Access static extension property:var empty = IEnumerable.Identity;Because extension blocks are source and binary compatible with existing extension methods, you can migrate one method at a time. Dependent assemblies don't need to be recompiled and continue to bind to the original symbol.You can learn more and explore extension members in the C# Guide and the extension keyword article. You can also read all the details on the feature design in the Extensions proposal.More productivity for youThis set of language features share a common goal: reduce the syntactic friction for everyday tasks so you can focus on domain logic instead of ceremony. They eliminate boilerplate, remove common conditional blocks, simplify lambda declarations, enhance partial types for source generators, and make nameof more expressive in generic scenarios. Individually each saves a few lines and more typing. Together they translate into cleaner code, fewer trivial identifiers, and code that communicates intent more cleanly.The field keywordUnbound generic types and nameofSimple lambda parameters with modifiersNull-conditional assignmentPartial events and constructorsThe field keywordMost properties start life as simple auto‑implemented properties. Later you discover you need small bits of logic — null coalescing, clamping, simple normalization, or raising a guard — on just one accessor. Before C# 14 that requirement forced you to convert to a fully hand‑written backing field pattern:// Beforeprivate string _message = "";public string Message{ get => _message; init => _message = value ?? throw new ArgumentNullException(nameof(value));}The contextual field keyword creates a middle step on that evolution path: keep the auto‑property terseness, inject minimal logic only where needed, and let the compiler synthesize and name the backing storage. You add just the accessor body that needs logic and refer to the compiler‑generated storage via field:// After (C# 14)public string Message{ get; // auto get init => field = value ?? throw new ArgumentNullException(nameof(value));}It's a bridge between auto‑implemented and fully hand‑written properties: start with public string Message { get; init; }, then when you need a quick guard, convert only the accessor that requires code and use field instead of introducing a private member and duplicating a trivial getter. This pattern scales when many properties each require a one‑line check—your class stays visually lightweight and diffs stay small. Another advantage of field is that it avoids creating a new named private field. All code in the type must use the property to access or modify the value of the property.This feature was available as a preview in .NET 9. The field contextual keyword is now generally available in C# 14 (see what's new).Unbound generic types and nameofPreviously, to log or throw using just the generic type name you either hardcoded a string or used a closed constructed type:// Beforevar listTypeName = nameof(List); // "List"// or:const string Expected = "List";Now nameof accepts an unbound generic type. This feature removes the need to pick an arbitrary type argument just to retrieve the generic type's name:// After (C# 14)var listTypeName = nameof(List); // "List"This produces the generic type name once, without implying any specific instantiation. Learn more in the nameof operator reference.Simple lambda parameters with modifiersIn earlier versions, parameter modifiers such as out in delegates required full type annotations on all parameters:// Beforedelegate bool TryParse(string text, out T value);TryParse parse = (string text, out int result) => int.TryParse(text, out result);Now you can keep the concise implicitly typed form while still using modifiers like out, ref, in, scoped on one or more parameters:// After (C# 14)TryParse parse = (text, out result) => int.TryParse(text, out result);The parameter types are still inferred, preserving the concise syntax of the lambda expression. Learn more in the C# language reference section on lambda expression parameter modifiers. It keeps lambdas terse while still exposing flow semantics (out, ref, in, scoped).Null-conditional assignmentGuarded assignments previously required an explicit null check:// Beforeif (customer is not null){ customer.Order = CreateOrder(); customer.Total += CalculateIncrement();}Now you can assign (and use compound assignment) directly with null-conditional operators on the left side of the assignment. The right side is evaluated only when the receiver of the assignment isn't null:// After (C# 14)customer?.Order = CreateOrder();customer?.Total += CalculateIncrement();That trims indentation and visually centers the important work.The feature integrates directly with the existing null-conditional operators so they can appear on the left side of an assignment. It evaluates the right-hand expression only when the receiver isn't null, avoiding helper locals or duplicated checks. See null-conditional assignment and the feature specification.Partial events and constructorsLarge generated or source‑generated partial types can now spread event and constructor logic across files, enabling generators or different files to contribute cleanly:public partial class Widget(int size, string name) // defining declaration of primary ctor{ public partial event EventHandler Changed; // declaring event declaration (field-like)}public partial class Widget{ public partial event EventHandler Changed // Defining declaration for event. { add => _changed += value; remove => _changed -= value; } private EventHandler? _changed; // Implementing declaration can add constructor body logic public Widget { Initialize(); }}This separation enables new source generation scenarios (e.g., a generator supplies the defining members, user code supplies behavior, or vice-versa). It simplifies the manually authored logic. It remains more focused on the algorithms you write by hand.See the programming guide for partial constructors and the partial member reference for syntax details.More performance for your usersMany of the raw throughput wins you'll see after upgrading to .NET 10 come from the runtime and BCL adopting new C# 14 capabilities. Core libraries already use these features so your apps often get faster even if you never write this syntax yourself. The .NET 10 performance improvements post highlights span-heavy parsing, UTF-8 processing, and numeric routines that benefit. Two language additions in particular unlock cleaner, faster library implementations: implicit span conversions and user-defined compound assignment.Implicit span conversionsSpan / ReadOnlySpan are central to allocation-free APIs. C# 14 adds implicit conversions among arrays, spans, and read-only spans so you write less ceremony and the JIT sees simpler call graphs. That translates into fewer temporary variables, fewer bounds checks, and more aggressive inlining in the framework (as described in the performance blog's sections covering text and parsing micro-benchmarks).Earlier C# versions required code like the following:// Beforestring line = ReadLine();ReadOnlySpan key = line.AsSpan(0, 5); // explicit AsSpanProcessKey(key);int[] buffer = GetBuffer();Span head = new(buffer, 0, 8); // explicit Span ctorAccumulate(head);Now, you can write the following:// After (C# 14)string line = ReadLine();ProcessKey(line[..5]); // substring slice implicitly convertsint[] buffer = GetBuffer();Accumulate(buffer[..8]);Library authors exploit these conversions to remove helper locals and express slice intent inline. The benefits include fewer explicit AsSpan or constructor calls, clearer slicing intent that encourages span-friendly overloads, and framework optimizations that reduce allocations through broader zero-allocation paths. Learn more by reading the first-class span types spec.User defined compound assignmentHigh-performance numeric and vector types often accumulate values in tight loops. Without a dedicated compound assignment operator, code either repeated the left hand reference or created intermediate temporaries through an ordinary binary operator—both patterns can inhibit certain JIT optimizations. C# 14 lets you declare a compound assignment operator (+=, -=, etc.) explicitly so the compiler dispatches directly to your implementation. Libraries taking advantage of this (for example, SIMD-friendly helpers referenced in the performance blog) to avoid extra temporaries and can expose more idiomatic APIs.Instead of this:// BeforeBigVector sum = BigVector.Zero;foreach (var v in values){ sum = sum.Add(v); // intermediate result each iteration}After you provide a compound operator that can update the result in-place:// After (C# 14)BigVector sum = BigVector.Zero;foreach (var v in values){ sum += v; // calls user-defined operator += directly}Defining both the binary and compound operators: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; }}Details appear in the operator overloading article in the C# guide and the compound assignment spec. You should also consult the compiler breaking changes article for potential issues. You might encounter issues regarding Enumerable.Reverse.SummaryThat's a quick tour of what we've delivered in C# 14: new extensions, a number of features that make you more productive, and enhancements that make your C# programs perform better. Download .NET 10 and try it on your apps. Participate in ongoing discussions to continue to make C# a great language choice for you.