Extension Methods and Extension Members

Extension members let you call your own methods and properties on a type you cannot edit, through this or a C# 14 extension block.

Syntaxpublic static int M(this string s) { ... } | extension(string s) { public int M() { ... } }

Sometimes you want a method on a type you cannot change: string, a type from a package, an interface someone else defined. An extension member gives you one. You write a static method elsewhere, and the compiler lets it be called as though it belonged to the type.

The classic form

Put this on the first parameter of a static method inside a static class: public static int WordCount(this string value). Then "a b".WordCount() and StringExtensions.WordCount("a b") are the same call, the first being shorthand for the second. Nothing is added to string itself. The member is visible only where its namespace is in scope, which is why LINQ needs using System.Linq;.

C# 14 extension blocks

C# 14 adds a second syntax. An extension(string value) block names the receiver once, then declares members that use it. A block can hold extension properties, static members and operators, none of which the this form can express. Put ref on the receiver, as in extension(ref Counter counter), when the member must modify a struct in place.

Both forms must live in a non-nested, non-generic static class, and both compile to the same IL, so callers cannot tell which you used.

What trips people up

An extension never beats a real member. If the type already has a member of that name with a compatible signature, the compiler binds to the type's own and your extension is never called. Also, the receiver is just an argument, so calling an extension on a null reference does not throw by itself; your code gets null and must handle it.

Example

Example · csharp
Console.WriteLine("the quick brown fox".WordCount());
Console.WriteLine("hello".IsBlank);
Console.WriteLine("   ".IsBlank);

// Both forms must sit in a non-nested, non-generic static class.
static class StringExtensions
{
    // The form that works in every C# version: `this` on the first parameter.
    // The same method can also be called as StringExtensions.WordCount(text).
    public static int WordCount(this string value) =>
        value.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;

    // C# 14 only: an extension block. `value` is the receiver, named once for
    // every member in the block, and properties become possible.
    extension(string value)
    {
        public bool IsBlank => value.Trim().Length == 0;
    }
}

// Output:
// 4
// False
// True

When to use it

  • Add a WordCount or Truncate helper to string without a utility class name at every call site.
  • Give an interface from a package a convenience method you need in several places.
  • Add a formatted display string to a shared data type without putting formatting on the type itself.
  • Write a helper over IEnumerable<T> that works on arrays and List<T> alike.

More examples

One extension for every sequence type

Extending the interface rather than a concrete type is how LINQ itself is built. Do this when the helper makes sense for anything you can iterate, instead of writing one version for arrays and another for List<T>.

Example · csharp
using System.Text;

namespace Reporting;

public static class SequenceExtensions
{
    // Declared on IEnumerable<T>, so it is available on arrays, List<T>,
    // and the result of any LINQ query.
    public static string ToBulletList<T>(this IEnumerable<T> source)
    {
        StringBuilder builder = new();

        foreach (T item in source)
        {
            builder.Append("- ").Append(item).Append('\n');
        }

        return builder.ToString();
    }
}

// With `using Reporting;` in scope:
// new[] { "a", "b" }.ToBulletList()  ->  "- a\n- b\n"

Instance and static extension members in one block

Only a C# 14 extension block can add a property or a static factory to a type you do not own. Reach for the static form when the natural place for a conversion is the type name rather than an instance.

Example · csharp
using System.Globalization;

namespace Money;

public static class DecimalExtensions
{
    // Instance members: called on a value, as amount.ToUsd().
    extension(decimal amount)
    {
        public bool IsPositive => amount > 0m;

        public string ToUsd() =>
            "$" + amount.ToString("0.00", CultureInfo.InvariantCulture);
    }

    // Receiver type with no name: static members, called on the type itself,
    // as decimal.FromCents(1250).
    extension(decimal)
    {
        public static decimal FromCents(long cents) => cents / 100m;
    }
}

// decimal price = decimal.FromCents(1250);
// price.ToUsd()    ->  "$12.50"
// price.IsPositive ->  true

Modifying a struct with a ref receiver

Structs are passed by value, so an extension that mutates one silently changes a copy unless the receiver is ref. Only value types, or generic types constrained to struct, may be a ref receiver.

Example · csharp
public struct Counter
{
    public int Value { get; set; }
}

public static class CounterExtensions
{
    // Without `ref` the block receives a copy of the struct and the caller
    // sees no change at all. A separate block is needed for each mode.
    extension(ref Counter counter)
    {
        public void Increment() => counter.Value++;
    }
}

// Counter c = new() { Value = 1 };
// c.Increment();
// c.Value  ->  2

Discussion

  • Be the first to comment on this lesson.