Extension Members: C# 14 Breaks the ‘this’ Barrier
For over 15 years, extension methods have been one of C#‘s most beloved features. But they’ve always had a frustrating limitation in that you can only add methods. Want an extension property? An extension operator? A static helper that feels like it belongs on the type? Sorry, best we can do is another this parameter.
C# 14 changes that. The new extension block syntax lets you add properties, operators, and static members to types you don’t own.
The Old Way
We’ve all written utility classes like this:
public static class StringExtensions{ public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s);
public static string Truncate(this string s, int maxLength) => s.Length <= maxLength ? s : s[..maxLength];}It works, but IsNullOrEmpty should be a property. There’s no argument, no verb, it’s clearly a state check. And if you wanted a static helper like string.Join but custom? Impossible through extensions.
The New Way
C# 14 introduces the extension block. You declare it inside a static class, specify the receiver type, and then define members as if you were writing them inside the type itself:
public static class StringExtensions{ extension(string? s) { // Extension property — no more parentheses for simple checks public bool IsNullOrEmpty => string.IsNullOrEmpty(s);
// Extension method — works just like before, cleaner grouping public string Truncate(int maxLength) => s is null || s.Length <= maxLength ? s! : s[..maxLength]; }}Now you call IsNullOrEmpty the way it always should have been:
string? name = GetName();
if (name.IsNullOrEmpty) Console.WriteLine("No name provided");
string preview = name.Truncate(50);No parentheses, no “is this a method or a property?” confusion. It just reads like a member.
Static Extension Members
Want to add a factory method or constant that lives on the type itself? Use the parameterless receiver syntax:
public static class GuidExtensions{ extension(Guid) { public static Guid CreateFrom(string input) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input)); return new Guid(hash.AsSpan(0, 16)); } }}Now it’s called like a real static member:
No GuidHelper.CreateFrom(...). No hunting for which utility class has the method. It’s right where you’d expect it.
Generics Work Too
Extension blocks support generic type parameters and constraints:
public static class EnumerableExtensions{ extension<T>(IEnumerable<T> source) { public bool IsEmpty => !source.Any();
public IEnumerable<T> WhereGreaterThan(T value) where T : IComparable<T> => source.Where(x => x.CompareTo(value) > 0); }}var numbers = new List<int> { 1, 2, 3, 4, 5 };
if (!numbers.IsEmpty){ var big = numbers.WhereGreaterThan(3); // [4, 5]}What You Can’t Do (Yet)
- No backing fields - extension properties are computed only, no stored state.
- No constructors or events - you’re extending the surface, not the internals.
- Old-style extensions still work - the classic
thisparameter syntax isn’t going anywhere. You can mix both in the same project.
The Bottom Line
Extension members take the most-used workaround in C# and make them real. Properties feel like properties. Static helpers live on the type. Your code reads more naturally, and your consumers don’t need to know the difference.
If you maintain a library or a shared “Extensions” folder, C# 14 just made it a lot more expressive.