Extension methods have been in C# since version 3: a static method with a this parameter that you can call as if it belonged to the type. C# 14 generalises the idea. An extension block can declare methods, properties, static members and operators against a type, and it groups them under one receiver so you write the type once. The old syntax still works; the new one covers the cases it never could.
The old way, for reference#
public static class StringExtensions
{
public static bool IsBlank(this string? s) => string.IsNullOrWhiteSpace(s);
}
" ".IsBlank(); // true
A static method in a static class, with this on the first parameter. It could only ever be a method, and the receiver was repeated on every member.
The extension block#
public static class StringExtensions
{
extension(string? s) // the receiver, named once
{
public bool IsBlank => string.IsNullOrWhiteSpace(s); // a property
public string OrDefault(string fallback) => IsBlank ? fallback : s!; // a method
public int WordCount => IsBlank ? 0 : s!.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
}
" ".IsBlank; // true - no brackets: it is a property
"hello world".WordCount; // 2
name.OrDefault("anonymous");
Inside the block, the receiver name, s, is available to every member. Members are declared as if they were instance members of the type. The containing class must still be static and non-nested, as before.
Extension properties#
This is the feature most requested for fifteen years. A computed value that reads naturally as a property rather than a method call:
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public T? FirstOrNull => source.FirstOrDefault();
}
}
if (orders.IsEmpty) { ... }
Extension properties are get-only or get-and-set through accessors that call your code; there is no storage. They cannot add a field to the type, because the type’s layout is fixed. A setter on an extension property has to write somewhere you provide.
Generic receivers#
The block can be generic, with type parameters declared on the block and inferred at the call site, exactly as generic extension methods were:
extension<TKey, TValue>(Dictionary<TKey, TValue> dict) where TKey : notnull
{
public TValue GetOr(TKey key, TValue fallback) => dict.TryGetValue(key, out var v) ? v : fallback;
}
Static extension members#
Leave the receiver unnamed and members become static, callable on the type itself:
public static class DateOnlyExtensions
{
extension(DateOnly)
{
public static DateOnly Today => DateOnly.FromDateTime(DateTime.Today);
public static DateOnly Parse(string s, string format) => DateOnly.ParseExact(s, format);
}
}
var d = DateOnly.Today; // as if the type had always had it
Useful for factory methods and constants on types you cannot modify.
Extension operators#
public static class VectorExtensions
{
extension((double X, double Y))
{
public static (double X, double Y) operator +((double X, double Y) a, (double X, double Y) b)
=> (a.X + b.X, a.Y + b.Y);
}
}
var sum = (1.0, 2.0) + (3.0, 4.0); // (4, 6)
Operators are declared as static members inside the block. This was impossible before: operators had to be defined inside the type.
How resolution works#
- A real instance member on the type always wins over an extension member with the same name. Extensions fill gaps; they cannot override.
- Extension members are found the same way extension methods were: the containing static class must be in scope, via
usingthe namespace. - Two extensions in scope with the same name and receiver produce an ambiguity error at the call site, as before.
- The block form and the
this-parameter form are interchangeable for methods: the compiler generates the same static method, so existing callers, reflection and other languages see no difference.
Old syntax or new#
| Situation | Use |
|---|---|
| One or two methods on a type | Either; the old form is shorter |
| Several members sharing a receiver | Block: the receiver is written once |
| A property, static member or operator | Block: the old form cannot do it |
| A library that must compile on older SDKs | Old form |
There is no need to convert existing extension methods. Mixing both forms in one static class is allowed.
Questions people ask#
Does this need .NET 10?
It needs the C# 14 compiler, which ships with the .NET 10 SDK. With LangVersion set to 14 it compiles for older target frameworks too, since the generated code is ordinary static methods.
Can extension members be private or internal?
They follow the normal accessibility rules of the containing static class and the member itself. Internal extensions for a library’s own use are common.
Can I extend an interface?
Yes, as with extension methods. All implementers get the members.
Do extension members show up in IntelliSense?
Yes, once the namespace is imported, marked with the extension icon as methods always were.
Where to go next#
- What’s new in C# 14 and .NET 10 — the rest of the release.
- C# generics — for the generic receivers.
- C# advanced language features — the wider toolkit.