Introduced in C# 3.0, extension methods are a valuable feature for external types, especially when those types are sealed, such as string.
Roughly two decades later, C# has now finally unveiled support for extending everything.
With the release of .NET 10 Preview 3 (C# 14), it is now possible to define static methods, instance properties, and static properties too. Support for other members will be incorporated in future releases.
Syntactically, an extension method should be defined within a top-level static class. The type of its first mandatory parameter, the one qualified with the this keyword, determines the type being extended. Henceforth, this will be referred to as the receiver type.
All standard query operators of LINQ are defined as extension methods. They are defined in the Enumerable static class within the BCL (in the System.Linq namespace).
For example, the Where extension method applies to all types that implement IEnumerable<T>. Validations and optimizations aside, the typical implementation is as outlined below.
namespace System.Linq;
public static class Enumerable
{
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
foreach (var element in source)
{
if (predicate(element))
{
yield return element;
}
}
}
}
Usage:
int[] values = [1, 2, 3, 4, 5];
var evens = values.Where(x => x % 2 == 0); // [2, 4]
// Method call as resolved by the compiler
// The Func delegate will be converted into a closure; however, that is a separate topic.
//IEnumerable<int> evens = Enumerable.Where(values, x => x % 2 == 0);
In this example, values is an integer array. The Array type implements IEnumerable<T>. Thus, it behaves as though Where is defined as an instance method within the Array type.
In case of naming conflicts, the extension method can be invoked directly (Enumerable.Where()). This is akin to how the compiler translates it internally.
The notation of using the receiver type as the method parameter prevented its extension to properties.
Extension Members:
After several efforts, the C# design team has successfully addressed this issue through the introduction of the extension block.
The same Where extension method in new syntax, here extension is a contextual keyword.
namespace System.Linq;
public static class Enumerable
{
// Extension block
extension<TSource>(IEnumerable<TSource> source)
{
public IEnumerable<TSource> Where(Func<TSource, bool> predicate)
{
foreach (var element in source)
{
if (predicate(element))
{
yield return element;
}
}
}
}
}
Features:
- The containing static class can have any number of extension blocks.
- Now, the receiver type is specified as part of the extension block with generic type parameters, if necessary.
- The receiver type can accept any number of generic type parameters.
- The generic type constraints, if any, should be defined after the receiver type (and its parameter).
extension<T>(T obj) where T : class
- For instance members, it is necessary to define a receiver instance.
- For static members, the receiver type alone suffices.
- No need to repeat the receiver parameter in the member definition, naturally aligns with the typical method usage.
- The static method invocation syntax (
Enumerable.Where()) will continue to work as it is. - As the receiver parameter is not part of the member definition, properties can be defined with relative ease.
- Static members need the inclusion of the
statickeyword as part of their definition. - Static and instance members can be part of the same extension block for the same type.
- But a static member definition is not allowed to reference the receiver instance.
Requirements:
- Install .NET 10 Preview 3 SDK for it to work
- Set the
LangVersionproject property topreview - Supported in VS2022 17.14.0 Preview 3.0 or later
Code Samples:
Instance Extension Property:
public static class Enumerable
{
extension<TSource>(IEnumerable<TSource> source)
{
public bool IsEmpty => !source.Any();
}
}
- For properties, the direct static invocation relies on the internal definition of the properties, structured as
get_<property_name>andset_<property_name>.- For Indexer, the property name defaults to
Item, soget_Itemandset_Item.
- For Indexer, the property name defaults to
- For this example, the direct invocation would be
Enumerable.get_IsEmpty(source).
Static Extension Method:
- Creates an empty list
extension<T>(List<T>)
{
public static List<T> Create() => [];
}
Usage:
// The Create method is associated with the type rather than the instance.
var values = List<int>.Create();
// Invoke the IsEmpty extension property on the instance.
if (values.IsEmpty) // True
{
// An empty sequence
}
Static Extension Property:
- Returns the informational version of .NET MAUI, omitting the commit hash.
using System.Reflection;
namespace MyApp.Extensions;
public static class AppExtensions
{
extension(MauiApp)
{
public static string Version
{
get
{
var version = typeof(MauiApp).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
return version?[..version.IndexOf('+')] ?? string.Empty;
}
}
}
}
Usage:
// Assume that the relevant namespace is imported.
// Refer to the static Version extension property
// as though it is defined within the MauiApp type.
var lblVersion = new Label()
{
Text = $".NET MAUI ver. {MauiApp.Version}"
};

Web Reference:
Happy coding. Stay connected as we continue to learn and share the experiences from this exciting journey of being a .NET developer.