Extension methods arrived in C# 3.0, mostly to make LINQ possible. For almost twenty years they stayed the same: static methods with a this parameter, instance methods only, no properties, nothing on the type itself.

C# 14, shipped with .NET 10, changes that with extension members: extension properties, static extension members and operators, grouped in extension blocks.

From this parameters to extension blocks

The classic way:

public static class StringExtensions { public static bool IsValidEmail(this string email) => Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"); } 

The C# 14 way: the block declares the receiver once, and every member inside uses it.

public static class StringExtensions { extension(string str) { public bool IsValidEmail => Regex.IsMatch(str, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"); public int WordCount => str.Split(new[] { ' ', '\t', '\n' }, StringSplitOptions.RemoveEmptyEntries).Length; } } 
Console.WriteLine("mario@example.com".IsValidEmail); // True 

The block still lives inside a static class, so classic this methods and the new members can live side by side, and you can migrate gradually.

Extension properties

This is the feature people asked for most. An Order that comes from somewhere else can get computed properties that read like its own:

public static class OrderExtensions { extension(Order order) { public decimal TotalAmount => order.Items.Sum(i => i.Price * i.Quantity); public bool IsShippable => order.Status == OrderStatus.Processing && order.TotalAmount > 0; } } 
Console.WriteLine($"Total: {order.TotalAmount:C}, shippable: {order.IsShippable}"); 

Before C# 14 the same code read order.GetTotalAmount() and order.GetIsShippable().

Static extension members

A block without a parameter name extends the type itself:

public record Currency(string Code, string Name, string Symbol); public static class CurrencyExtensions { extension(Currency) { public static Currency EUR => new("EUR", "Euro", "€"); public static Currency USD => new("USD", "US Dollar", "$"); } extension(Currency currency) { public string FormatAmount(decimal amount) => $"{currency.Symbol}{amount:N2}"; } } 
var price = Currency.EUR.FormatAmount(1234.56m); // €1,234.56 with the en-US culture 

It works on interfaces too, so something like IConfiguration.CreateDevelopment() becomes possible.

Operators

You can also add operators to a type you can't change:

public static class MoneyExtensions { extension(Money) { public static Money operator +(Money left, Money right) => left.Currency == right.Currency ? new Money(left.Amount + right.Amount, left.Currency) : throw new InvalidOperationException("Different currencies"); } } 

What you still can't do

  • No state: a block can't declare fields or auto-implemented properties. A cache has to live in the enclosing static class.
  • No events.
  • No indexers in C# 14: the .NET 10 compiler only offers them as a preview feature.
  • No virtual dispatch: like classic extension methods, everything is resolved at compile time.

A few rules

  • Use extension properties for cheap, side-effect-free values, and methods for actions. A property that hides expensive work surprises the caller.
  • Group blocks by domain (order validation, order formatting), not by type.
  • Migrate gradually: existing this methods keep working next to the new blocks.

Want the full story?

This is the short version. The complete C# Insights article goes further: how extension methods evolved since C# 3.0, more e-commerce and configuration examples, setters on extension properties, performance and caching, naming conventions, a migration plan with a Roslyn analyzer, and what may come next.

Read it here: C# 14 Extension Members Guide: Static and Instance Properties in .NET 10.

Thanks for reading.