Sitemap
iamprovidence

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence

Creative use of Extension Methods

9 min readJan 17, 2026

--

Press enter or click to view image in full size

While the rest of the world can simply define a function, C# was designed in a way that all behavior belongs to a type 😒.

So if you need some functionality that does not depend on an instance, you still need to define a class with a static method:

public static class MathUtils
{
public static bool IsEven(int number)
{
return number % 2 == 0;
}
}

// Usage:
bool result = MathUtils.IsEven(10);

However, this does not read fluently and poorly integrates cleanly into the object-oriented style of C#. When calling them, you must reference the helper class (MathUtils.IsEven(input)), even if the logic conceptually belongs to the target type.

Therefore, Microsoft decided to add extension methods.

They are just pure syntax sugar that allows you to call a static method on an instance:

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

// Usage:
string name = "";
bool result = name.IsNullOrEmpty(); // true

Although this is a traditional way of calling them, you can still invoke them through the defining static class, just like a regular static method:

string name = "";
bool result = StringExtensions.IsNullOrEmpty(name); // true

The cool thing about them, that you can add new functionality to existing classes, enhance built-in types, or third-party libraries without modifying their source code.

The methods are great, but what about properties? Can we extend that? 🤔

For a long time, it was not possible, but since C#14 you can. The syntax is slightly different, though:

public static class StringExtensions
{
extension(string value) // extension block
{
public bool IsNullOrEmpty() // extension method
{
return string.IsNullOrEmpty(value);
}
}
}

Now you can add a property to the extension block:

public static class StringExtensions
{
extension(string value)
{
public bool IsEmpty // extension property
{
get
{
return string.IsNullOrEmpty(value);
}
}
}
}

// Usage:
string name = "";
bool result = name.IsEmpty; // true
StringExtensions.get_IsEmpty(name); // 🥴

But it does not stop there, you can also add static methods to the type:

public static class StringExtensions
{
extension(string value)
{
// static extension method:
public static bool IsEmpty(string val)
{
return string.IsNullOrEmpty(val);
}

// static extension property:
public static string New => string.Empty;

// static user defined operator:
public static string operator |(string left, string right)
{
return string.Concat(left, right);
}
}
}

// Usage:
string name = string.New; // extend String type
string.IsEmpty(name);
string res = name | "Test";

// btw, you can still call them through StringExtensions 🥴
string name = StringExtensions.get_New;
StringExtensions.IsEmpty(name);
string res = StringExtensions.op_BitwiseOr(name, "Test");

This makes them a powerful tool for adding behaviour without modifying existing code or breaking the open/closed principle.

The article could have ended here, but don't rush to close the tab. Let’s see what else you can extend. I am sure you will be surprised 😉.

What Can Be Extended in C#

Before diving into specific examples, it’s important to understand just how flexible extension methods really are. Many developers only use them for basic helper utilities, but extension methods can be applied to almost any type in C#.

Behold, some of my favorite extension tricks. Of course, most of the time I will be using the old syntax, since it is easier to understand, or I'm just old😅.

Task

Just like other types, you can add extension methods to Task<T>. Developers tend to forget it, because nowadays Task<T> is just perceived as a return type.

But you can make your asynchronous code cleaner and more expressive:

public static class TaskExtensions
{
public static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout)
{
if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
{
return await task;
}

throw new TimeoutException("Task timed out");
}

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken token)
{
. . .
}
}

Now you can use these methods as if they were part of Task<T> itself:

int result = await task.WithTimeout(TimeSpan.FromSeconds(2));

With this approach, you can encapsulate common asynchronous patterns like timeouts, retries, and cancellation in a single place, making your code more readable and reducing repetitive boilerplate.

Enum

Enums in C# are great for representing a fixed set of values, but sometimes you need extra behavior or a friendly display. Very few developers know you can add extension methods to an enum.

public enum TrafficLight
{
Red,
Yellow,
Green
}

public static class TrafficLightExtensions
{
public static bool IsAllowedToGo(this TrafficLight light)
{
return light == TrafficLight.Green;
}

public static string ToMessage(this TrafficLight light)
{
return light switch
{
TrafficLight.Red => "Stop!",
TrafficLight.Yellow => "Get ready...",
TrafficLight.Green => "Go!",

_ => "Unknown light"
};
}
}

Now you can use these methods as if they were part of the enum itself:

TrafficLight trafficLight = TrafficLight.Yellow;

Console.WriteLine(trafficLight.IsAllowedToGo()); // False
Console.WriteLine(trafficLight.ToMessage()); // "Get ready..."

This not only allows you to encapsulate enum behaviour in a single place, but also helps to avoid repetitive switch or if blocks throughout your code.

Object

In C#, every type derives from object. This means you can create extension methods for object itself, giving all types a new behavior 😛.

public static class ObjectExtensions
{
public static void ThrowIfNull(this object obj, [CallerArgumentExpression(nameof(obj))] string paramName = null!)
{
if (obj == null)
{
throw new ArgumentNullException(paramName);
}
}
}

The method above can be used to simplify your guards:

int? number = null;

number.ThrowIfNull(); // throws ArgumentNullException("number")

Notice that you can call extension methods on null, and it won't throw NullReferenceException as with regular methods:

SomeClass myClass = null;
myClass.SomeMethod(); // throw NullReferenceException()

myClass.ThrowIfNull(); // works because is the same as ObjectExtensions.ThrowIfNull(myClass)

Be careful not to overuse object extensions, as they appear on every type in IntelliSense, and can clutter your API.

Generic

You can create extension methods not just for concrete types but also for generic types.

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

This makes your extensions extremely flexible, allowing them to work with many types while keeping your code type-safe.

public static class GenericExtensions
{
public static T GetValueOrDefault<T>(this T obj, T defaultValue = default)
{
return obj == null ? defaultValue : obj;
}
}

// Usage:
string text = null;
Console.WriteLine(text.GetValueOrDefault("default")); // "default"

One of my favorite small tricks would be to add this one:

public static class TaskExtensions
{
public static Task<T> AsCompletedTask<T>(this T obj)
{
return Task.FromResult(T);
}
}

Now you can create a Task from any object in a fluent manner, instead of wrapping everything in Task.FromResult() :

public Task<List<UserDto>> GetUsers()
{
return users
.Select(u => new UserDto(u))
.ToList()
.AsCompletedTask();
}

Similarly to object extensions, avoid overusing them, as they appear on every type and can clutter IntelliSense.

Interfaces

Interfaces are just like generics, can be used to write reusable code for different types🙃.

The funny thing is, you can extend interfaces just like classes. This is powerful because every implementation of the interface instantly gains the new behaviour — without modifying existing code.

For example, it could be a way to decorate all your repositories with a cache.

public static class RepositoryExtensions
{
public static List<T> GetAll<T>(this IRepository<T> repo, IMemoryCache cache)
{
return cache.GetOrCreate(key: repo.GetType().Name + nameof(repo.GetAll), (entry) =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);

return repo.GetAll();
});
}
}

This keeps your code cleaner by centralising cross-cutting logic.

Extension Magic ✨

I love C#. But what I enjoy even more is creating a clean, elegant API that makes other developers stop, stare, and drop their jaws in surprise 😁.

By letting you “attach” behaviour to types you don’t own, extensions open the door to cleaner APIs, lightweight abstractions, and even to duck typing.

Let’s explore how extensions can make your code elegant.

Deconstructors

Deconstruction is a feature that allows us to break an object into separate variables. This makes the code more concise and easier to read:

class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }

public void Deconstruct(out string fname, out string lname)
{
fname = FirstName;
lname = LastName;
}
}

// Usage:
var p = new Person();

var (firstName, lastName) = p;

However, .Deconstruct()does not have to be defined in the same class. You can use it as long as your type supports it. As long as this method is accessible, you can add deconstruction support yourself through an extension method:

// third-party library
sealed class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

// your code
static class PersonExtensions
{
extension(Person p)
{
public void Deconstruct(out string fname, out string lname)
{
fname = p.FirstName;
lname = p.LastName;
}
}
}

// Usage:
var p = new Person();

var (firstName, lastName) = p;

If you are interested in how deconstruction works, find more details here.

This allows you to write some cool deconstruction on in-built types, like:

// arrays
var numbers = new[] { 1, 2, 3, 4, 5 };
var (first, mid, last) = numbers;

// file
var file = new FileInfo("photo.png");
var (name, ext, size) = file;

// ranges
Range range = 2..5;
Index (start, end) = range;

// HttpResponse
var response = await http.GetAsync("https://example.com");
var (status, content) = await response;

// Uri
var uri = new Uri("https://example.com:8080/path?query=123#frag");
var (scheme, host, port, path, query, fragment) = uri;

// Version
var version = new Version("3.1.4.159");
var (major, minor, build, revision) = version;

await

The same goes for async/await. A simple extension like that:

public static class TaskExtensions
{
public static TaskAwaiter GetAwaiter(this TimeSpan timeSpan)
{
return Task.Delay(timeSpan).GetAwaiter();
}
}

Allows you to await any TimeSpan:

await TimeSpan.FromSeconds(5);

You can go even further by adding an extension to int 😅:

await 5.Seconds();

foreach

You can do the same trick with foreach. Extensions allow you to make any object iterable as long as it “implements” IEnumerable interface:

public static class RangeExtensions
{
public static IEnumerator<int> GetEnumerator(this Range range)
{
for (var i = range.Start.Value; i <= range.End.Value; i++)
{
yield return i;
}
}
}

Now you can use foreach in creative ways:

// iterate range
foreach (var i in 4..6)
{
Console.WriteLine(i);
}

// iterate tuple
foreach (var item in (1, 2, 3))
{
Console.WriteLine(item);
}

// iterate files in directory
foreach (var file in new DirectoryInfo("."))
{
Console.WriteLine(file.Name);
}

This technique lets you treat almost any type like a collection, creating expressive and readable iteration without modifying the original type.

Extensions in Unit Tests

Extension methods in unit tests are a controversial topic.

Some developers love them because they behave like pure functions — small, isolated helpers that keep tests clean and expressive.

Others argue the opposite: extension methods act like hidden static utilities that increase coupling and spread logic across the codebase in ways that are hard to track.

Often, extension methods are small helpers that do not require mocking. You just use them as-is and focus your tests on the behavior of the code, not the extension method itself.

public class UserService
{
public IEnumerable<User> GetActive()
{
return _dbContext
.Users
.Where(u => u.IsActive) // no need to mock extension method .Where()
.ToList();
}
}

However, the challenge becomes bigger when your extension calls methods on the extended object.

public class UserService(ILogger _logger)
{
public void SaveUser(string name)
{
. . .
_logger.LogInformation("Saving user {Name}", name); // extension -> calls ILogger.Log()
}
}

In that case, mocking gets tricky: you need to mock internal behavior you don’t control.

[Fact]
public void SaveUser_LogInformation()
{
// Arrange
var logger = Substitute.For<ILogger>();

// You can't mock LogInformation(...) because it's an extension.
// You must mock the internal Log(...) method it calls.
logger
.When(l => l.Log(
Arg.Any<LogLevel>(),
Arg.Any<EventId>(),
Arg.Any<object>(),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>()))
.Do(_ => { });

// Act
_userService.SaveUser("John");

// Assert
logger.Received(1).Log(
LogLevel.Information,
Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString()!.Contains("Saving user")),
null,
Arg.Any<Func<object, Exception?, string>>());
}

If Microsoft changes the internals of how .LogInformation() calls .Log(), all your mocks break — even though your extension didn't change at all.

This matters especially for library authors, who must avoid introducing breaking changes in new releases.

Summary

Most developers treat extension methods as small convenience helpers — but they’re far more powerful. In C#, extensions let you enhance built-in types, enrich third-party libraries, add missing APIs, introduce deconstruction, enable duck typing, extend enums, tasks, and even interfaces.

I hope this article helped you better understand extension methods, how far you can push them, and how they can help you create cleaner, more expressive APIs 😉.

Share in the comments your favorite extension methods 💬
You can use the link below to buy me a coffee☕️
And follow to get more ✅✅

--

--

Written by iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence