Disambiguation of extension members in C# 14
In my previous two blog posts, I wrote about extension members in general and generic extension members. But there's one more topic related to extension members I'd like to cover: how ambiguous extension member references in code can be resolved.
Ambiguity could already happen with the old-style extension method syntax: you could have two extension methods with the same syntax in two different static container classes:
public static class FirstExtensions
{
public static string Prefix(this string receiver)
{
return $"First: {receiver}";
}
}
public static class SecondExtensions
{
public static string Prefix(this string receiver)
{
return $"Second: {receiver}";
}
}
If they were both in scope, the compiler couldn't determine which one you want to invoke:
The solution was to invoke the method as if it was a regular static method instead of as an extension method:
FirstExtensions.Prefix("foo").ShouldBe("First: foo");
Of course, you can also have two extension members in new style syntax with the same signature in scope at the same time:
public static class FirstExtensions
{
extension(string? receiver)
{
public string? Prefix()
{
return $"First: {receiver}";
}
}
}
public static class SecondExtensions
{
extension(string? receiver)
{
public string? Prefix()
{
return $"Second: {receiver}";
}
}
}
And the compiler again won't know which one you want to invoke. Due to different syntax, there is seemingly no equivalent static method available to invoke. However, the compiler still creates those static methods for you as if you were using the old-style syntax. So you can use them to tell the compiler which method to invoke:
FirstExtensions.Prefix("foo").ShouldBe("First: foo");
But what about the other types extension members: instance properties, static methods and properties, operators?
public static class FirstExtensions
{
extension(string? receiver)
{
public string WithPrefix
{
get => $"First: {receiver}";
}
public static string Create()
{
return "First";
}
public static string ExtensionId
{
get => "First";
}
public static string operator !(string? value)
{
return $"First: {value}";
}
}
}
Even for those, the compiler creates static methods:
FirstExtensions.get_WithPrefix("foo").ShouldBe("First: foo");
FirstExtensions.Create().ShouldBe("First");
FirstExtensions.get_ExtensionId().ShouldBe("First");
FirstExtensions.op_LogicalNot("foo").ShouldBe("First: foo");
As you can see, for the instance property, it creates a static method with a prefixed name (get_
for the getter, set_
for the setter). For the static methods and properties, it follows the same convention as for the instance ones, except that there is no need for an extra first parameter to pass in the instance. And for the operators, it composes the name from the op_
prefix and the operator name.
Fortunately, you don't need to remember all the naming rules. As long as you know that all extension members are available as static methods of the container static class, you can take advantage of IntelliSense to see the actual method names:
For generic extension members, there's another important rule that affects the static methods being generated. It only matters when both the receiver type in the extension block and the extension member itself are generic:
public static class FirstExtensions
{
extension<TItem>(IEnumerable<TItem> receiver)
{
public TResult Choose<TResult>(TResult first, TResult second)
{
return first;
}
}
}
In the above example, the extension method only has a single generic type argument, but the static method generated by the compiler will have two. The generic type arguments in the generated static method will always start with the ones from the receiver type, followed by the ones from the extension member:
FirstExtensions
.Choose<int, string>(Enumerable.Range(1, 5), "First", "Second")
.ShouldBe("First");
With the old-style extension method syntax you can decide yourself, in what order you will list the generic type arguments. You can put the argument of the receiver type at whichever position you want, even at the end:
public static class FirstExtensions
{
public static TResult Choose<TResult, TItem>(
this IEnumerable<TItem> receiver,
TResult first,
TResult second
)
{
return first;
}
}
There is no way to reimplement such an extension method using the new syntax without reversing the order of type parameters. Trying to do so would break source-level compatibility of any static method invocations with type arguments explicitly listed in their order:
FirstExtensions
.Choose<string, int>(Enumerable.Range(1, 5), "First", "Second")
.ShouldBe("First");
Thanks to type inference, you don't need to list the type arguments explicitly for most invocations, so this becomes much less of a problem. However, the two methods with a different order of type arguments are still incompatible at binary level, even if their signatures are otherwise the same. You would have to recompile the code, even if no code changes were necessary.
As already mentioned in my previous two blog posts, the project language version must be set to preview to make extension members available. Extension operators only work since .NET 10 preview 7, while the other extension members are already available in .NET 9. You can use the latest non-preview version (17.14.12) of Visual Studio 2022 to try them out.
You can find a sample project with all the different cases of ambiguous extension members in my GitHub repository. The tests demonstrate how they can be invoked using the generated static methods instead.
It's rather unlikely that you will even encounter ambiguity when using extension members. Despite that, I think it's good to know that you can still disambiguate such calls and how you can do it. Knowing about it also gives you a better understanding how extension members are actually implemented.