Extension Method Resolution
Understand how the compiler finds extensions.
Extension Method Resolution is a free C# Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
How the Compiler Finds Extensions
When you write value.Method() and no instance method matches, the compiler looks for an extension method. But it only considers extensions whose containing namespace is in scope through a using directive.
using Directives Bring Extensions Into Scope
An extension is only visible if you import its namespace. Without the right using, the method appears not to exist even though it is compiled into the assembly.
using System;
using MyHelpers; // brings StringExtensions into scope
namespace MyHelpers
{
public static class StringExtensions
{
public static string Reverse(this string s)
{
var arr = s.ToCharArray();
Array.Reverse(arr);
return new string(arr);
}
}
}
public class Program
{
public static void Main()
{
Console.WriteLine("hello".Reverse());
}
}Instance Methods Are Checked First
The compiler resolves the call in stages: first it searches for a matching instance method (including inherited ones). Only if none fits does it search for extension methods.
using System;
using System.Collections.Generic;
public static class ListExt
{
// Ignored: List<int> already has Add(int)
public static void Add(this List<int> list, int a, int b)
=> list.Add(a + b);
}
public class Program
{
public static void Main()
{
var list = new List<int>();
list.Add(5); // the built-in instance Add wins for this signature
Console.WriteLine(list[0]);
}
}Closer Namespaces Are Preferred
If two extensions match, the one whose namespace is nearer in the nesting hierarchy wins. An ambiguity that the compiler cannot break causes a compile error, forcing you to disambiguate.
using System;
namespace Outer
{
public static class A { public static string Tag(this int n) => "A" + n; }
namespace Inner
{
public static class B { public static string Tag(this int n) => "B" + n; }
public class Program
{
public static void Main()
{
// B is in the nearer (Inner) namespace, so it is preferred
Console.WriteLine(7.Tag());
}
}
}
}Disambiguating With a Static Call
When extensions conflict or you want to be explicit, call the static method directly using its full class name. This bypasses extension resolution entirely.
using System;
public static class FormatExt
{
public static string Bracket(this string s) => "[" + s + "]";
}
public class Program
{
public static void Main()
{
// Explicit static call, no ambiguity possible
Console.WriteLine(FormatExt.Bracket("x"));
}
}Overload Resolution Among Extensions
If several extension methods share a name, normal overload resolution picks the best match by parameter types, just like ordinary method overloading.
using System;
public static class PrintExt
{
public static string Describe(this int n) => "int:" + n;
public static string Describe(this string s) => "str:" + s;
}
public class Program
{
public static void Main()
{
Console.WriteLine(42.Describe());
Console.WriteLine("hi".Describe());
}
}Generic Inference During Resolution
For generic extensions, the compiler infers the type arguments from the receiver and arguments. If inference fails, the extension is not considered.
using System;
using System.Collections.Generic;
public static class SeqExt
{
public static T FirstItem<T>(this IEnumerable<T> source)
{
foreach (var item in source) return item;
throw new InvalidOperationException("empty");
}
}
public class Program
{
public static void Main()
{
var nums = new List<int> { 5, 6, 7 };
// T inferred as int from the List<int> receiver
Console.WriteLine(nums.FirstItem());
}
}Global usings and Implicit Imports
Modern C# can declare global using directives so a namespace is in scope across the whole project. This makes shared extension libraries available everywhere without repeating imports.
using System;
// In a real project a separate file might say: global using MyHelpers;
namespace MyHelpers
{
public static class IntExt { public static int Twice(this int n) => n * 2; }
}
public class Program
{
public static void Main()
{
// Works here because MyHelpers is referenced explicitly
Console.WriteLine(MyHelpers.IntExt.Twice(8));
}
}When the Receiver Type Matters
Resolution depends on the static type of the receiver, not its runtime type. An extension declared for a base type applies even when the variable holds a derived instance, unless a more specific extension is in scope.
using System;
public class Animal { }
public class Cat : Animal { }
public static class AnimalExt
{
public static string Kind(this Animal a) => "animal";
}
public class Program
{
public static void Main()
{
Animal a = new Cat();
// Chosen by static type Animal
Console.WriteLine(a.Kind());
}
}A Mental Model for Resolution
Think of the order as: (1) instance methods, then (2) extension methods from imported namespaces, nearest first, then (3) overload resolution among the candidates. Knowing this order explains almost every surprising result.
using System;
public static class Ext
{
public static string Wrap(this string s) => "<" + s + ">";
}
public class Program
{
public static void Main()
{
// No instance Wrap exists, so the extension is used
Console.WriteLine("node".Wrap());
}
}Try It Yourself
Define an extension in a namespace, import it, and call it. Then call the same method explicitly as a static method to see both routes work.
using System;
using Text.Helpers;
namespace Text.Helpers
{
public static class StringExtensions
{
public static int Vowels(this string s)
{
int count = 0;
foreach (var c in s.ToLower())
if ("aeiou".IndexOf(c) >= 0) count++;
return count;
}
}
}
public class Program
{
public static void Main()
{
Console.WriteLine("education".Vowels());
Console.WriteLine(Text.Helpers.StringExtensions.Vowels("education"));
}
}Quick Check
Consider what makes an extension method visible.
Recap
Extension method resolution follows a clear order.
- Instance methods are tried first; extensions only fill gaps.
- An extension is visible only if its namespace is in scope via
usingorglobal using. - Nearer namespaces win; unresolved ties are compile errors.
- You can always call the static method explicitly to disambiguate.
- Resolution uses the static type of the receiver.
Frequently asked questions
Is the “Extension Method Resolution” lesson free?
Yes — the full text of “Extension Method Resolution” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Extension Method Resolution”?
Understand how the compiler finds extensions. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Extension Method Resolution” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Defining Extension Methods
- Extending Interfaces and Generics
- Extension Method Resolution
- Designing Good Extensions