Extension Members
|
This section documents C# 14 on .NET 10 (LTS), as published at learn.microsoft.com/dotnet/csharp, which is the reference these pages are written and verified against. Features introduced by C# 15 / .NET 11 are still in preview and are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against learn.microsoft.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Extension members let you call a method on a type as though it declared that method, without modifying or deriving from it. They are a compile-time trick — the compiler rewrites the call into a plain static call — which is why they work on sealed types, interfaces and types from other assemblies alike.
Classic Extension Methods
An extension method is a static method in a static class whose first parameter carries this:
public static class StringExtensions
{
public static bool IsNullOrBlank(this string? value)
=> string.IsNullOrWhiteSpace(value);
public static string Truncate(this string value, int maxLength, string suffix = "…")
{
ArgumentNullException.ThrowIfNull(value);
return value.Length <= maxLength ? value : value[..maxLength] + suffix;
}
// Extension methods may be generic.
public static IEnumerable<TSource> WhereNotNull<TSource>(
this IEnumerable<TSource?> source) where TSource : class
{
foreach (TSource? item in source)
{
if (item is not null)
{
yield return item;
}
}
}
}
public static class ExtensionDemo
{
public static void Run()
{
Console.WriteLine(" ".IsNullOrBlank()); // True
Console.WriteLine("a long sentence".Truncate(6)); // a long…
string?[] values = ["a", null, "b"];
Console.WriteLine(string.Join(",", values.WhereNotNull())); // a,b
// Calling it as a plain static method is always legal, and is what the
// compiler actually emits.
Console.WriteLine(StringExtensions.Truncate("abcdef", 3));
}
}
The containing class must be static, non-generic and non-nested. this may appear only on the first
parameter, and may be combined with ref/in/ref readonly for value types (but not out).
Discovery: the using Directive
Extension methods are found by namespace. A method is in scope only if its containing namespace is imported:
namespace Irurueta.Sample.Text;
public static class Casing
{
public static string ToTitleCase(this string value)
=> CultureInfo.InvariantCulture.TextInfo.ToTitleCase(value.ToLowerInvariant());
}
using Irurueta.Sample.Text; // without this, ToTitleCase does not exist
public static class DiscoveryDemo
{
public static void Run() => Console.WriteLine("hello world".ToTitleCase());
}
This is why using System.Linq; is what makes Where and Select appear on every sequence, and why
ImplicitUsings includes it. The practical guidance: put extension methods in the namespace of the type they
extend when they are meant to be ubiquitous, or in your own clearly-named namespace when the caller should opt
in.
LINQ: The Canonical Example
Every LINQ operator is an extension method on IEnumerable<T> — which is how one interface with a single
method gained a hundred operations without any implementer changing:
public static class MiniLinq
{
// This is essentially how System.Linq.Enumerable.Where is written.
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(predicate);
return Iterate();
IEnumerable<TSource> Iterate()
{
foreach (TSource item in source)
{
if (predicate(item))
{
yield return item;
}
}
}
}
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
foreach (TSource item in source)
{
yield return selector(item);
}
}
}
Writing your own operators is a normal thing to do; see LINQ.
Extending Interfaces and Your Own Types
public interface IAuditable
{
DateTimeOffset ModifiedAt { get; }
string ModifiedBy { get; }
}
public static class AuditableExtensions
{
// Behaviour shared by every implementer, with no base class and no
// default interface member.
public static bool ModifiedSince(this IAuditable auditable, DateTimeOffset when)
=> auditable.ModifiedAt > when;
public static string Describe(this IAuditable auditable)
=> $"{auditable.ModifiedBy} at {auditable.ModifiedAt:u}";
}
An extension method on an interface is the lighter alternative to a default interface member: it needs no interface change, it is callable on the concrete type, but it cannot be overridden by an implementer.
Extension Methods and null
An extension method is a static call, so the receiver is not dereferenced — a null receiver reaches the method body as a null argument:
public static class NullReceiver
{
public static bool IsEmpty(this string? value) => value is null || value.Length == 0;
public static void Demo()
{
string? nothing = null;
Console.WriteLine(nothing.IsEmpty()); // True -- no NullReferenceException!
// But an *instance* method on the same null would throw:
// Console.WriteLine(nothing.Length); // NullReferenceException
}
}
This is occasionally useful (IsNullOrEmpty-style helpers) and often confusing — a reader expects
x.Foo() to throw when x is null. Annotate the parameter string? when you handle null deliberately, and
guard with ArgumentNullException.ThrowIfNull when you do not.
Guidelines
-
Extend a type you do not own. If you own it, add a real member.
-
Do not "extend" to reach private state — you cannot, and the workaround will be worse than a method.
-
Keep them in a narrow, purposefully-named namespace so callers opt in.
-
Name the class
<Type>Extensionsby convention. -
An extension method never overrides anything: an instance member of the same name always wins (see below), so adding an instance member to the type later silently changes which code runs.
Extension Blocks (C# 14)
C# 14 generalises extensions beyond methods. An extension block names a receiver once, then declares any
number of members against it — including properties, static members and operators, which the classic
syntax could never express:
public static class SequenceExtensions
{
// The receiver is declared once for the whole block.
extension<TSource>(IEnumerable<TSource> source)
{
// An extension PROPERTY -- impossible before C# 14.
public bool IsEmpty => !source.Any();
public int CountOrZero => source?.Count() ?? 0;
// An extension method, in the same block.
public IEnumerable<TSource> WhereNot(Func<TSource, bool> predicate)
=> source.Where(item => !predicate(item));
}
// A block for a specific closed type.
extension(string text)
{
public bool IsBlank => string.IsNullOrWhiteSpace(text);
public string Reversed
{
get
{
char[] characters = text.ToCharArray();
Array.Reverse(characters);
return new string(characters);
}
}
public string Repeat(int times) => string.Concat(Enumerable.Repeat(text, times));
}
}
public static class ExtensionBlockDemo
{
public static void Run()
{
int[] numbers = [1, 2, 3, 4];
Console.WriteLine(numbers.IsEmpty); // False -- a property!
Console.WriteLine(string.Join(",", numbers.WhereNot(n => n % 2 == 0))); // 1,3
Console.WriteLine(" ".IsBlank); // True
Console.WriteLine("abc".Reversed); // cba
Console.WriteLine("ab".Repeat(3)); // ababab
}
}
Static Extension Members
Naming the type rather than a receiver parameter declares members on the type itself:
public static class TypeExtensions
{
// No receiver parameter -- these become static members of `string`.
extension(string)
{
public static string Empty2 => "";
public static string Join(char separator, params ReadOnlySpan<string> parts)
=> string.Join(separator, parts.ToArray());
}
extension(int)
{
public static int Clamp01(int value) => Math.Clamp(value, 0, 1);
}
}
public static class StaticExtensionDemo
{
public static void Run()
{
Console.WriteLine(string.Empty2.Length); // 0
Console.WriteLine(string.Join('-', "a", "b")); // a-b
Console.WriteLine(int.Clamp01(5)); // 1
}
}
Extension Operators
public static class MatrixOperators
{
extension(int[] left)
{
// An operator declared on a type you do not own.
public static int[] operator +(int[] a, int[] b)
{
if (a.Length != b.Length)
{
throw new ArgumentException("Length mismatch.", nameof(b));
}
var result = new int[a.Length];
for (int i = 0; i < a.Length; i++)
{
result[i] = a[i] + b[i];
}
return result;
}
}
}
public static class ExtensionOperatorDemo
{
public static void Run()
{
int[] a = [1, 2, 3];
int[] b = [10, 20, 30];
Console.WriteLine(string.Join(",", a + b)); // 11,22,33
}
}
The classic this-parameter syntax remains fully supported and is not deprecated; extension blocks are the
superset. A single static class may contain both.
Extension Indexers (C# 15 preview)
|
Preview feature — C# 15 / .NET 11
This requires a .NET 11 preview SDK and |
C# 15 completes the set by allowing an indexer in an extension block:
public static class DictionaryIndexers
{
extension<TKey, TValue>(IReadOnlyDictionary<TKey, TValue> source)
where TKey : notnull
{
// An extension indexer with a fallback, on an interface that has no setter.
public TValue this[TKey key, TValue fallback]
=> source.TryGetValue(key, out TValue? value) ? value : fallback;
}
}
// Used as:
// IReadOnlyDictionary<string, int> counts = ...;
// int hits = counts["missing", 0];
Resolution: Extension versus Instance Members
The rule is simple and absolute: an applicable instance member always beats an extension member. The compiler only looks for extensions after finding no instance member that works.
public sealed class Widget
{
public string Describe() => "instance";
}
public static class WidgetExtensions
{
public static string Describe(this Widget widget) => "extension";
public static string DescribeVerbose(this Widget widget) => "extension only";
}
public static class ResolutionDemo
{
public static void Run()
{
var widget = new Widget();
Console.WriteLine(widget.Describe()); // instance -- the extension is shadowed
Console.WriteLine(widget.DescribeVerbose()); // extension only
// The extension is still reachable explicitly.
Console.WriteLine(WidgetExtensions.Describe(widget)); // extension
}
}
Between competing extension candidates, the compiler applies normal overload resolution and prefers the one
whose receiver type is more specific (IList<T> beats IEnumerable<T>); a genuine tie is an ambiguity error
that you resolve by calling the static method directly.
The consequence worth internalising: adding an instance method to a type can silently change the meaning of code that was calling an extension of the same name. This is the main reason not to write extension methods for types you control.
See Also
-
LINQ — extension methods at their most ambitious.
-
Interfaces — default interface members, the other way to add behaviour to an existing contract.
-
Methods and Parameters — overload resolution in general.
-
Generics — generic extension methods and constraints.
References
-
Microsoft Learn — Extension members and the
extensionkeyword. -
github.com/dotnet/csharplang — the extension members and extension indexers proposals.