Extension Members in C# 1404/19/2026 | 8 minutes to read
In this post, I explore C# 14's extension members feature and how it is compiled into IL code.
From Extension Methods to Extension Members
Previous versions of C# allowed developers to declare extension methods. The code snippets in this post are for demonstration purposes only and should not be used directly in production applications:
string value = "ThisIsPascalCased"; Console.WriteLine(value.ToCamel()); public static class MyExtensions { public static string ToCamel(this string str) => str.Length switch { 0 => string.Empty, 1 => str.ToLowerInvariant(), _ => new([char.ToLower(str[0]), .. str[1..]]) }; }
The ToCamel method takes a PascalCased (upper camel cased) string and converts it to camel cased by lowering the case of the first character. It is also an extension method: it is declared in a static class, as a static method, and with the this keyword indicating the extended type. Extension methods have been around for a long time, but they had some limitations: no support for static methods (methods that extend a type's static members) or other extension members, such as properties or events.
Extension methods are widely used across applications and frameworks. For example, LINQ uses them to extend enumerables and collections.
Extension methods are compiled as regular static methods decorated with the ExtensionAttribute. For example, the ToCamel method compiles to:
.method public hidebysig static // ๐ static method
string ToCamel (
string str
) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
01 00 01 00 00
)
.custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( // ๐ attribute
01 00 00 00
)
In this blog post, I used ildasm and ILSpy to disassemble the JIT code.
Since extension methods are compiled as static methods, they can also be invoked as static methods:
Console.WriteLine(MyExtensions.ToCamel("ThisIsPascalCased"));
An interesting sidenote: extension methods allow partial method declarations, but extension members do not:
public static partial class MyExtensions { public static partial string ToCamel(this string str); }
C# 14 Extension Members
C# 14 extends the capabilities of extension methods (pun intended) with a new feature: extension members. Extension members allow defining properties, static methods (here, static means a static member of the extended type), and static operators. These use cases are well-defined in the official documentation, hence this blog post focuses on how extension members are compiled into IL code.
The example above can be rewritten using the extension members syntax:
public static class MyExtensionMembers { extension(string str) { public string ToCamelExtensionMember() => str.Length switch { 0 => string.Empty, 1 => str.ToLowerInvariant(), _ => new([char.ToLower(str[0]), .. str[1..]]) }; } }
The extended type (and a parameter name) is moved to the declaration of the extension block. This ToCamelExtensionMember method is no longer declared as static, and it drops the parameter with the this keyword. However, it can reference the string str parameter in a way that feels like a readonly field. This extension member can be used as a method on an instance of the extended type or as a static method of the enclosing static type:
string value = "ThisIsPascalCased"; Console.WriteLine(value.ToCamelExtensionMember()); Console.WriteLine(MyExtensionMembers.ToCamelExtensionMember(value));
Let's extend MyExtensionMembers with extension properties and static extension methods:
extension(string str) { // ... public static string ToCamelStatic(string input) => input.ToCamelExtensionMember(); public bool IsCamelStart => char.IsLower(str[0]); } // Usage examples 👇 Console.WriteLine(string.ToCamelStatic(value)); Console.WriteLine(value.IsCamelStart);
Static extension methods are invoked using static method syntax (e.g., string.ToCamelStatic(value)). Extension properties can have getters and setters, but they can only access the public interface of the extended type or the static context. While the syntax and usage feel natural, the generated IL looks significantly different from extension methods. Let's examine the generated IL next (explanations are added as code comments).
The compiler still generates the static methods in the MyExtensionMembers class:
.method public hidebysig static
string ToCamelExtensionMember (
string str
) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = (
01 00 00 00
)
// IL code of the method omitted for brevity
}
.method public hidebysig static
string ToCamelStatic (
string input
) cil managed
{
// IL code of the method omitted for brevity
}
.method public hidebysig static
bool get_IsCamelStart (
string str
) cil managed
{
// IL code of the method omitted for brevity
}
These methods contain the compiled IL for method bodies, making extension members backward compatible with extension methods. Existing compilers and tooling can handle extension members, though they may not display them correctly. Note that the IsCamelStart property's getter is compiled to a method named get_IsCamelStart, which can be invoked from the static enclosing type's scope as:
Console.WriteLine(MyExtensionMembers.get_IsCamelStart(value));
The enclosing MyExtensionMembers now has a new nested type:
.class nested public auto ansi sealed specialname '$34505F560D9EACF86A87F3ED1F85E448'
extends [System.Runtime]System.Object
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = (
01 00 00 00
)
// ๐ Implementation of an inner-inner nested Type discussed later
.class nested public auto ansi abstract sealed specialname '$ECEADA5840DE47E35C0C15BC038C831E' // ...
// โ ๏ธ Notice ToCamelExtensionMember is an instance member.
.method public hidebysig instance string ToCamelExtensionMember() cil managed
{
// ๐ ExtensionMarker Attribute referencing the inner-inner nested type
.custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionMarkerAttribute::.ctor(string) = (...)
.maxstack 8
// ๐ All member bodies throw
IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor()
IL_0005: throw
}
// ๐ Notice ToCamelStatic is a static member on this type.
.method public hidebysig static
string ToCamelStatic (
string input
) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionMarkerAttribute::.ctor(string) = (...)
.maxstack 8
IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor()
IL_0005: throw
}
.method public hidebysig specialname
instance bool get_IsCamelStart () cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionMarkerAttribute::.ctor(string) = (...)
.maxstack 8
IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor()
IL_0005: throw
}
.property instance bool IsCamelStart()
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionMarkerAttribute::.ctor(string) = (...)
.get instance bool MyExtensionMembers/'$34505F560D9EACF86A87F3ED1F85E448'::get_IsCamelStart()
}
}
This generated class with a special (hidden) type name <G>$34505F560D9EACF86A87F3ED1F85E448 looks like a regular type with instance methods, static methods, and properties. However, it only defines the signatures of the methods, all implementations throw a NotSupportedException. Remember, the actual implementations are defined in the backward compatible way. There is no reason to duplicate the same IL.
The ToCamelExtensionMember is an instance method. But how can the compiler tell on what type is extended? The <G>$34505F560D9EACF86A87F3ED1F85E448 special type has no reference to the string str declaration on the extension block. This is where another nested public type helps: all members of the current type are attributed with ExtensionMarkerAttribute pointing to yet another inner nested type. This nested type has a single method with a no-op implementation:
.class nested public auto ansi abstract sealed specialname '$ECEADA5840DE47E35C0C15BC038C831E'
extends [System.Runtime]System.Object
{
.method public hidebysig specialname static
void '$' (
string str
) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00)
.maxstack 8
IL_0000: ret
}
}
This type has a method with the parameter string str matching the extension block declaration. Compilers, tooling, and the runtime can use this information to infer the extended type.
Interesting Aspects
Compile time binding
The documentation clarifies the primary use-case for extension methods:
Extension members are preferable when the original source isn't under your control, when a derived object is inappropriate or impossible, or when the functionality has limited scope.
Let's use custom type with an extension method through an example. We can assume that MyType is defined in a 3rd party nuget package, and the extension member is defined in a local codebase referencing the nuget package:
public record class MyType(int Value); //... extension(MyType mt) { public int InflatedValue => mt.Value * 2; }
The InflatedValue property returns the double of the Value property from MyType.
var myType = new MyType(1); Console.WriteLine(myType.InflatedValue); // 👈 prints 2
Now the owner of the nuget package introduces InflatedValue property directly on MyType:
public record class MyType(int Value) { public int InflatedValue => Value * 3; }
When the consuming application updates to the new NuGet package version, the previous example will alter the behavior:
var myType = new MyType(1); Console.WriteLine(myType.InflatedValue); // 👈 prints 3
This is because the compiler prefers to bind to the InflatedValue property on the defining type itself rather than the extension property.
Extension Members with ref modifier
Extension members allow ref and in modifiers (not out):
extension(ref int value) { public void Calculate() => value = 10; } //... int num = 4; ref int numRef = ref num; numRef.Calculate(); Console.WriteLine(numRef); // 👈 prints 10
Conclusion
Extension members in C# 14 provide a powerful and natural way to extend types with properties, static methods, and operators without modifying their original definitions. They maintain backward compatibility with extension methods while opening new possibilities for developers. However, understanding compile time binding semantics and potential member shadowing is essential for using this feature effectively. With careful consideration of its capabilities and best practices, extension members can significantly enhance modern C# development.