Extension Members in C# 14
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:
var userId = Guid.CreateFrom("user@example.com");
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 this parameter syntax isn't going anywhere. You can mix both in the same project.
Conclusion
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.