Ever wished you could add a method to string
, IEnumerable<T>
or even your own DTOs without touching their source code or creating awkward helper classes? Good news: with three keywords you can extend almost anything and make your code read like a fluent story.
Extension methods in one minute

Extension methods are static methods that appear as instance methods on another type. You define them in a static
class; the first parameter is prefixed with this
to mark the target type.
namespace MyCompany.Common;
public static class StringExtensions
{
public static string Truncate(this string? value, int max, string ellipsis = "…")
{
if (string.IsNullOrEmpty(value) || max <= 0) return string.Empty;
return value!.Length <= max ? value : value.Substring(0, Math.Max(0, max - ellipsis.Length)) + ellipsis;
}
}
Usage (after using MyCompany.Common;
):
var title = "C# Extension Methods: Add Power Without Inheritance";
Console.WriteLine(title.Truncate(20)); // => C# Extension Method
Under the hood (compiler view)
- The call
title.Truncate(20)
is rewritten toStringExtensions.Truncate(title, 20)
at compile time. - If a real instance method with the same signature exists, the instance method wins.
- Extensions are discovered by namespaces in scope (
using
directives). If you forget theusing
, your method “disappears”.
Think of it like a universal adapter that snaps onto a type without inheritance and without modifying the original assembly.
Quick wins you can ship today
Below are small but mighty extensions I’ve used in production to keep code tidy and intention‑revealing.
Null‑safe functional helpers for any type
public static class ObjectExtensions
{
// Pipe a value through a function (handy in fluent flows)
public static TResult Pipe<T, TResult>(this T value, Func<T, TResult> f) => f(value);
// Do something (side effect) and return the original value
public static T Tap<T>(this T value, Action<T> sideEffect)
{
sideEffect(value);
return value;
}
}
Usage:
var user = new User { Name = "Ann" }
.Tap(u => logger.LogInformation("Created user {Name}", u.Name))
.Pipe(u => u with { Name = u.Name.ToUpperInvariant() });
Guard clauses that read like English
public static class GuardExtensions
{
public static T NotNull<T>(this T? value, string? paramName = null)
where T : class => value ?? throw new ArgumentNullException(paramName ?? nameof(value));
public static string NotNullOrWhiteSpace(this string? value, string? paramName = null)
=> !string.IsNullOrWhiteSpace(value) ? value! : throw new ArgumentException("Must not be empty", paramName ?? nameof(value));
public static int Positive(this int value, string? paramName = null)
=> value > 0 ? value : throw new ArgumentOutOfRangeException(paramName ?? nameof(value), "Must be positive");
}
Usage:
void Enqueue(string? queueName, int maxRetries)
{
queueName.NotNullOrWhiteSpace(nameof(queueName));
maxRetries.Positive(nameof(maxRetries));
// ...
}
LINQ that actually reads
public static class LinqExtensions
{
// .NET has Chunk, but this supports 'partial last' projection too
public static IEnumerable<IReadOnlyList<T>> Batch<T>(this IEnumerable<T> source, int size)
{
if (source is null) throw new ArgumentNullException(nameof(source));
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
var bucket = new List<T>(size);
foreach (var item in source)
{
bucket.Add(item);
if (bucket.Count == size)
{
yield return bucket.AsReadOnly();
bucket = new List<T>(size);
}
}
if (bucket.Count > 0) yield return bucket.AsReadOnly();
}
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class
=> source.Where(x => x is not null)!.Cast<T>();
public static ILookup<TKey, TSource> ToLookupSafe<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
where TKey : notnull
=> source.ToLookup(keySelector);
}
Usage:
var batches = Enumerable.Range(1, 12).Batch(5); // [1..5], [6..10], [11..12]
Human‑friendly time formatting
public static class DateTimeExtensions
{
public static string ToFriendlyAge(this DateTime utc, DateTime? nowUtc = null)
{
var now = (nowUtc ?? DateTime.UtcNow);
var span = now - utc;
if (span.TotalSeconds < 60) return $"{(int)span.TotalSeconds}s ago";
if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes}m ago";
if (span.TotalHours < 24) return $"{(int)span.TotalHours}h ago";
if (span.TotalDays < 7) return $"{(int)span.TotalDays}d ago";
return utc.ToString("yyyy-MM-dd");
}
}
Task timeouts without ceremony
public static class TaskExtensions
{
public static async Task WithTimeout(this Task task, TimeSpan timeout, string? message = null)
{
using var cts = new CancellationTokenSource();
var completed = await Task.WhenAny(task, Task.Delay(timeout, cts.Token));
if (completed == task)
{
cts.Cancel();
await task; // propagate exceptions
return;
}
throw new TimeoutException(message ?? $"Task did not complete within {timeout}.");
}
public static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout, string? message = null)
{
await ((Task)task).WithTimeout(timeout, message);
return await task;
}
}
ReadOnlySpan goodies (zero allocations)
public static class SpanExtensions
{
public static bool EqualsIgnoreCase(this ReadOnlySpan<char> left, ReadOnlySpan<char> right)
=> left.Equals(right, StringComparison.OrdinalIgnoreCase);
public static bool IsAsciiDigit(this char c) => c is >= '0' and <= '9';
}
Usage:
ReadOnlySpan<char> a = "HELLO";
ReadOnlySpan<char> b = "hello";
var eq = a.EqualsIgnoreCase(b); // true
Minimal APIs: one‑line registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddFeatureFlags(this IServiceCollection services)
{
services.AddOptions<FeatureFlags>().BindConfiguration("FeatureFlags");
services.AddSingleton<IFeatureFlagChecker, FeatureFlagChecker>();
return services;
}
}
public static class WebApplicationExtensions
{
public static WebApplication UseRequestTiming(this WebApplication app)
{
app.Use(async (ctx, next) =>
{
var sw = Stopwatch.StartNew();
await next();
sw.Stop();
app.Logger.LogInformation("{Path} -> {StatusCode} in {Elapsed} ms",
ctx.Request.Path, ctx.Response.StatusCode, sw.ElapsedMilliseconds);
});
return app;
}
}
Usage in Program.cs
:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFeatureFlags();
var app = builder.Build();
app.UseRequestTiming();
app.Run();
EF Core: paging without repetition
public static class QueryableExtensions
{
public static IQueryable<T> Page<T>(this IQueryable<T> query, int page, int size)
{
page = Math.Max(1, page);
size = Math.Clamp(size, 1, 1000);
return query.Skip((page - 1) * size).Take(size);
}
}
HTTP JSON with policies
public static class HttpClientExtensions
{
public static async Task<T?> GetJsonOrDefaultAsync<T>(this HttpClient client, string url, CancellationToken ct = default)
{
using var res = await client.GetAsync(url, ct);
if (!res.IsSuccessStatusCode) return default;
await using var stream = await res.Content.ReadAsStreamAsync(ct);
return await JsonSerializer.DeserializeAsync<T>(stream, cancellationToken: ct);
}
}
Domain‑specific fluent APIs
public record Money(decimal Amount, string Currency)
{
public override string ToString() => $"{Amount:0.##} {Currency}";
}
public static class MoneyExtensions
{
public static Money In(this decimal amount, string currency) => new(amount, currency);
public static Money Add(this Money left, Money right)
=> left.Currency == right.Currency
? left with { Amount = left.Amount + right.Amount }
: throw new InvalidOperationException("Currency mismatch");
public static bool IsZero(this Money m) => m.Amount == 0;
}
Usage:
var tax = 5m.In("USD");
var total = 95m.In("USD").Add(tax);
if (total.IsZero()) { /* ... */ }
Design principles for great extension methods
- Name for intent, not mechanics. Prefer
ToFriendlyAge()
overFormatTimespanAsShortEnglish()
. - Keep them small and pure. If you’re mutating global state inside an extension, it’s probably in the wrong place.
- Avoid surprising allocations. For hot paths, consider
ReadOnlySpan<T>
/Span<T>
overloads. - Don’t hide business rules. Domain logic in extensions is great when it clarifies intent, but don’t bury critical decisions where they’re hard to discover.
- Keep extension classes cohesive. Group by target type or feature (e.g.,
DateTimeExtensions
,QueryableExtensions
). - Respect discoverability. If teammates can’t find your methods, they won’t use them. Document with XML comments; consider analyzers or a
README
in the project.
Common pitfalls (and how to dodge them)
Ambiguity with instance methods
If a type later introduces an instance method with the same name/signature as your extension, your extension quietly loses. Strategy: pick names that are unlikely to collide (Page()
vs SkipTakePage()
?), or keep extensions internal to your solution.
Null receivers
Extensions can be called on null
receivers (because they’re static). Guard if necessary:
public static int SafeLength(this string? s) => s?.Length ?? 0;
Namespace collisions & missing using
If two namespaces bring extensions with the same signature into scope, calls may become ambiguous. Keep extensions under clear namespaces like Company.Product.Feature.*
and import intentionally.
Overuse and God‑extension classes
If everything is an extension, nothing is readable. Use them to express intent; prefer regular static methods for heavy lifting.
Binary compatibility
Once published as a NuGet package, renaming or removing extensions is a breaking change. Treat public extensions as public API.
Extension methods vs alternatives
Approach | Pros | Cons |
---|---|---|
Extension methods | Fluent syntax; work with sealed/3rd‑party | Can clutter IntelliSense; static dispatch |
Inheritance | Polymorphism; substitutability | Heavy; impossible for sealed/BCL types |
Helper/utility class | Simple; explicit call site | Less discoverable; noisy call sites |
Partial classes | Organizes your types | Only for types you own, same assembly |
Use extension methods when you need discoverable helpers on types you don’t own or want a fluent API without new abstractions.
Testing extension methods
Treat extensions like any other logic: give them unit tests.
public class StringExtensionsTests
{
[Fact]
public void Truncate_Should_Append_Ellipsis_When_Too_Long()
{
var s = "abcdefghijkl";
var r = s.Truncate(5, "...");
Assert.Equal("ab...", r);
}
[Fact]
public void Truncate_Should_Return_Empty_On_Null()
{
string? s = null;
var r = s.Truncate(5);
Assert.Equal(string.Empty, r);
}
}
Tip: If you keep extension methods small and pure, tests are trivial and blazing fast.
Real‑world refactor: from helpers to extensions
Before (helper dumping ground):
public static class Utils
{
public static string Slugify(string input) { /* ... */ }
}
// usage
var slug = Utils.Slugify(articleTitle);
After (intent on the left):
public static class SlugExtensions
{
public static string ToSlug(this string input)
{
var normalized = input.ToLowerInvariant();
var sb = new StringBuilder(normalized.Length);
foreach (var ch in normalized)
{
if (char.IsLetterOrDigit(ch)) sb.Append(ch);
else if (char.IsWhiteSpace(ch) || ch is '-' or '_') sb.Append('-');
}
return Regex.Replace(sb.ToString(), "-+", "-").Trim('-');
}
}
// usage
var slug = articleTitle.ToSlug();
The second version reads like natural language and co‑locates the transformation with the type it extends, improving discoverability.
When not to use extension methods
- When the method must access internals or invariants of a type (add it to the type, not beside it).
- When behavior is surprising (e.g., hidden I/O or network calls from a
string
extension). - When you need state between calls. Extensions should be stateless and side‑effect‑light.
- When you’re tempted to replace real abstractions. If you’re building a feature, consider interfaces and composition first; use extensions to decorate, not to define.
FAQ: Extension methods in the wild
No. They’re compiled to regular static calls. Just watch allocations inside your implementation.
Yes, and it’s a great way to add optional behaviors. Consumers see them on all implementations of the interface.
No. Extensions don’t participate in virtual dispatch. If you need polymorphism, define it on the type/interface.
Put them in a dedicated project (e.g., Company.Product.Extensions
) and share via a NuGet package. Mind namespaces and XML docs.
No – only methods. But you can simulate via GetX()
patterns.
Add XML comments and consider an analyzer (e.g., Roslyn) to enforce naming or ban heavy logic inside extensions.
Conclusion: Extensions make intent obvious
Extension methods let you speak your domain’s language directly in code – without inheritance, without clutter, and without touching third‑party types. Start with a guard or two, add a LINQ helper, and wire a neat Minimal API extension. Your team will thank you when every line reads like a sentence.
Which extension method saves you the most time? Share it in the comments – let’s build a tiny library together.