You've been writing extension methods in C# for over fifteen years. The this parameter trick has served well enough, but it always felt like a workaround rather than a language feature. You couldn't add properties. You couldn't define operators. You couldn't attach static members to a type you didn't own. Every time you reached for one of those, you'd end up with a helper method that broke the natural API surface you were trying to create.
C# 14 changes this with extension members -- a new syntax that lets you define extension properties, extension operators, and static extension members alongside the extension methods you already know. The old syntax still works. Nothing breaks. But the new approach opens doors that were firmly closed before.
The extension block syntax
The core of the new feature is the extension block. Instead of scattering this-parameter methods across a static class, you group related extensions together by receiver type:
public static class OrderExtensions
{
extension(Order order)
{
public decimal TotalWithVat => order.Total * 1.2m;
public bool IsHighValue => order.Total > 500m;
public string Summary()
=> $"Order {order.Id}: {order.Items.Count} items, {order.Total:C}";
}
}
The extension keyword introduces a block scoped to a specific receiver type. Inside, members reference the receiver by name -- order in this case -- rather than accepting it as a parameter. The result reads like a natural part of the type:
var report = orders
.Where(o => o.IsHighValue)
.Select(o => new { o.Summary(), o.TotalWithVat });
No parentheses on IsHighValue. No parentheses on TotalWithVat. These are properties now, not methods pretending to be properties.
Extension properties
Extension properties are the headline feature. Before C# 14, every extension that looked up or computed a value had to be a method with empty parentheses -- order.GetTotal(), customer.IsActive(), sequence.IsEmpty(). This always felt wrong when the operation was a simple, cheap read with no side effects.
Now you can write extensions that follow the same conventions as regular properties:
public static class StringExtensions
{
extension(string s)
{
public bool IsNullOrEmpty => string.IsNullOrEmpty(s);
public int WordCount
=> s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
public string Reversed => new string(s.Reverse().ToArray());
}
}
Consumers use these exactly as they would any other property:
var input = " hello world ";
if (!input.IsNullOrEmpty)
{
Console.WriteLine($"{input.WordCount} words");
Console.WriteLine(input.Reversed);
}
// TIP
Extension properties only support get accessors. You cannot define a set accessor because extension members cannot add backing fields to the extended type. If you need mutable state, you still need a regular method that takes the new value as a parameter.
Static extension members
Traditional extension methods could only extend instances. If you wanted to add a factory method or a well-known constant to a type, you were out of luck -- you'd put it on a separate helper class, and the discoverability vanished.
C# 14 introduces static extension members. The syntax uses an extension block without a parameter name:
public static class ResultExtensions
{
extension(Result)
{
public static Result Success => new(true, null);
public static Result Failure(string message) => new(false, message);
}
}
Now Result.Success and Result.Failure("something went wrong") appear as if they were defined on Result itself. This is particularly useful for types you don't own -- adding factory methods, sentinel values, or well-known instances to third-party or framework types.
A more practical example: extending HttpStatusCode with grouped helpers:
public static class HttpStatusCodeExtensions
{
extension(HttpStatusCode code)
{
public bool IsSuccess => (int)code >= 200 && (int)code < 300;
public bool IsClientError => (int)code >= 400 && (int)code < 500;
public bool IsServerError => (int)code >= 500 && (int)code < 600;
}
extension(HttpStatusCode)
{
public static HttpStatusCode[] SuccessCodes =>
[
HttpStatusCode.OK,
HttpStatusCode.Created,
HttpStatusCode.Accepted,
HttpStatusCode.NoContent
];
}
}
The first block extends instances (the parameter code is named), the second extends the type itself (no parameter name). A single static class can contain multiple extension blocks for the same type.
Extension operators
This is where extension members get genuinely powerful. Before C# 14, if you wanted operator support on a type you didn't own, you had no option. You could wrap it in your own type, or you could write verbose method calls. Neither was satisfying.
Now you can define operators directly:
public static class MoneyExtensions
{
extension(Money)
{
public static Money operator +(Money left, Money right)
{
if (left.Currency != right.Currency)
throw new InvalidOperationException(
$"Cannot add {left.Currency} to {right.Currency}");
return new Money(left.Amount + right.Amount, left.Currency);
}
public static Money operator -(Money left, Money right)
{
if (left.Currency != right.Currency)
throw new InvalidOperationException(
$"Cannot subtract {right.Currency} from {left.Currency}");
return new Money(left.Amount - right.Amount, left.Currency);
}
public static Money operator *(Money money, decimal factor)
=> new Money(money.Amount * factor, money.Currency);
}
}
Now arithmetic with Money reads naturally:
var subtotal = lineItems.Aggregate(Money.Zero, (sum, item) => sum + item.Price);
var vat = subtotal * 0.2m;
var total = subtotal + vat;
// WARNING
Extension operators follow the same resolution rules as regular operators. If the type later adds its own + operator, that definition will take precedence over your extension. Design your extensions with the assumption that the type owner could add competing members in a future release.
Generics and constraints
Extension blocks fully support generic type parameters with constraints:
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public T? SecondOrDefault => source.Skip(1).FirstOrDefault();
}
extension<T>(IEnumerable<T> source) where T : INumber<T>
{
public T Sum => source.Aggregate(T.Zero, (acc, x) => acc + x);
public T Average
{
get
{
var count = T.Zero;
var total = T.Zero;
foreach (var item in source)
{
total += item;
count++;
}
return total / count;
}
}
}
}
The generic type parameters sit between the extension keyword and the receiver declaration. You can have multiple extension blocks for the same base type with different constraints, just as you would with separate static classes today.
Migrating from traditional extension methods
The old this-parameter syntax isn't going anywhere. Both forms are binary and source compatible, and they can coexist in the same static class. But if you're tidying up or adding new members, the new syntax has some advantages:
Before (traditional):
public static class DateTimeExtensions
{
public static bool IsWeekend(this DateTime date)
=> date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
public static DateTime StartOfDay(this DateTime date)
=> date.Date;
public static DateTime EndOfDay(this DateTime date)
=> date.Date.AddDays(1).AddTicks(-1);
}
After (extension members):
public static class DateTimeExtensions
{
extension(DateTime date)
{
public bool IsWeekend
=> date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
public DateTime StartOfDay => date.Date;
public DateTime EndOfDay => date.Date.AddDays(1).AddTicks(-1);
}
}
Notice that IsWeekend, StartOfDay, and EndOfDay all became properties. They were always property-shaped -- no parameters, no side effects, cheap to compute -- but the old syntax forced them into method form. The migration isn't just a syntax swap; it's a chance to express intent more accurately.
// NOTE
You don't need to migrate anything. The old and new forms produce compatible IL. Migration makes sense when you're already touching the code or when you want to add properties or operators alongside existing methods.
How the compiler lowers extension members
Under the hood, extension blocks compile down to static methods -- exactly like traditional extension methods. An extension property such as IsWeekend becomes a get_IsWeekend static method with the receiver as its only parameter. Extension operators lower to the standard op_Addition, op_Subtraction pattern.
This matters for two reasons. First, there's no runtime overhead. Extension members aren't a new CLR feature; they're a compiler convenience. Second, you can still call the lowered form directly if you need to disambiguate:
// Normal usage
var isWeekend = myDate.IsWeekend;
// Explicit disambiguation if needed
var isWeekend = DateTimeExtensions.get_IsWeekend(myDate);
This follows the same pattern you've always used when two extension methods from different namespaces collide.
What you still can't do
Extension members are a big step forward, but they have clear boundaries:
- No backing fields. You cannot add stored state to a type through extensions. Properties must be computed. Auto-properties with
get; set;won't compile because they require a backing field. - No events, indexers, or constructors. The
extensionblock supports methods, properties, and operators. Fields, events, indexers, nested types, and constructors are out of scope. - No
refreceiver in extension blocks. If you have aref thisextension method on a struct (to mutate it in place), you can use the new syntax with a named receiver, but therefsemantics carry over from how the receiver is declared in the block. - Type parameter ordering constraints. In the lowered form, receiver type parameters come before method type parameters. If your existing extension method has them in a different order, it can't be ported to the new syntax. Keep using the traditional form for those cases.
// This won't compile -- extension properties can't have setters
extension(Order order)
{
// Bad -- requires a backing field
public string Notes { get; set; }
}
Common pitfalls
Forgetting the parameter name for instance extensions. If you write extension(Order) without naming the parameter, you get a static extension block. Members inside won't have access to an instance. If you intended instance members, add a name: extension(Order order).
Mixing static and instance members in one block. A single extension block is either instance or static, not both. If you need both, declare two blocks:
public static class OrderExtensions
{
// Instance extensions
extension(Order order)
{
public decimal TotalWithVat => order.Total * 1.2m;
}
// Static extensions
extension(Order)
{
public static Order Empty => new();
}
}
Assuming extension operators always win. If the type's author later adds an operator that matches your extension operator's signature, the type's own operator takes precedence. Your extension silently stops being called. Consider this before relying on extension operators for core logic in a library you ship to others.
Over-migrating. Not every extension method benefits from the new syntax. A method with three parameters and complex logic is still a method. The new syntax shines for property-shaped members, operators, and static factories. Don't rewrite everything just because you can.
Summary
- Extension blocks (
extension(Type name) { ... }) group related extension members by receiver type. - Extension properties let you define
get-only properties on types you don't own, replacing awkward zero-parameter methods. - Static extension members (
extension(Type) { ... }without a name) attach static properties and methods to a type, improving discoverability. - Extension operators bring
+,-,*, and other operators to types that didn't have them. - Generics work as expected, with type parameters and constraints on the extension block.
- The old
this-parameter syntax still works and remains binary compatible. Migrate when it makes your API clearer, not for the sake of it. - Extension members compile to the same static methods under the hood -- no runtime cost, no new CLR features required.