Skip to main content

Extension Methods in Practice in C#

In my previous post (part one), I showed you the fundamentals of extension methods by extending IEnumerable<T>.

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(

In this post I will show you a few things to be aware of that I have learned the hard way.

  • What happens when extension methods share name with instance methods?
  • How does the compiler decide what code it should call?
  • What real-world patterns to use.

I will show you a real-world example of a Order class, where we will add extension methods.

A Simple Order Class

Most projects I have been involved in, has something to do with order management. You might have guessed it - let's start with a basic Order class that represents a customer order in a system.

namespace ExtensionMethod;

internal class Order
{
    public int OrderId { get; set; }
    public double TotalAmount { get; set; }
    public bool IsShipped { get; set; }
    public string CustomerName { get; set; }
    
    public Order(int orderId, double totalAmount, bool isShipped, string customerName)
    {
        OrderId = orderId;
        TotalAmount = totalAmount;
        IsShipped = isShipped;
        CustomerName = customerName;
    }
    
    public override string ToString() => 
        $"Order ID: {OrderId}, Customer: {CustomerName}, Total: {TotalAmount:C}, Shipped: {IsShipped}";
    
    public void ApplyDiscount(double flatDiscount)
    {
        TotalAmount -= flatDiscount;
    }
}

Did your sharp eyes see the ApplyDiscount(double flatDiscount) and the ToString() methods? If not, read again 😄

Extending the Order Class

Alright - let's extend the Order class with some useful extension methods.

internal static class OrderExtensions
{
    // Extension method to apply a percentage discount
    public static void ApplyDiscount(this Order order, double discountPercentage)
    {
        order.TotalAmount -= order.TotalAmount * (discountPercentage / 100);
    }
    
    // Extension method to check if the order is high-value
    public static bool IsHighValue(this Order order, double threshold)
    {
        return order.TotalAmount > threshold;
    }
    
    // Extension method to mark an order as shipped
    public static void MarkAsShipped(this Order order)
    {
        order.IsShipped = true;
    }
}

Confused? That was the intention 😅 We now got two methods named ApplyDiscount. What is the difference, Christian?!

  • One instance method in the Order class (takes a flat discount)
  • One extension method in OrderExtensions (takes a percentage)

How does this work? Let's find out.

Method Overloading and Resolution Rules

This is where I messed up in the beginning of my career, and would like to teach you a thing, continue reading. Let's take a look at the usage of these methods.

public class OrderExtensionsDemo
{
    public static void Execute()
    {
        Order order = new(101, 1200.50, false, "Christian");
        Console.WriteLine(order);  // Initial Order
        
        // Apply a flat discount of $100 (Order Instance Method)
        order.ApplyDiscount(flatDiscount: 100);
        
        // Apply a 10% discount (Order Extension Method)
        order.ApplyDiscount(discountPercentage: 10);
        
        Console.WriteLine("After Discount:");
        Console.WriteLine(order);
        
        // Check if the order is high-value with a threshold of $1000
        bool isHighValue = order.IsHighValue(1000);
        Console.WriteLine($"Is High Value: {isHighValue}");
        
        // Mark the order as shipped
        order.MarkAsShipped();
        Console.WriteLine("After Marking Shipped:");
        Console.WriteLine(order);
    }
}

So... why is the code above safe and works as intended? Named arguments save the day 🙈 Did you notice the use of named arguments in the discount calls I made?

order.ApplyDiscount(flatDiscount: 100);        // Calls instance method
order.ApplyDiscount(discountPercentage: 10);   // Calls extension method

This saves our ass. By using named arguments, we can achieve

  1. Disambiguate between methods with the same name.
  2. Self-documenting code—readers immediately understand the difference.
  3. Leverage method overloading between instance and extension methods.

Method Resolution Priority

The C# compiler follows these rules when resolving method calls - I had to go the hard way to find out, so here are the golden points.

  1. Instance methods have priority. If an instance method matches the signature, it's chosen over an extension method.
  2. Extension methods are considered next. If no instance method matches, the compiler looks for extension methods.
  3. Closer namespaces win. Extension methods in the current namespace are preferred over those in imported namespaces. Did you know that?
  4. Ambiguity causes compilation errors. If multiple extension methods match equally well, you'll get a compiler error.

What If We Don't Use Named Arguments?

Without named arguments, this code would be ambiguous. If you were using an IDE, it would warn you about this because of the compiler.

order.ApplyDiscount(10);  // Which method?

It's pretty simple, the compiler can't figure out what you want, since it has two options and both takes in the same parameter type.

  • A $10 flat discount (the instance method)
  • A 10% discount (the extension method)

Both methods takes in a double as parameter, so without named arguments, the call is ambiguous.

Running the Example

I have enabled debugging and placed a ton of break points in my code to trace what is happening. Let's break it down, and I will explain step-by-step what is happening.

Order ID: 101, Customer: Christian, Total: $1,200.50, Shipped: False

After flat discount of $100, we will have the following result

Total: $1,100.50

After 10% discount, the value will be

Total: $990.45  // $1,100.50 - ($1,100.50 * 0.10)

After checking high-value status, we got

Is High Value: False  // $990.45 is not > $1,000

After marking it as shipped, we will have the following

Order ID: 101, Customer: Christian, Total: $990.45, Shipped: True

Real-World Patterns and Best Practices

I have learned a lot, and also the hard way. Below are some of my best practices I have learned during the years of development and making architecture. You can skip them, read them and pick what you think works for you.

Use Extension Methods to Separate Concerns

I have come to the conclusion that extension methods are perfect for adding functionality without bloating your core classes.

// Core class stays focused on data and essential operations
class Order { /* ... */ }

// Business logic extensions
static class OrderBusinessExtensions { /* ... */ }

// Validation extensions
static class OrderValidationExtensions { /* ... */ }

// Formatting extensions
static class OrderFormattingExtensions { /* ... */ }

Why do this? It gives us the option to keep our classes focused and makes it easier to test different aspects of our codebase independently. What's not to like!?

Consider Mutability Carefully

Have you noticed that my extension methods mutate the Order object?

public static void MarkAsShipped(this Order order)
{
    order.IsShipped = true;  // Mutating state
}

Why does that work? It's simple - the Order is a reference type. But I would strongly encourage you to consider whether you want your extension methods to mutate the state or return a new instance, like here.

// Mutating (current approach)
order.ApplyDiscount(10);

// Immutable alternative
var discountedOrder = order.WithDiscount(10);

The only thing you have to be aware of is that for value types (structs), mutation in extension methods will not work as you expect, without you are using the ref keyword, as I am showing here.

public static void ExtensionMethod(ref this MyStruct s)  // Note the ref
{
    s.Value = 42;
}

Use Meaningful Names and Avoid Conflicts

When I am naming my extension methods, I have some rules.

  • Always be specific. MarkAsShipped() is better than Ship().
  • Follow conventions. If it returns bool, consider starting with Is, Has, or Can. It makes the codebase easier to read and maintain in the future.
  • Avoid collision with future framework methods. Don't create extensions that might conflict with future .NET versions. I know this is a hard to know about, but try to stay out of their framework.

Let’s say you add an extension method to a very common .NET type, like here.

public static class StringExtensions
{
    public static bool IsNullOrEmpty(this string value)
    {
        return string.IsNullOrEmpty(value);
    }
}

This works today, but it’s risky! But why?

  • string.IsNullOrEmpty already exists as a static method.
  • .NET could add an instance method called IsNullOrEmpty() in a future version.
  • If they do, your extension method either
    • Stops being called.
    • Causes ambiguity errors.
    • Or behaves differently across .NET versions.

That’s a collision with the framework.

Consider Discoverability

Extension methods are only available when the namespace is imported. That is why I always try to group related extension into one namespace, and disable the pragma warning in the IDE for it.

using ExtensionMethod;  // Now OrderExtensions are available

Group related extensions in the same namespaces, will help you and other developers discover them.

Document Your Parameter Expectations

You should always use named parameters when it improves clarity of your extension method. Like shown here.

// Clear intent
order.IsHighValue(threshold: 1000);

// Less clear
order.IsHighValue(1000);  // 1000 what?

When to Use Extension Methods vs. Instance Methods?

Use Instance Methods When

  • ✅ The functionality is core to the type's purpose.
  • ✅ The method needs access to private members.
  • ✅ The method is part of the type's contract/interface.
  • ✅ Polymorphism is required (virtual/override).

Use Extension Methods When

  • ✅ You don't own the type (framework types, third-party libraries).
  • ✅ The functionality is supplementary or domain-specific.
  • ✅ You want to separate concerns across different assemblies.
  • ✅ You're creating fluent APIs or query helpers.
  • ✅ You want to avoid bloating a core class.

Extension Method Chains

Remember i mentioned something about writing code that looks like a nice API? This is what chaining allows us to do. Imagine being able to do this:

var processedOrder = order
    .ApplyDiscount(discountPercentage: 15)
    .MarkAsShipped()
    .GenerateInvoice()
    .SendConfirmation();

For this to work, each extension method needs to return the order (or a modified version).

public static Order MarkAsShipped(this Order order)
{
    order.IsShipped = true;
    return order;  // Enable chaining
}

Key Takeaways

As always, I have some key takeaways 😄

  1. Instance methods always win. They have priority over extension methods in method resolution, that's just how it is.
  2. Named arguments disambiguate. Use them when you have overloaded instance and extension methods.
  3. Extension methods enable separation of concerns. Keep core classes focused, extend with specialized functionality. This also promotes testability of your code.
  4. Be mindful of mutability. Decide whether your extensions should mutate or return new instances.
  5. Namespace organization matters. Group related extensions and use clear naming conventions, that makes it easier to discover all extensions for an object.
  6. Extension methods can't access private members. They operate on the public surface of a type. I did not mention that earlier, but now you know.

Summary

An extension method is a very powerful tool in the developer toolbox. It gives us the option to write clean, and maintainable code. With extensions we can easily extend both built-in framework types and our own classes without violating the Single Responsibility Principle (from SOLID).

SOLID Principles - The art of writing maintainable code 👨‍💻
Learn what the SOLID principles are about and how you can use them to write clean and maintainable code in your applications. With C# examples.

The big key is to use them when needed. Only create extensions when it makes sense and keep your method names clean and clear. Always go for named arguments when you got an ambiguity.

If you combine extension methods with LINQ, generics and other awesome C# features, your extension methods will become cornerstones. If you enjoyed this post, let me know in the comments. Until next time - happy coding! ✌️

Updated on Feb 10, 2026