Last Updated: June 6, 2026
An extension method is a static method that the compiler lets you call as if it were an instance method on some other type. Extension methods add convenience methods to types you don't own or can't change, like string, decimal, List<T>, or any third-party class, without subclassing, wrapping, or recompiling them. This chapter covers the syntax, the rule the compiler follows to rewrite the call, the edge cases, and when to use an extension method versus a regular helper.
Consider a small validator for product codes. The rule is: a valid product code is exactly eight characters, starts with P-, and the rest are digits. One option is a static helper:
That works, but the call site reads as ProductCodeValidator.IsValidProductCode(code). The thing being validated, code, is the argument, not the subject of the sentence. With a chain of these checks (IsValidProductCode, IsValidEmail, IsValidPostalCode), the code starts looking like a pile of static-class names instead of "do this thing to this value."
A more readable form is code.IsValidProductCode(), as if IsValidProductCode were a method on string. System.String is sealed, and even if it weren't, polluting a built-in type with project-specific methods is a bad idea.
Extension methods provide this. A small adjustment to the declaration above turns it from "a static helper passed a string" into "a method string appears to have":
Two small differences. The class was renamed ProductCodeExtensions (the convention), and the first parameter got the this keyword. That this is the whole feature. Everything else, the static class, the static method, the body, behaves the same.
The call code.IsValidProductCode() is shorthand. The compiler emits the same call as the static version, ProductCodeExtensions.IsValidProductCode(code). Runtime behavior is identical. The benefit is at the call site: better reading order, IntelliSense suggests the method when you press dot on a string, and chaining works naturally.
static class, static Method, this Type selfThe rules for declaring an extension method are strict and small. Get any of them wrong and the compiler rejects the code.
A valid extension method requires:
static (no instances, no constructors).static.this modifier.Put together:
Now any decimal in scope of DecimalExtensions looks like it has a ToUsCurrency() method:
The first parameter doesn't have to be called anything in particular. this decimal amount, this decimal value, this decimal self, all work. The name is a choice; the this keyword is what marks it as an extension. Some teams pick a consistent name (self, source, or the lowercased type name) for readability.
Beyond the first parameter, extension methods accept normal parameters like any other method:
At the call site, only the second and later parameters are passed explicitly. The first one comes from the value on the left of the dot.
Extension methods are a compile-time feature. The C# compiler sees value.Method(args) and tries instance methods first, then falls back to extension methods in scope. If it finds an extension method, it rewrites the call into the equivalent static call.
These two lines compile to the exact same IL:
Both go through call ProductCodeExtensions.IsValidProductCode(string) in the compiled assembly. The dot syntax is a convenience, not a different mechanism.
The rewriting picture:
A few consequences fall out of this:
A small demonstration that the call site is syntactic sugar:
Both forms compile to the same call. The first reads better, but the second is always available.
Extension methods have zero runtime overhead compared to a normal static method call. They're a compile-time rewrite, nothing more. Don't avoid them on performance grounds.
using Directive Must Be In ScopeBecause the compiler picks the extension method while compiling, the method must be visible in the current file's set of imported namespaces. If the namespace containing the extension class isn't pulled in with a using directive (or a global using), the compiler won't see the method, and the code.IsValidProductCode() call will fail with a normal "method not found" error.
Suppose the extensions live in a namespace:
Then in another file:
The error message says string doesn't have such a method. Strictly that's true, but a clearer framing is "the compiler couldn't find an extension method named IsValidProductCode because no extension classes are in scope." Adding the namespace fixes it:
That dependency on using directives is what keeps extension methods from becoming a global mess. Only methods from imported namespaces are visible. Two libraries can each define string.NormalizeEmail() with different implementations, and the chosen one is selected by which namespace is imported.
If a project uses global using directives (introduced in C# 10), the import lives in one place for the whole project. That's common in projects that ship a lot of extensions and don't want every file to repeat the same imports.
The most-used extension methods in .NET are the LINQ operators on IEnumerable<T>. Methods like Where, Select, OrderBy, First, Count, and ToList aren't methods on the List<T> or array types they're called on. They're extension methods on IEnumerable<T>, defined in the System.Linq.Enumerable static class.
products.Where(...) doesn't call a method on List<Product>. The compiler resolves it to Enumerable.Where<Product>(products, p => p.Stock > 0), because:
List<T> doesn't define a Where method itself.List<T> implements IEnumerable<T>.System.Linq.Enumerable has public static IEnumerable<T> Where<T>(this IEnumerable<T> source, ...).using System.Linq; in scope.Drop the using System.Linq; and the call breaks with CS1061. The methods didn't disappear, they just stopped being visible.
This is a major use case for extension methods. The BCL team added LINQ in C# 3 without changing a single line of List<T>, Array, Dictionary<TKey, TValue>, or any other existing collection. Every collection that implements IEnumerable<T> gained the same fifty-plus methods, and old code kept compiling because nothing about the collections themselves changed.
Extensions for IEnumerable<T> are also straightforward to write. The following is a small filter helper for products in stock:
The extension is on IEnumerable<Product>, not List<Product>. That means it works on arrays, lists, sets, the result of another LINQ call, anything that's enumerable. Extending the most general type the method can handle composes with everything.
Wrapping a Where in a custom extension method has no extra cost. It's deferred like a raw Where call: no iteration happens until something pulls items out. The wrapper compiles to the same call, plus one more frame on the stack during enumeration.
Extension methods look like instance methods, but they aren't, and the cases where the illusion breaks need attention.
If the type already has an instance method with the same name and matching parameters, the instance method is picked first. The extension is ignored, with no error or warning.
Both methods exist, both are in scope, and both have compatible signatures. The compiler picks the instance method without flagging the extension. The rule: extension methods fill in gaps, they don't override.
This becomes an issue when a library adds an instance method in a new version that collides with an extension. The code still compiles, the extension just stops being called. Code depending on the extension's behavior sees a silent change. Avoid extension method names that may collide with future instance methods.
An extension method is a static method in a separate class. It has no special privileges. It can't touch private or protected members of the type it extends. Only the public API is available to the extension.
If the natural implementation needs the type's private state, an extension method doesn't fit. Add a real method to the class. Extensions are for things that can be built on top of the type's public surface.
When the compiler sees value.Method(args), it follows a specific search order:
The order is: instance methods (including inherited ones), then extension methods in scope. If multiple extension methods match, the one in the closest using namespace wins, with ambiguity rules that mostly mirror normal overload resolution. The shortcut: instance always beats extension.
Extension methods bind to the compile-time type of the receiver, not the runtime type. An extension method cannot be "overridden" in a subclass.
a is typed as Animal, even though it holds a Dog at runtime. The compiler picks the extension that matches the compile-time type, so Animal's version wins. A real virtual method on a class would have printed Dog thanks to runtime dispatch. Extension methods don't get that. If polymorphism matters, use a virtual method or an interface.
static or sealedsealed classes (like string) are fine to extend with extension methods, which is a big part of the point. static classes are not. An extension method can't be added to a static class, because there can never be a value of that type to call it on. The first parameter this StaticClass self doesn't make sense.
Extension operators are not yet supported (until the future extension feature lands), neither are extension properties or fields in C# through C# 13, and there are no extension events. Those features are under discussion in the language design, but as of .NET 8, only extension methods are available.
A more complete example uses the three running themes for this chapter: validating product codes on string, formatting currency on decimal, and filtering List<Product>. All three live in one project, in one namespace.
From the example: the call site reads p.Code.IsValidProductCode(), not Validator.IsValidProductCode(p.Code). The currency formatting reads p.Price.ToUsCurrency(), not a helper call. The list filter, products.OnlyInStock(), chains with LINQ's ToList(). Three small extension classes, one namespace import, and a lot of boilerplate at every call site disappears.
The naming follows the common pattern: {Type}Extensions for the static class (StringExtensions, DecimalExtensions). When extending a more specific shape, name the class for the shape (ProductEnumerableExtensions because it operates on enumerables of products). The convention isn't enforced by the compiler, but C# codebases consistently use it, and IDEs scan for it when offering refactorings.
Extension methods are a tool that fits some places and not others. The rule of thumb: extension methods are for adding methods to types you don't own or shouldn't change. If you own the type and the method belongs there, add a real method.
A practical decision table:
| Situation | Use extension? | Why |
|---|---|---|
Add helpers to string, int, decimal, DateTime | Yes | You can't modify BCL types. |
Add helpers to IEnumerable<T> or IQueryable<T> | Yes | Interfaces can't carry implementations like classes can (default methods aside). LINQ does it for a reason. |
| Add helpers to a NuGet-package type you don't own | Yes | You can't add real members. |
| Add a method to a class you wrote, in your own project | No | Add it as a real method. Discoverability and access to private state are better. |
| Add a method that needs private fields of the target type | No | Extension methods can't access privates. |
| Add a method that should be virtual or polymorphic | No | Extensions bind at compile time and don't support overriding. |
| Wrap a third-party type with a fluent shape | Sometimes | Extensions can be the friendly face for an awkward API. |
The "shouldn't change" case is real. A project that depends on a generated client (gRPC, OpenAPI, EF Core scaffolded entities) loses its changes on regeneration. Putting helpers in extension methods, in a separate static class, keeps them out of the regeneration path entirely.
Two more rules of thumb:
object, so an extension on object shows up in IntelliSense for everything. That clutters the auto-complete list across the entire project. If something is needed for all types, use a generic extension with a constraint, not this object.using brings them all into scope. Splitting them across many small namespaces forces callers to import a lot.Conventions for extension method classes are consistent across .NET, and they're worth following so the code matches everything else.
Class name: {TargetType}Extensions.
string, use StringExtensions.decimal, use DecimalExtensions.ProductEnumerableExtensions, not IEnumerable<Product>Extensions (which isn't valid syntax anyway).I: EnumerableExtensions, not IEnumerableExtensions. Microsoft does both in the BCL; the leading I form is slightly more common in newer code.Class visibility: public if the extensions are part of the library's surface, internal if they're only for use inside the project. Don't make them private; a private static class can't be in scope from anywhere useful.
Method name: Same conventions as any C# method, PascalCase, verb-first, specific. IsValidProductCode reads as a question and returns bool. ToUsCurrency formats. OnlyInStock filters. Don't prefix names with Extension or Ext; the call site already makes that clear.
File layout: One static class per file is the common pattern, named the same as the class (StringExtensions.cs). For a small project, grouping a few related extension classes in one file is fine. For a library that publishes extensions as part of its API, one-class-per-file is standard.
Namespace: Put extensions in a namespace that callers will want to import. If a library project is AlgoMaster.Store, the extensions might live in AlgoMaster.Store.Extensions. Some libraries put extensions directly in the namespace of the type they extend, so importing the main namespace also brings in the extensions. This is a judgment call.
A small concrete layout:
Each file in Extensions/ contains exactly one static class named after its file, with public static methods that all start with this {SomeType} .... A consumer adds using AlgoMaster.Store.Extensions; and gets everything.
One final note: don't add extension methods to types from another library's namespace in a way that hides where they came from. If a caller looks at cart.RecalculateTax() and tries to find the method in the Cart source code, they shouldn't have to hunt through six different extension classes. A clear {Type}Extensions name and a sensible namespace make that lookup fast.
9 quizzes