Generic extension members in C# 14

September 5th 2025 C# 14 C# .NET

I already wrote about extension members in C# 14 in my previous blog post, but I only covered the non-generic members. Since generics were fully supported in extension methods, they are of course also supported in extension members.

Let's start with a simple generic method:

public static class ObjectExtensions
{
    public static TAttribute? GetCustomAttributes<TAttribute>(this object receiver)
        where TAttribute : Attribute
    {
        return receiver.GetType().GetCustomAttribute<TAttribute>();
    }
}

We can convert it into the new extension method syntax, the same way we did it with non-generic methods:

public static class ObjectExtensions
{
    extension(object receiver)
    {
        public TAttribute? GetCustomAttributes<TAttribute>()
            where TAttribute : Attribute
        {
            return receiver.GetType().GetCustomAttribute<TAttribute>();
        }
    }
}

We simply moved the receiver declaration from the method signature to the extension block and removed the static modifier. Everything else remained the same, including the generic type argument.

But if the receiver type was generic instead of just the method return type:

public static class EnumerableExtensions
{
    public static IEnumerable<T> WhereWithinRange<T>(
        this IEnumerable<T> receiver,
        T min,
        T max
    )
        where T : INumber<T>
    {
        return receiver.Where(item => item >= min && item <= max);
    }
}

Then, the generic type argument would be moved to the extension block along with its constraint:

public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T> receiver) where T : INumber<T>
    {
        public IEnumerable<T> WhereWithinRange(T min, T max)
        {
            return receiver.Where(item => item >= min && item <= max);
        }
    }
}

If you aren't keeping track with new C# language features, you might be confused by the INumber<T> interface. It was introduced in C# 11 as part of generic math support, which allowed using mathematical operators with generic types. Thanks to it, I don't have to write separate overloads for each numeric type anymore, and the same generic code will work even with custom generic types which implement the required interface(s). But I digress. Let's focus again on generic extension members.

Generic extension methods can have more than one type argument: some of those can apply to the receiver type, the others to other extension method parameters or return type:

public static class EnumerableExtensions
{
    public static IEnumerable<TItem> WhereWithinRangeBy<TItem, TValue>(
        this IEnumerable<TItem> receiver,
        Func<TItem, TValue> projection,
        TValue min,
        TValue max
    )
        where TValue : INumber<TValue>
    {
        return receiver
            .Where(item => projection(item) >= min && projection(item) <= max);
    }
}

When converting such a method to the new extension member syntax, the type argument and constraint of the receiver type will move to the extension block, while the other generic type arguments and their constraints will remain on the method:

public static class EnumerableExtensions
{
    extension<TItem>(IEnumerable<TItem> receiver)
    {
        public IEnumerable<TItem> WhereWithinRangeBy<TValue>(
            Func<TItem, TValue> projection,
            TValue min,
            TValue max
        )
            where TValue : INumber<TValue>
        {
            return receiver
                .Where(item => projection(item) >= min && projection(item) <= max);
        }
    }
}

The method constraint can of course reference both the method and the extension block type arguments. On the other hand, the extension block constraint can't reference the method type argument. If you have such an extension method, you won't be able to convert it to the new extension member syntax.

So far, we have only converted the old extension method syntax to the new extension member syntax. Such methods can be invoked the same way before and after the conversion:

obsolete
    .GetCustomAttributes<ObsoleteAttribute>()
    ?.Message
    .ShouldBe("Made up for testing.");

Enumerable.Range(1, 5).WhereWithinRange(2, 4).ShouldBe(Enumerable.Range(2, 3));

collection
    .WhereWithinRangeBy(((int value, string _) item) => item.value, 2, 4)
    .ShouldBe(collection[1..4]);

But we could also create extension members which previously weren't supported: properties and static members. Especially static extension members of generic types allow some interesting approaches which previously weren't possible.

When dealing with enums, I've seen extension methods like this:

public static class StringExtensions
{
    public static TEnum ParseAsEnum<TEnum>(this string receiver)
        where TEnum : struct, Enum
    {
        return Enum.Parse<TEnum>(receiver);
    }
}

This allowed the following code:

Enum.Parse<DayOfWeek>("Monday").ShouldBe(DayOfWeek.Monday);

To be rewritten like this:

"Monday".ParseAsEnum<DayOfWeek>().ShouldBe(DayOfWeek.Monday);

Arguably, the said method makes mores sense as a static method of Enum. And with extension members, you can achieve this:

public static class EnumExtensions
{
    extension<TEnum>(TEnum)
        where TEnum : struct, Enum
    {
        public static TEnum Parse(string value)
        {
            return Enum.Parse<TEnum>(value);
        }
    }
}

Now, the sample code from the above can be rewritten like this:

DayOfWeek.Parse("Monday").ShouldBe(DayOfWeek.Monday);

Just like non-generic extension members, most generic extension members also work with .NET 9 if you set the language version in your project to preview, except extension operators which require .NET 10 preview 7. You can try them all out with the latest non-preview version (17.14.12) of Visual Studio 2022 installed.

You can download a sample project with all the code from this post and more from my GitHub repository. It has a separate set of tests for the old and the new syntax, so that you can more easily compare the two.

Generic extension members work just as you would expect them to. In addition to being an alternative syntax for the generic extension methods you have already been writing, the support for extension properties and static members allows stuff which wasn't possible before. We might see some interesting novel approaches in libraries once C# 14 and .NET 10 become generally available.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License