Understanding Extension Methods in C#
One awesome feature I like about C# is the extension methods. Extension methods allow you to add new business logic to existing types without you have to modify their original implementation or you have to create derived classes.
I assume you know about LINQ and have used methods like Where(), Select(), or FirstOrDefault()? Do you know what they really are? Extension methods! 👌
In this collection post, I will show you how extension methods work, why they are handy, and how you can create your own to extend your own classes or framework types like e.g. IEnumerable<T>.
What Are Extension Methods?
As I just mentioned, an extension method makes it possible to "add" methods to an existing type. In your IDE at development time, they appear as instance methods on the type you extend, but really they are just static methods you define in a static class.
I often refer to this as syntactic sugar - why? It makes our code more readable, and enable you and others to write code in a fluent, and "API" friendly design.
An extension method must always:
- Be defined in a static class.
- Be a static method.
- Have
thiskeyword along with the first parameter. This tells us about the type being extended.
Alright - let's get to the fun part, and see it in action 🔥
Building a Custom LINQ-Style Method
Let's start soft by extending IEnumerable<T> with a custom find operation. Here is the code - I will explain below.
namespace ExtensionMethod;
internal static class IEnumerableExtensions
{
public static IEnumerable<T> CustomFind<T>(this IEnumerable<T> source, Func<T, bool> isMatch)
{
foreach (var item in source)
{
if (isMatch(item))
{
yield return item;
}
}
}
}Okay, let me break it down and tell you what is happening above.
The Method Signature
public static IEnumerable<T> CustomFind<T>(this IEnumerable<T> source, Func<T, bool> isMatch)this IEnumerable<T> source. Thethiskeyword makes this an extension method onIEnumerable<T>. Any collection implementing this interface can now callCustomFind().- Generic Type
<T>. Makes the method work with any type. Awesome part about that is it provides type safety 🙌 Func<T, bool> isMatch. A predicate function that figures out whether an item matches our criteria or not.
Deferred Execution with yield return
Do you care about performance? I do, and that is why I am using yield return inside the method, it is actually crucial when talking performance if you ask me.
foreach (var item in source)
{
if (isMatch(item))
{
yield return item;
}
}Why is yield return making it perform better? The reason is because it creates an iterator that provides deferred execution. Christian... what the h... does that mean? Let me try to explain that for you.
- The method doesn't process the entire collection instantly/immediately.
- Items are evaluated one at a time, and only when requested.
- If you only need the first matching item, you won't iterate through the entire collection of items.
- Multiple operations can be chained without creating intermediate collections.
The exact same pattern is actually used by LINQ methods like Where(), Select(), and Take(). You can check out their source implementation at the link below if you are curious.
Using the Extension Method
Now that we have the extension method and we know how any why it works, let's try using it in some practical code.
public class IEnumerableExtensionsDemo
{
public static void Execute()
{
List<int> numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Find all even numbers
var evenNumbers = numbers.CustomFind(n => n % 2 == 0);
Console.WriteLine("Even Numbers:");
foreach (var number in evenNumbers)
{
Console.WriteLine(number);
}
// Find all numbers greater than 5
var greaterThanFive = numbers.CustomFind(n => n > 5);
Console.WriteLine("Numbers Greater Than 5:");
foreach (var number in greaterThanFive)
{
Console.WriteLine(number);
}
}
}Can you see it now? Our extension method looks like it is a built-in method in the LINQ framework, but on the List<int> variable. Why does this work? It works because List<int> implements IEnumerable<int>, this makes our extension available automatically.
The result?
Even Numbers:
2
4
6
8
10
Numbers Greater Than 5:
6
7
8
9
10Why Not Just Use LINQ's Where()?
I know what you might be thinking. Why do that? Why not use the Where() method? and you are absolutely right! In a real-world solution, you would just stick to the LINQ method for solving that.
This was a demonstration, and with the code above, I was able to demonstrate the following:
- How LINQ actually works under the hood. Many LINQ methods are extension methods using similar patterns.
- How to create domain-specific extensions. You can build custom query methods tailored to your business logic. Cool right!?
- The power of extending framework types. You're not limited to what Microsoft provides, the sky is the limit.
When to Create Extension Methods on Framework Types?
With great power comes great responsibility, and extension methods on framework types are powerful, let me tell you that, and it is also the reason why you should be careful.
Below are some of my key takeaways for using them and avoiding them in my projects. You are welcome to follow that route or just stick to your own.
✅ I use them when I
- Create domain-specific query operations that compose well with LINQ.
- Add utility methods that would benefit from a fluent syntax - we are back to the "API-like" design here.
- Fill gaps in framework functionality for my specific needs in a solution.
- Have to create more readable alternatives to static utility methods.
❌ Avoid them when
- The functionality is too specific to one use case.
- They would confuse other developers (especially naming conflicts with future framework additions)
- A simple static helper method would be clearer for other developers.
Key Takeaways
- Extension methods are syntactic sugar. They're static methods that appear as instance methods.
- Use
thison the first parameter. This tells us the type being extended. - Deferred execution matters. Use
yield returnfor collection operations to maintain performance. ALWAYS! - Generic extension methods are powerful. They work across all types that match the constraint.
- Namespace matters: You need to use the
usingnamespace containing the extension class to access the methods. I often practice placing them in a global file for the project or placing them in a root namespace (if it makes sense - often the case in a class library).
End of part one
The end of the first post in this section. You should now have a great foundational knowledge about extension methods. By now I hope you have the idea that they make it possible for you to write expressive fluent code by extending on types.
When you learn to use them correctly, they can take your codebase to the next level and make it more like an API. I really enjoy reviewing code written in that style! Turn it into art, please 🙏
In the next and final post in this section, I will show you some cool and a bit more advanced scenarios. We will also take a look at what happens when an extension method clashes with instance methods, how the resolution of them works, and again some practical examples for how you can extend your own classes.