May 8th, 2025

C# 14 – Exploring extension members

Kathleen Dollard
Principal Program Manager

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!

Author

Kathleen Dollard
Principal Program Manager

Kathleen Dollard loves to code and loves to teach and talk about code. She’s written tons of articles, a book, and spoken around the world. She’s on the .NET Team at Microsoft where she works on the .NET Core CLI and SDK and managed languages (C# and Visual Basic.NET). She’s always ready to help developers take the next step in exploring the wonderful world we call code.

24 comments

  • Jedel Rubix

    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)

  • Maxim 1 day ago · Edited

    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)

  • Rafał Kłys · Edited

    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...

    Read more
    • Kathleen DollardMicrosoft employee Author 3 days ago

      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...

      Read more
  • Hamed Fathi · Edited

    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.

    Read more
  • Vijay Anand E G

    The naming of `this-parameter` extension methods feels awkward. It would be better to refer to them as traditional or classic extension methods.

  • Thomas Levesque

    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.

      • Thomas Levesque 2 days ago · Edited

        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.

      • anonymous 2 days ago

        this comment has been deleted.

  • Richard Deeming · Edited

    How does the lowering handle conflicting names for static extension methods?

    For example, if you had:

    public static class MyExtensions
    {
        extension(List<T>)
        {
            public static List<T> Create()
                => [];
        }
        extension(HashSet<T>)
        {
            public static HashSet<T> Create()
                => [];
        }
    }

    what would the lowered method names be? Or would the compiler reject that class?

      • anonymous

        this comment has been deleted.

    • Kalle Niemitalo 6 days ago · Edited

      On .NET SDK 10.0-preview.3.25171.5, the compiler rejects the class:

      using System.Collections.Generic;
      
      public static class MyExtensions
      {
          extension<T>(List<T>)
          {
              public static List<T> Create()
                  => [];
          }
          extension<T>(HashSet<T>)
          {
              public static HashSet<T> Create()
                  => [];
          }
      }

      Class1.cs(12,34): error CS0111: Type ‘MyExtensions’ already defines a member called ‘Create’ with the same parameter types

  • Fabian Schmied · Edited

    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...

    Read more
    • Mads TorgersenMicrosoft employee 6 days ago

      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...

      Read more
  • Gábor Szabó · Edited

    Great to see this feature being finally taking its first steps!

    You mentioned how the team considered multiple syntaxes and people were surprised at the additional indentation level. Have you considered the static class taking the receiver as a parameter? Similar in syntax to primary constructors. And obviously, this being a static class, would actually seem like a closure, but it would instead be lowered, so no capture would be made.
    <code>

    I haven't given it much thought, and generics would probably be wonky (what's a generic static class?), just wanted to ask.

    Read more
    • Kathleen DollardMicrosoft employee Author 6 days ago

      We considered that and still may release a shortcut later that puts the receiver on the type.

      That approach will work for simple cases. But as you encounter variations in the receiver, such as different attributes, you will need more than just the static type.

      Also, the static class is very important in disambiguation. So, if we did not allow the two syntactic approaches to coexist in the same class, designing for easy disambiguation would be more difficult. The user has a much easier time with disambiguation if they can predict the name of the static class and kick in...

      Read more
      • Gábor Szabó 5 days ago · Edited

        I appreciate the answer, thank you. I'm not surprised you have given this thought beforehand, you guys have always been very thorough. It's good to see how seemingly surface-level issues can sometimes have cascading problems and I understand the line has to be drawn somewhere for the feature to eventually ship.

        Still, if it would be an option to use one over the other... I feel in the current static class-based syntax the static class actually holds no benefit to the user. The extension block does, and that does not seemingly require the static class, besides for disambiguation. But for...

        Read more
      • Praveen Potturu · Edited

        Like Gábor Szabó mentioned, the additional indentation is something that will prevent me from using this new syntax. File scoped namespace reduced one level of indentation that I applied to all my code. I am not going back with this new syntax. I would rather prefer having extension keyword replace the static class keywords. That would also help us separate the extension methods that extend different types into separate classes. Hopefully something like this will be implemented before the release (or at least in C# 15).

        <code>

        Read more
  • xoofx 7 days ago · Edited

    Extension members are pretty amazing and I’m glad to see that the C# language team found a sweet spot in terms of syntax, versatility and usability as it is going to open a wide range of new opportunities, really looking forward to this feature! ☺️
    Incidentally I have been prototyping some stuffs in the past days and found a limitation https://github.com/dotnet/roslyn/issues/78472 that I hope can be solved before RC. 🤞

    • Mads TorgersenMicrosoft employee 6 days ago

      Thanks for sharing. Glad you like the new feature! We are looking at whether we can remove the restriction you are running up against.