C# Extension Methods — Static Classes, This Parameter, and LINQ Internals
In this tutorial, you will learn about C# Extension Methods. We cover key concepts, practical examples, and best practices to help you master this topic.
C# extension methods enable adding new methods to existing types without modifying the original type, using static classes and the this keyword on the first parameter.
What You'll Learn
You will master extension methods in C#: defining extension methods in static classes, the this parameter convention, how LINQ uses extension methods for IEnumerable<T>, creating fluent APIs, and best practices for .NET extension method design.
Why It Matters
Extension methods are the foundation of LINQ — every LINQ method (Where, Select, OrderBy) is an extension method on IEnumerable
Real-World Use
LINQ is the most famous example. ASP.NET Core uses extension methods for IApplicationBuilder.UseMiddleware(), IServiceCollection.AddScoped(), and IEndpointRouteBuilder.MapGet(). Configuration uses IConfiguration.GetValue<T>(). Logging uses ILogger.LogInformation(). Fluent validation, AutoMapper, and Swagger all use extension methods.
Learning Path
graph LR
A["24: Lambdas"] --> B["25: Extension Methods"]
B --> C["26: Nullable Reference Types"]
C --> D["27: Pattern Matching"]
D --> E["28: Records & Structs"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Basic Extension Method
public static class StringExtensions
{
public static bool IsNullOrWhitespace(this string? value)
{
return string.IsNullOrWhiteSpace(value);
}
public static string Truncate(this string value, int maxLength)
{
return value?.Length <= maxLength ? value : value[..maxLength] + "...";
}
}
// Usage
string? text = null;
Console.WriteLine(text.IsNullOrWhitespace()); // True
string longText = "This is a very long string that needs truncation";
Console.WriteLine(longText.Truncate(20)); // "This is a very lon..."
Creating Extension Methods
Rules:
- Must be in a static class
- Must be a static method
- First parameter uses
thiskeyword - Extension classes should not be nested
public static class EnumerableExtensions
{
// Extension method on IEnumerable<T>
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (var item in source)
action(item);
}
// Extension method with predicate
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
where T : class
{
return source.Where(x => x != null)!;
}
// Extension method returning a different type
public static string JoinString<T>(this IEnumerable<T> source, string separator)
{
return string.Join(separator, source);
}
}
var numbers = new[] { 1, 2, 3, 4, 5 };
numbers.ForEach(n => Console.Write($"{n} ")); // 1 2 3 4 5
Console.WriteLine();
string joined = numbers.JoinString(", ");
Console.WriteLine(joined); // "1, 2, 3, 4, 5"
Extension Methods on Interfaces
Extension methods on interfaces are the basis of LINQ:
public static class IEnumerableExtensions
{
// My own LINQ-like methods
public static bool IsEmpty<T>(this IEnumerable<T> source)
{
return !source.Any();
}
public static T? SecondOrDefault<T>(this IEnumerable<T> source)
{
using var enumerator = source.GetEnumerator();
if (!enumerator.MoveNext()) return default;
if (!enumerator.MoveNext()) return default;
return enumerator.Current;
}
public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
var list = source.ToList();
return list.Take(Math.Max(0, list.Count - count));
}
}
var items = new[] { 10, 20, 30, 40, 50 };
Console.WriteLine(items.IsEmpty()); // False
Console.WriteLine(items.SecondOrDefault()); // 20
Console.WriteLine(string.Join(", ", items.SkipLast(2))); // 10, 20, 30
Fluent API Design
Extension methods enable fluent (chaining) APIs:
public static class FluentStringExtensions
{
public static string AddPrefix(this string s, string prefix) => $"{prefix}{s}";
public static string AddSuffix(this string s, string suffix) => $"{s}{suffix}";
public static string ToTitleCase(this string s) =>
System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(s.ToLower());
public static string WrapInHtmlTag(this string s, string tag) => $"<{tag}>{s}</{tag}>";
}
// Fluent chaining
string result = "hello world"
.ToTitleCase()
.AddPrefix(">> ")
.AddSuffix(" <<")
.WrapInHtmlTag("h1");
Console.WriteLine(result); // <h1>>> Hello World <<</h1>
Extension Methods with Generics
public static class GenericExtensions
{
public static T? As<T>(this object? obj) where T : class
=> obj as T;
public static bool In<T>(this T value, params T[] values)
=> values.Contains(value);
public static TResult Pipe<T, TResult>(this T input, Func<T, TResult> func)
=> func(input);
}
Console.WriteLine(5.In(1, 3, 5, 7)); // True
Console.WriteLine("apple".In("banana", "cherry")); // False
int squared = 5.Pipe(x => x * x);
Console.WriteLine(squared); // 25
Extension Methods on Specific Types
public static class DateTimeExtensions
{
public static bool IsWeekend(this DateTime date)
=> date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
public static DateTime StartOfWeek(this DateTime date, DayOfWeek startOfWeek = DayOfWeek.Monday)
{
int diff = (7 + (date.DayOfWeek - startOfWeek)) % 7;
return date.AddDays(-1 * diff).Date;
}
public static string ToRelativeTimeString(this DateTime dateTime)
{
var span = DateTime.UtcNow - dateTime.ToUniversalTime();
return span switch
{
{ TotalMinutes: < 1 } => "just now",
{ TotalHours: < 1 } => $"{(int)span.TotalMinutes}m ago",
{ TotalDays: < 1 } => $"{(int)span.TotalHours}h ago",
{ TotalDays: < 30 } => $"{(int)span.TotalDays}d ago",
_ => dateTime.ToString("MMM dd, yyyy")
};
}
}
var now = DateTime.UtcNow;
Console.WriteLine(now.StartOfWeek()); // Monday of this week
Console.WriteLine(now.AddHours(-3).ToRelativeTimeString()); // "3h ago"
How LINQ Uses Extension Methods
// This is how LINQ's Where, Select, etc. are defined:
namespace System.Linq
{
public static class Enumerable
{
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
{
foreach (var item in source)
if (predicate(item))
yield return item;
}
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
foreach (var item in source)
yield return selector(item);
}
}
}
// Without extension methods:
// Enumerable.Where(numbers, n => n > 5)
// With extension methods:
// numbers.Where(n => n > 5)
Extension Methods vs Instance Methods
If an instance method has the same signature as an extension method, the instance method wins:
public class MyClass
{
public void Print() => Console.WriteLine("Instance method");
}
public static class MyExtensions
{
public static void Print(this MyClass obj) => Console.WriteLine("Extension method");
}
var obj = new MyClass();
obj.Print(); // "Instance method" (instance always wins)
Common Mistakes
Mistake 1: Defining Extension Methods in the Wrong Namespace
Extension methods are only visible when their containing namespace is imported. Put them in the same namespace as the extended type or in a dedicated namespace that consumers know to import.
Mistake 2: Creating Extension Methods on object
Extension methods on object pollute all types and can conflict with instance methods. Avoid unless absolutely necessary.
Mistake 3: Forgetting That Extension Methods Cannot Access Private Members
Extension methods have no special access. They can only use the public API of the type they extend. Use regular static methods if you need private access.
Mistake 4: Naming Extension Methods That Conflict with LINQ
If you create Where or Select extension methods with different signatures, they may cause ambiguity. Be careful with method names that match existing LINQ methods.
Mistake 5: Not Handling Null in Extension Methods
The this parameter can be null. Extension methods must check for null explicitly:
public static bool IsNullOrEmpty<T>(this IEnumerable<T>? source)
{
return source == null || !source.Any(); // Must check null explicitly
}
Mistake 6: Creating Extension Methods When Regular Static Methods Suffice
Extension methods imply that the operation "belongs" to the type. If the operation is not logically associated with the type, use a regular static method in a utility class.
Practice Questions
- What are the requirements for defining an extension method in C#?
- How does the compiler resolve between an instance method and an extension method?
- Write an extension method on
stringthat counts the number of words in a sentence. - How does LINQ use extension methods to enable chaining?
- What namespace should you put extension methods in for discoverability?
Challenge
Create a fluent validation library using extension methods. Define Validate<T> as an extension on T that enables chaining: value.Validate().IsNotNull().IsGreaterThan(0).IsLessThan(100). Return a result object with IsValid and Errors properties.
FAQ
Mini Project
Create a fluent CSV Builder using extension methods:
public class CsvBuilder
{
private readonly List<string[]> _rows = new();
private string[]? _headers;
public CsvBuilder WithHeaders(params string[] headers)
{
_headers = headers;
return this;
}
public CsvBuilder AddRow(params string[] values)
{
_rows.Add(values);
return this;
}
public string Build()
{
var lines = new List<string>();
if (_headers != null)
lines.Add(string.Join(",", _headers.Select(EscapeField)));
foreach (var row in _rows)
lines.Add(string.Join(",", row.Select(EscapeField)));
return string.Join(Environment.NewLine, lines);
}
private static string EscapeField(string field)
{
if (field.Contains(',') || field.Contains('"') || field.Contains('\n'))
return $"\"{field.Replace("\"", "\"\"")}\"";
return field;
}
}
public static class CsvExtensions
{
public static CsvBuilder ToCsv<T>(this IEnumerable<T> source,
params Func<T, string>[] selectors)
{
var builder = new CsvBuilder();
foreach (var item in source)
{
var values = selectors.Select(s => s(item)).ToArray();
builder.AddRow(values);
}
return builder;
}
public static CsvBuilder ToCsvWithHeaders<T>(this IEnumerable<T> source,
params (string Header, Func<T, string> Selector)[] columns)
{
var builder = new CsvBuilder()
.WithHeaders(columns.Select(c => c.Header).ToArray());
foreach (var item in source)
{
var values = columns.Select(c => c.Selector(item)).ToArray();
builder.AddRow(values);
}
return builder;
}
}
var products = new[]
{
new { Name = "Laptop", Price = 999.99, Category = "Electronics" },
new { Name = "Mouse", Price = 29.99, Category = "Electronics" },
new { Name = "Desk", Price = 299.99, Category = "Furniture" }
};
string csv = products.ToCsvWithHeaders(
("Product Name", p => p.Name),
("Price", p => p.Price.ToString("F2")),
("Category", p => p.Category)
).Build();
Console.WriteLine("Generated CSV:");
Console.WriteLine(csv);
Expected output:
Generated CSV:
Product Name,Price,Category
Laptop,999.99,Electronics
Mouse,29.99,Electronics
Desk,299.99,Furniture
What's Next
You have mastered extension methods in C#. The next lesson covers nullable reference types: annotations, warnings, and the null-forgiving operator.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro