C# 14 introduces extension members. C# has long had extension methods and the new extension member syntax builds on this familiar feature. The latest preview adds static extension methods and instance and static extension properties. We’ll release more member kinds in the future.
Extension members also introduce an alternate syntax for extension methods. The new syntax is optional and you do not need to change your existing extension methods. In this post, we’ll call the existing extension method syntax this-parameter extension methods and the new syntax extension members.
No matter the style, extension members add functionality to types. That’s particularly useful if you don’t have access to the type’s source code or it’s an interface. If you don’t like using !list.Any()
you can create your own extension method IsEmpty()
. Starting in the latest preview you can make that a property and use it just like any other property of the type:
// An extension property `IsEmpty` is declared in a separate class
public void Operate(IEnumerable<string> strings)
{
if (strings.IsEmpty)
{
return;
}
// Do the operation
}
Using the new syntax, you can also add extensions that work like static
properties and methods on the underlying type.
This post looks at the benefits of the current syntax, the design challenges we solved, and a few deeper scenarios. If you’ve never written an extension method, you’ll see how easy it is to get started.
Creating extension members
Here are examples of writing extensions using this-parameter extension syntax and the new syntax:
public static class MyExtensions
{
public static IEnumerable<int> ValuesLessThan(this IEnumerable<int> source, int threshold)
=> source.Where(x => x < threshold);
extension(IEnumerable<int> source)
{
public IEnumerable<int> ValuesGreaterThan(int threshold)
=> source.Where(x => x > threshold);
public IEnumerable<int> ValuesGreaterThanZero
=> source.ValuesGreaterThan(0);
}
}
Extension members need two kinds of information: the receiver that the member should be applied to, and what parameters it needs if it’s a method. This-parameter extension methods, like ValueLessThan
, put these together in the parameter list, prefixing the receiver with this
.
The new extension method syntax separates the receiver, placing it on the extension block. This provides a home for the receiver when the member does not have parameters, like extension properties.
Within the extension block, code looks just like it would appear if the members were placed on the underlying type.
Another benefit of the extension member syntax is grouping extensions that apply to the same receiver. The receiver’s name, type, generic type parameters, type parameter constraints, attributes and modifiers are all applied in the extension block, which avoids repetition. Multiple extension blocks can be used when these details differ.
The containing static class can hold any combination of extension blocks, this-parameter extension methods, and other kinds of static members. Even in greenfield projects that use only the new extension member syntax, you may also declare other static helper methods.
Allowing extension blocks, this-parameter extension methods, and other static members in the same static class minimizes the changes you need to make to add new kinds of extension members. If you just want to add a property, you just add an extension block to your existing static class. Since you can use the two syntax styles together, there is no need to convert your existing methods to take advantage of the new member types.
Extension blocks gives you full freedom to organize your code however it makes sense. That’s not just to support personal or team preference. Extension members solve many different kinds of problems that are best handled with different static class layouts.
As we designed extension members, we looked at code in public locations like GitHub and found huge variations in how people organize this-parameter extension methods. Common patterns include static classes that contain extension methods for a specific type (like StringExtensions
), having all the extensions for a project grouped in a single static class, and grouping multiple overloads with different receiver types into a static class. These are all the right approach to certain scenarios.
The way that extensions are organized into their containing static classes is significant because the static class name is used to disambiguate. Disambiguation is uncommon, but when ambiguity occurs the user needs a solution. Moving an extension to a differently named static class is a breaking change because someone somewhere uses the static class name to disambiguate.
Design challenges for extension members
The C# design team considered supporting other member kinds multiple times since this-parameter extension methods were introduced. It’s been a hard problem because challenges are created by the underlying realities:
- Extension methods are deeply embedded in our ecosystem – there are a lot of them and they are heavily used. Any changes to the extension syntax needs to be natural for developers consuming them.
- Converting an extension method to a new syntax should not break code that uses it.
- Developers organize their extensions appropriately for their scenario.
- The receiver has to be declared somewhere – methods have parameters but properties and most other member kinds do not.
- The current approach for disambiguation is well known and based on the containing static class name.
- Extension methods are called much more often than they are created or altered.
The next section takes a deeper look at receivers, disambiguation, and balancing the needs of extension authors and developers that use extensions.
Receivers
When you call an extension method, the compiler rewrites the call, which is called lowering. In the code below, calling the method on the receiver (to assign x1
) is lowered to calling the static method with the receiver as the first parameter, which is equivalent to the code that sets x2
:
int[] list = [ 1, 3, 5, 7, 9 ];
var x1 = list.ValuesLessThan(3);
var x2 = MyExtensions.ValuesLessThan(list, 3);
In the declaration of a this-parameter extension method, the receiver is the first parameter and is prefixed with this
:
public static class MyExtensions
{
public static IEnumerable<int> ValuesLessThan(this IEnumerable<int> source, int threshold) ...
In this declaration, the receiver is this IEnumerable<int> source
and int threshold
is a parameter to the method. This works well for methods, but properties and most other kinds of members do not have parameters that can hold the receiver. Extension members solve this by placing the receiver on the extension block. The ValueLessThan
method in the new syntax would be:
public static class MyExtensions
{
extension(IEnumerable<int> source)
{
public IEnumerable<int> ValuesLessThan(int threshold) ...
The compiler lowers this to a static method that is identical to the previous this-parameter extension method declaration, using the receiver parameter from the extension block. The lowered version is available to you to disambiguate when you encounter ambiguous signatures. This approach also guarantees that extension methods written with the this-parameter syntax or the new extension member syntax work the same way at both source and binary levels.
Properties lower to get
and set
methods. Consider this extension property:
public static class MyExtensions
{
extension(IEnumerable<int> source)
{
public IEnumerable<int> ValuesGreaterThanZero
{
get
{ ...
This lowers to a get
method with the receiver type as the only parameter:
public static class MyExtensions
{
public static IEnumerable<int> get_ValuesGreaterThanZero(this IEnumerable<int> source) ...
For both methods and properties, the type, name, generic type parameters, type parameter constraints, attributes and modifiers of the extension block are all copied to the receiver parameter.
Disambiguation
Generally, extensions are accessed as members on the receiver just like any other property or method, such as list.ValuesGreaterThan(3)
. This simple approach works well almost all the time. Occasionally, an ambiguity arises because multiple extensions with valid signatures are available. Ambiguities are rare, but when they occur the user needs a way to resolve it.
Disambiguation for this-parameter extension methods is done by calling the static extension method directly. Disambiguation for the new extension member syntax is done by calling the lowered methods created by the compiler:
var list = new List<int> { 1, 2, 3, 4, 5 };
var x1 = MyExtensions.ValuesLessThan(list, 3);
var x2 = MyExtensions.ValuesGreaterThan(list, 3);
var x3 = MyExtensions.get_ValuesGreaterThanZero(list);
Using the containing static class is consistent with traditional extension methods and the approach we think developers expect. When the static class name is entered, IntelliSense will display the lowered members, such as get_ValuesGreaterThanZero
.
Prioritizing extension users
We believe the most important thing for extension users is that they never need to think about how an extension is authored. Specifically, they should not be able to tell the difference between extension methods that use the this-parameter syntax and the new style syntax, existing extension methods continue to work, and users are not disrupted if the author updates a this-parameter extension method to the new syntax.
The C# team has explored adding other types of extension members for many years. There is no perfect syntax for this feature. While we have many goals, we settled on prioritizing first the experience of developers using extensions, then clarity and flexibility for extension authors, and then brevity in syntax for authoring extension members.
Static methods and properties
Static extension members are supported and are a little different because there isn’t a receiver. The parameter on the extension block does not need a name, and if there is one it is ignored:
public static class MyExtensions
{
extension<T>(List<T>)
{
public static List<T> Create()
=> [];
}
Generics
Just like this-parameter extension methods, extension members can have open or concrete generics, with or without constraints:
public static class MyExtensions
{
extension<T>(IEnumerable<T> source)
where T : IEquatable<T>
{
public IEnumerable<T> ValuesEqualTo(T threshold)
=> source.Where(x => x.Equals(threshold));
}
Generic constraints allow the earlier examples to be updated to handle any numeric type that supports the INumber<T>
interface:
public static class MyExtensions
{
extension<T>(IEnumerable<T> source)
where T : INumber<T>
{
public IEnumerable<T> ValuesGreaterThan(T threshold)
=> source.Where(x => x > threshold);
public IEnumerable<T> ValuesGreaterThanZero
=> source.ValuesGreaterThan(T.Zero);
public IEnumerable<TResult> SelectGreaterThan<TResult>(
T threshold,
Func<T, TResult> select)
=> source
.ValuesGreaterThan(threshold)
.Select(select);
}
The type parameter on the extension block (<T>
) is the generic type parameter inferred from the receiver.
The SelectGreaterThan
method also has a generic type parameter <TResult>
, which is inferred from the delegate passed to the select
parameter.
Some extension methods cannot be ported
Rules for the new extension member syntax will result in a few this-parameter extension methods needing to remain in their current syntax.
The order of generic type parameters in the lowered form of the new syntax is the receiver type parameters followed by the method type parameters. For example, SelectGreaterThan
above would lower to:
public static class MyExtensions
{
public static IEnumerable<TResult> SelectGreaterThan<T, TResult>(
this IEnumerable<T> source, T threshold, Func<T, TResult> select) ...
If the type parameters of the receiver do not appear first, the method cannot be ported to the new syntax. This is an example of a this-parameter extension method that can not be ported:
public static class MyExtensions
{
public static IEnumerable<TResult> SelectLessThan<TResult, T>(
this IEnumerable<T> source, T threshold, Func<T, TResult> select) ...
Also, you can’t currently port to the new syntax if a type parameter on the receiver has a constraint that depends on a type parameter from the member. We’re taking another look at whether we can remove that restriction due to feedback.
We think these scenarios will be rare. Extension members that encounter these limitations will continue to work using the this-parameter syntax.
A small problem with nomenclature
The C# design team often refers to extension members in terms of static and instance, methods and properties. We mean – methods and properties that behave as though they are either static or instance methods and properties on the underlying type. Thus, we think of this-parameter extension methods as being instance extension methods. But, this might be confusing because they are declared as static
methods in a static
class. Hopefully being aware of this possible confusion will help you understand the proposal, articles and presentations.
Summary
Creating extension members has been a long journey and we’ve explored many designs. Some needed the receiver repeated on every member, some impacted disambiguation, some placed restrictions on how you organized your extension members, and some created a breaking change if you updated to the new syntax. Some had complicated implementations. Some just didn’t feel like C#.
The new extension member syntax preserves the enormous body of existing this-parameter extension methods while introducing new kinds of extension members. It offers an alternate syntax for extension methods that is consistent with the new kinds of members and fully interchangeable with the this-parameter syntax.
You can find out more about extension members in the C# 14 Preview 3 Unboxing and you can check out all the new features at What’s new in C# 14.
We heard the feedback that the extra block and indentation level is surprising and wanted to share how we arrived at these design decisions.
Keep the feedback and questions coming. We love talking about C# features and can’t wait to see what you build with extension members!
The nesting struck me too, and makes me wish for the Kotlin feature to create a class from just the filename so that we could avoid the class declaration and nesting. I guess that wouldn’t be static though so wouldn’t work. I’m struggling to find where the file class feature has been discussed before but I’m sure it has been
I hope you all fix the erroneous code violations that are cropping up about nested types and how the methods should be static soon. Creating XML comments for these extension members is not working well with Copilot either.
I hear you! To be clear, this feature is in preview, and it’s still about six months until we expect it to ship. We don’t have the same bar for the tooling experience as a released feature; essentially just that it isn’t blocking you from trying the feature out. I do appreciate you pointing out where the experience currently falls short.
The code mentioned
public static class MyExtensions
{
extension(IEnumerable source)
where T : INumber
{
public IEnumerable ValuesGreaterThan(T threshold)
=> source.Where(x => x > threshold);
public IEnumerable ValuesGreaterThanZero
=> source.ValuesGreaterThan(T.Zero);
public IEnumerable SelectGreaterThan(
T threshold,
Func select)
=> source
.ValuesGreaterThan(x => x > threshold)
.Select(select);
}
is this correct, or there is a mistake in the SelectGreaterThan function, as ValuesGreaterThan requires input T.
we can change it to .Where(x => x > threshold) or .ValuesGreaterThan(threshold)
You are right, there was a bug in the example. I’ve fixed it. Thanks for pointing it out!
C# does not use the keywords “method”, “property”, “constructor” or “finalizer” in member declarations.
Have you considered other approaches to keyword choice? For me, the keyword “this” would be more natural than “extension” for C#.
Added: I mean, the “extension” looks like a top-level syntactic element that for some reason can only be declared inside a static class)
I would also vote to use “this” to replace “extension” keyword, which avoided introduce another keywords, and also feel more nature with the old usage and regular way to declare class.
I like the idea of splitting the receiver from regular parameters. But I believe there is not much values in ability to cover all existing ways extension methods can be organized into classes. As the unit of code organization in C# is class, instead of introducing some new extension unit, what about restricting extension classes to single receiver? Something like:
NOTE: press "Read more" to see the code example!
<code>
Note how nicely it reuse the this keyword from existing extension methods. It also reuse "primary constructor" syntax, but this time in static classes, so it is easy to understand that this is...
We spent considerable time exploring attaching the receiver to the static class. You point out many of the arguments in favor of this approach. The arguments against this approach included:
- Cases where developers combined random extension methods into a single class would require splitting up into multiple classes to use the new syntax
- "Class per receiver type" would also mean class per unique set of type parameters
- Splitting up the static class in this way would be a breaking change for users of the extension methods, because someone somewhere had...
One thing that Microsoft doesn't mention in the docs is extending static classes like Path, Directory or File, which is the best part of it. It is good that we don't need to create a utility counterpart for all new static class members.
<code>
<code>
I found the solution after lots of investigation, but Microsoft should mention it in their docs.
Thanks for the addition! I’ll pass it on to the documentation folks.
The naming of `this-parameter` extension methods feels awkward. It would be better to refer to them as traditional or classic extension methods.
To be honest, I’m a bit disappointed by the chosen approach. I understand the reasons, but I feel like it’s a missed opportunity to improve things with extension methods.
Specifically, the fact that a generic extension with a generic method is lowered to an extension method with 2 generic type parameters, is a shame. This means we will still have to specify both type arguments, even if the one for the receiver could have been inferred.
While that is painful, it is a broader problem than just extension methods. Feel free to upvote this issue: https://github.com/dotnet/csharplang/discussions/8967
Thank you @MadsTorgersen for considering my idea of having generic arguments on the containing classes. In fact, this was a crucial part of a proposal of mine in the Extensions discussion. I'm aware of that 99.9% backwards compatibility is a high priority goal, but this issue should be considered and hopefully fixed as soon as possible, since it's not only annoying but can be extremely confusing. I know I didn't understand why I need to provide an extra generic type argument until I checked what was the lowered form.
Recently I had to explain to my team why do we have...
In response to this comment (can't nest replies any deeper!):
We had not considered that! That's an interesting idea - thanks!
Having to pass "all" type arguments is certainly a nuisance, and one of the more unfortunate carry-overs from the existing extension method syntax in the name of full compatibility.
Our current stance is that we'll live with it for the initial release and see how much of an issue it becomes in the wild. An alternative to what you propose that we did discuss is to also allow passing only the method's type arguments while still inferring the receiver type arguments. This...
Have you considered adding support for generic containing classes? So instead of adding the generic type parameter to the extension block, you could add it to the static containing class. In this way the lowered method would have the proper arity of generic type arguments. But this should be optional as it wouldn’t be 100% binary compatible with classic extension methods, but would hugely benefit for new extension members.
Thanks. Partial type inference would indeed be useful, however I don’t feel that either of the proposed approaches fully solves the problem for extension methods. Even if you can omit one type argument with <,int>, <var, int> or <T: int>, the existence of the second type argument is still visible. If it were really a method of the extended type, it would have a single type parameter; that’s where the current extension method model breaks down, IMO.
this comment has been deleted.
How does the lowering handle conflicting names for static extension methods?
For example, if you had:
what would the lowered method names be? Or would the compiler reject that class?
You are correct. These are seen as duplicate names and the class will not compile.
This extension blocks syntax gives us the impression of creating a scope for members inside it even though it actually doesn’t. It will confuse new learners.
this comment has been deleted.
On .NET SDK 10.0-preview.3.25171.5, the compiler rejects the class:
I'd be interested in the arguments for introducing a completeley new syntax with added redundancy in the language (i.e., two ways of doing the same thing, declaring extension methods), over extending the existing syntax for other member types in some consistent way. (E.g., allowing a "this" parameter on properties and events in some form, perhaps adding a "static this" for static extension menbers, too, etc.)
Sure, the new syntax can be said to look "nicer" than extending property or event syntaxes, but that's very subjective and surely adding redundancy to the language should have "-100 points" from the start. So, could...
In short, we tried very hard on several occasions over the last decade and a half to come up with a syntax that was a natural extension (pun only slightly intended) of the extension method syntax. The outcome has been pretty gruesome every time! 😄 This is part of why it's taken us so long. Our willingness to step back and come up with a different syntax is what has allowed us to finally move on this long-requested feature set.
For a bit more detail, other member kinds would have to accommodate not only parameters (for the `this` parameter) but also...