Skip to content

~ / blog / csharp-extension-methods

C# Extension Methods: Adding Methods to Types You Do Not Own

C# 10 min read

The LINQ post left one thing unexplained. Where and Select are not defined on List<T>, and yet you call them on a list with a dot, exactly as if they were. That is not magic and it is not a compiler special case for LINQ. It is a feature you can use yourself, in ten lines, on any type in .NET including string and int. This post is how it works.

The problem extension methods solve

Say every service in your codebase needs to know whether a title is a “long title”, meaning more than twenty characters. string is sealed and owned by Microsoft, so you cannot add a method to it. The classic answer is a static helper class:

public static class StringHelper
{
    public static bool IsLongTitle(string value)
    {
        return value != null && value.Length > 20;
    }
}

// at the call site
if (StringHelper.IsLongTitle(book.Title)) { ... }

It works. The problem is the call site. The subject of the sentence is book.Title, but you have to write the helper class first and bury the actual subject inside brackets. Chain two helpers together and it inverts entirely:

var slug = StringHelper.Truncate(StringHelper.Slugify(book.Title), 40);

You read that inside out, right to left. That is the problem: helper classes put the verb before the noun, and nesting them reverses the reading order.

The extension method version

Add one keyword, this, to the first parameter of a static method in a static class:

public static class StringExtensions
{
    public static bool IsLongTitle(this string value)
    {
        return value != null && value.Length > 20;
    }
}

The only change is this string value instead of string value. Now:

if (book.Title.IsLongTitle()) { ... }

IsLongTitle appears in IntelliSense on every string in the project, with a slightly different icon. And the chained version reads left to right, in the order it happens:

var slug = book.Title.Slugify().Truncate(40);

There is no magic here

This matters for interviews and for reasoning about your own code. The compiler rewrites the extension call back into the plain static call:

book.Title.IsLongTitle();          // what you write
StringExtensions.IsLongTitle(book.Title);   // what the compiler emits

Nothing is added to string. The type is not modified, no runtime patching happens, nothing is reflected over. It is syntax, resolved at compile time, and everything follows from that one fact:

  • An extension method cannot access private or protected members of the type. It is an outsider, holding a normal reference, subject to normal access rules.
  • An extension method cannot override an existing method. If the type already has an instance method with a matching signature, the instance method always wins, silently. Your extension is simply never called.
  • Extension methods are not polymorphic. They are bound at compile time based on the declared type of the variable, not the runtime type. Declare a variable as object and string extensions disappear from it.
  • Calling one on null does not throw by itself, because there is no instance dispatch. ((string)null).IsLongTitle() runs the method with value as null, which is why the method above checks for null. That is a genuinely useful property, and string.IsNullOrEmpty-style extensions rely on it.

The rules for declaring one

Four requirements, and the compiler enforces all of them:

public static class BookExtensions      // 1. class must be static
{
    public static bool IsExpensive(     // 2. method must be static
        this Book book,                 // 3. first parameter marked with 'this'
        int threshold = 10)             //    further parameters are normal
    {
        return book.Price > threshold;
    }
}

The fourth rule is scope: the extension is only visible where its namespace is in scope. That is the whole answer to “why do I need using System.Linq; to see Where”. Where lives in the System.Linq namespace, and without the using the compiler will not consider it. Modern projects have implicit usings that include System.Linq, which is why it usually just works now.

Because the first parameter is a normal parameter after the this, extension methods can be generic, take further arguments, and have defaults:

new BookRepository().GetBooks()
    .Where(b => b.IsExpensive())        // your extension, inside a LINQ lambda
    .ToList();

This is how LINQ is built

Now the LINQ post’s loose end ties off. Here is a rough version of Where, and it is genuinely close to the real thing:

public static class MyLinq
{
    public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        foreach (var item in source)
        {
            if (predicate(item))
                yield return item;
        }
    }
}

Read what that combines. Generics from the first post, so it works for any T. Delegates from the second, so the caller supplies the decision. Lambdas from the third, so supplying it is one line at the call site. Extension methods from this post, so it reads as if IEnumerable<T> had the method all along. And yield return, which is the next post.

Then it actually works:

var expensive = books.MyWhere(b => b.Price > 10);

That is the real design insight behind LINQ, and it is the reason the interface IEnumerable<T> has exactly one method on it while supporting fifty operators. If Where, Select and OrderBy were instance methods on the interface, every collection type ever written would have to implement all fifty. As extension methods, they are written once, against the smallest possible interface, and every present and future type that implements IEnumerable<T> gets them free. Extension methods let a library add behaviour to an interface without breaking every existing implementer.

When to use them, and when not to

Reach for an extension method when:

  • The type is not yours to change: string, DateTime, int, an interface from a NuGet package, generated clients.
  • You are adding behaviour to an interface that should apply to every implementation, which is the LINQ case.
  • The call reads better as a chain than as nested static calls.
  • You want the method to work safely on null for the receiver.

Prefer a normal method or a service when:

  • You own the type. If Book is yours, book.IsExpensive() should be a method on Book. An extension method is what you use when the front door is locked, not a way to avoid opening it.
  • The logic needs private state. An extension cannot see it.
  • The behaviour has dependencies, hits a database, or needs injecting for tests. Extension methods are static, so they cannot be mocked or injected. Static behaviour hidden behind instance-looking syntax is a real testing problem: the call site looks like a normal method call, so nobody notices the hard dependency until they try to write a unit test for it.
  • You would be extending object. That adds a method to every type in the program and pollutes IntelliSense everywhere.

One team convention worth adopting: put extensions in a class named <Type>Extensions in a namespace people must opt into deliberately, and keep them free of side effects. Extension methods that quietly mutate their receiver surprise readers, because the syntax suggests something belonging to the type while the behaviour belongs to you.

The same idea inside Microsoft, Apple and Google

Microsoft (ASP.NET Core). Every line of a Program.cs startup file is extension methods, and this is the pattern’s biggest showcase:

builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(cs));
app.UseAuthentication();
app.MapGet("/books", () => books);

AddControllers, AddDbContext, UseSqlServer, UseAuthentication, MapGet are all extension methods on IServiceCollection, IApplicationBuilder or DbContextOptionsBuilder. That is how a NuGet package adds one line of configuration to your app without Microsoft ever editing the interface. Install a package, add a using, and a new verb appears. This is the plugin model of the entire .NET ecosystem, built entirely from this one feature.

Apple (Swift). Swift has extension as a first-class keyword, and unlike C# it can add computed properties and conform an existing type to a new protocol:

extension String {
    var isLongTitle: Bool { count > 20 }
}

Apple’s own SDKs use it so heavily that a single Swift type’s behaviour is often spread across a dozen extension blocks by feature.

Google (Kotlin). Kotlin extension functions are the same feature with the receiver moved into the name, and they are equally central to Android:

fun String.isLongTitle(): Boolean = length > 20

Kotlin’s whole standard library, including filter and map from the LINQ post, is extension functions on Iterable. Same compile-time rewrite, same reason.

JavaScript, and why the idea has a bad name there. JavaScript has no extension methods, so libraries used to modify built-in prototypes instead (Array.prototype.contains = ...). That is actual runtime modification, it is global, and when two libraries did it differently, things broke; the community named it monkey patching and largely banned it. C# extension methods look similar and are not: they are scoped to a namespace, resolved at compile time, and never change the type. Say that in an interview if someone calls them monkey patching.

Summary

  • An extension method is a static method whose first parameter is marked this, letting you call it as if it were an instance method on that type.
  • It is pure compile-time syntax. The compiler rewrites x.Foo() into Extensions.Foo(x); the type itself is untouched.
  • It cannot access private members, cannot override an existing instance method, and is not polymorphic.
  • It does not throw on a null receiver, so a good extension checks for null itself.
  • It is only visible where its namespace is imported, which is why LINQ needs using System.Linq.
  • LINQ, ASP.NET Core startup and most of the .NET package ecosystem are built from extension methods on interfaces.
  • Use them for types you do not own and interfaces you want to enrich. Use a normal method when you own the type, and avoid them for anything with injectable dependencies, because static methods cannot be mocked.

Interview questions and answers

1. What is an extension method?

A static method in a static class whose first parameter is prefixed with this, which allows it to be invoked with instance-method syntax on that parameter’s type. It lets you add callable behaviour to a type you cannot modify, such as string or an interface from a library.

2. How does the compiler handle an extension method call?

It rewrites it into an ordinary static method call. title.IsLongTitle() becomes StringExtensions.IsLongTitle(title). Resolution happens entirely at compile time, and nothing about the target type changes at runtime.

3. What are the requirements for declaring one?

The containing class must be static and non-generic, the method must be static, the first parameter must be marked this, and the namespace containing the class must be in scope at the call site.

4. Can an extension method access private fields of the type it extends?

No. It is an ordinary static method holding a reference, so it obeys the same accessibility rules as any other outside caller. It can only use the public surface of the type.

5. What happens if an extension method has the same signature as an instance method?

The instance method wins, always, with no error or warning. Extension methods are only considered after the compiler fails to find a suitable instance method, which also means adding an instance method to a class later can silently take over calls that previously went to your extension.

6. Can you call an extension method on a null reference?

Yes. There is no instance dispatch involved, so no null check happens before the call, and the method runs with a null first parameter. This is deliberate and useful, but it means the method must handle null itself if it dereferences the value.

7. Why is LINQ implemented with extension methods rather than interface methods?

So that a single implementation on IEnumerable<T> serves every collection type, present and future, without each one having to implement fifty operators. Adding methods to an interface is a breaking change for every existing implementer; adding extension methods is not.

8. Are extension methods polymorphic?

No. Overload resolution uses the compile-time declared type of the receiver, not the runtime type. If a variable is declared as object and holds a string, string extensions are not available, and if extensions exist for both a base type and a derived type, the one chosen depends on how the variable is declared.

9. What are the downsides of extension methods?

They are static, so they cannot be injected or mocked, which makes them a poor place for anything with dependencies or I/O. They can be hard to discover because they live in another file and require a using. And they can hide the fact that behaviour lives outside the type, which is confusing when you actually own the type and should have put the method on it.

10. How do extension methods differ from monkey patching?

Monkey patching modifies a type at runtime, globally, for everyone in the process. Extension methods change nothing at runtime: they are compile-time syntax, scoped to whoever imports the namespace, cannot override existing members, and cannot touch private state. Same convenience, none of the global mutation.

# tags csharpextension-methodslinqdotnetinterview-questions
$ share linkedin x / twitter

Tayyab Shahid, Senior Software Engineer in London

Tayyab Shahid

Senior Software Engineer & .NET Consultant · London, UK

about github linkedin

§ related posts

Building something like this?

I take on freelance .NET projects, consulting, and senior roles.

get in touch

← cd ~/blog

CV