Skip to main content

.NET LINQ Extensions

Introduction

LINQ (Language Integrated Query) is already a powerful feature in .NET, but sometimes you may need functionality that isn't available out of the box. LINQ extensions allow you to create custom query operators that work seamlessly with the standard LINQ methods, extending its capabilities to fit your specific needs.

In this tutorial, we'll explore how to create custom LINQ extension methods, making your code more readable, maintainable, and powerful. By the end, you'll be able to build your own LINQ toolkit tailored to your application's requirements.

Understanding Extension Methods

Before diving into LINQ extensions, let's briefly review extension methods in C#:

csharp
public static class StringExtensions
{
public static string Reverse(this string input)
{
char[] chars = input.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
}

// Usage
string text = "Hello";
string reversed = text.Reverse(); // Outputs: "olleH"

Extension methods allow you to "add" methods to existing types without modifying the original type. They're static methods that use the this keyword before the first parameter, which specifies the type being extended.

Creating Basic LINQ Extensions

Let's start by creating some simple but useful LINQ extensions:

Example 1: DistinctBy

While LINQ already has a Distinct() method, it only works with the default equality comparer. Let's create a DistinctBy extension that allows us to select which property to use for comparison:

csharp
public static class LinqExtensions
{
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
HashSet<TKey> seenKeys = new HashSet<TKey>();
foreach (var element in source)
{
if (seenKeys.Add(keySelector(element)))
{
yield return element;
}
}
}
}

Usage:

csharp
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

// Create a sample collection
var people = new List<Person>
{
new Person { Name = "Alice", Age = 25 },
new Person { Name = "Bob", Age = 30 },
new Person { Name = "Charlie", Age = 25 },
new Person { Name = "Diana", Age = 35 }
};

// Get people with distinct ages
var distinctByAge = people.DistinctBy(p => p.Age).ToList();
// Result: Alice and Bob and Diana (Charlie is excluded as his age is the same as Alice's)

foreach (var person in distinctByAge)
{
Console.WriteLine($"{person.Name}: {person.Age}");
}

// Output:
// Alice: 25
// Bob: 30
// Diana: 35

Example 2: ForEach Extension Method

Let's create a ForEach extension method that allows us to perform an action on each element in a collection:

csharp
public static class LinqExtensions
{
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (action == null) throw new ArgumentNullException(nameof(action));

foreach (var item in source)
{
action(item);
}
}
}

Usage:

csharp
var numbers = new[] { 1, 2, 3, 4, 5 };

// Perform an action on each element
numbers.ForEach(n => Console.WriteLine($"Number: {n}"));

// Output:
// Number: 1
// Number: 2
// Number: 3
// Number: 4
// Number: 5

Advanced LINQ Extensions

Now let's explore some more advanced LINQ extensions that solve common programming challenges:

Example 3: Batch Processing

When working with large datasets, it's often efficient to process items in batches. Let's create a Batch extension method:

csharp
public static class LinqExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(
this IEnumerable<T> source, int batchSize)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (batchSize <= 0) throw new ArgumentException("Batch size must be greater than 0.", nameof(batchSize));

using var enumerator = source.GetEnumerator();
while (enumerator.MoveNext())
{
yield return GetBatch(enumerator, batchSize);
}
}

private static IEnumerable<T> GetBatch<T>(IEnumerator<T> enumerator, int batchSize)
{
yield return enumerator.Current;

for (int i = 1; i < batchSize && enumerator.MoveNext(); i++)
{
yield return enumerator.Current;
}
}
}

Usage:

csharp
var numbers = Enumerable.Range(1, 10);

var batches = numbers.Batch(3).ToList();

// Print each batch
for (int i = 0; i < batches.Count; i++)
{
Console.WriteLine($"Batch {i + 1}: {string.Join(", ", batches[i])}");
}

// Output:
// Batch 1: 1, 2, 3
// Batch 2: 4, 5, 6
// Batch 3: 7, 8, 9
// Batch 4: 10

Example 4: ToDataTable Extension

When working with databases or legacy systems, you might need to convert LINQ query results to a DataTable:

csharp
public static class LinqExtensions
{
public static DataTable ToDataTable<T>(this IEnumerable<T> source)
{
var properties = typeof(T).GetProperties();
var dataTable = new DataTable();

// Create columns
foreach (var property in properties)
{
dataTable.Columns.Add(property.Name, Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType);
}

// Add rows
foreach (var item in source)
{
var row = dataTable.NewRow();

foreach (var property in properties)
{
row[property.Name] = property.GetValue(item) ?? DBNull.Value;
}

dataTable.Rows.Add(row);
}

return dataTable;
}
}

Usage:

csharp
var people = new List<Person>
{
new Person { Name = "Alice", Age = 25 },
new Person { Name = "Bob", Age = 30 },
new Person { Name = "Charlie", Age = 35 }
};

var dataTable = people.ToDataTable();

// Display the DataTable contents
Console.WriteLine($"Columns: {string.Join(", ", dataTable.Columns.Cast<DataColumn>().Select(c => c.ColumnName))}");
foreach (DataRow row in dataTable.Rows)
{
Console.WriteLine($"Row: {row["Name"]} - {row["Age"]}");
}

// Output:
// Columns: Name, Age
// Row: Alice - 25
// Row: Bob - 30
// Row: Charlie - 35

Real-World Application Examples

Example 5: Shopping Cart Analysis

In an e-commerce application, we might want to analyze user shopping carts. Let's create some LINQ extensions to help with this:

csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}

public class ShoppingCart
{
public int UserId { get; set; }
public List<Product> Products { get; set; } = new List<Product>();
}

public static class ShoppingCartExtensions
{
public static decimal TotalValue(this IEnumerable<Product> products)
{
return products.Sum(p => p.Price);
}

public static IEnumerable<IGrouping<string, Product>> GroupByCategory(
this IEnumerable<Product> products)
{
return products.GroupBy(p => p.Category);
}

public static Product MostExpensiveItem(this IEnumerable<Product> products)
{
return products.OrderByDescending(p => p.Price).FirstOrDefault();
}
}

Usage:

csharp
var cart = new ShoppingCart
{
UserId = 101,
Products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 1200, Category = "Electronics" },
new Product { Id = 2, Name = "Headphones", Price = 200, Category = "Electronics" },
new Product { Id = 3, Name = "Book", Price = 15, Category = "Books" },
new Product { Id = 4, Name = "T-Shirt", Price = 25, Category = "Clothing" }
}
};

// Calculate total cart value
decimal total = cart.Products.TotalValue();
Console.WriteLine($"Cart total: ${total}");

// Get most expensive item
var mostExpensive = cart.Products.MostExpensiveItem();
Console.WriteLine($"Most expensive item: {mostExpensive.Name} (${mostExpensive.Price})");

// Group by category
var categories = cart.Products.GroupByCategory();
foreach (var category in categories)
{
Console.WriteLine($"Category: {category.Key}");
foreach (var item in category)
{
Console.WriteLine($" - {item.Name}: ${item.Price}");
}
}

// Output:
// Cart total: $1440
// Most expensive item: Laptop ($1200)
// Category: Electronics
// - Laptop: $1200
// - Headphones: $200
// Category: Books
// - Book: $15
// Category: Clothing
// - T-Shirt: $25

Example 6: Data Processing Pipeline

Let's create an extension method for data processing pipelines that can handle errors gracefully:

csharp
public static class PipelineExtensions
{
public static IEnumerable<TResult> SafeSelect<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector,
Action<TSource, Exception> onError = null)
{
foreach (var item in source)
{
TResult result;
try
{
result = selector(item);
}
catch (Exception ex)
{
onError?.Invoke(item, ex);
continue;
}
yield return result;
}
}
}

Usage:

csharp
var data = new[] { "1", "2", "abc", "4", "5" };

// Try to parse each string to an integer, handling errors gracefully
var numbers = data.SafeSelect(
s => int.Parse(s),
(item, ex) => Console.WriteLine($"Error parsing '{item}': {ex.Message}")
).ToList();

Console.WriteLine("Successfully parsed numbers: " + string.Join(", ", numbers));

// Output:
// Error parsing 'abc': Input string was not in a correct format.
// Successfully parsed numbers: 1, 2, 4, 5

Best Practices for Creating LINQ Extensions

When creating your own LINQ extensions, follow these best practices:

  1. Name consistently: Follow the naming patterns used by the existing LINQ methods.

  2. Document well: Include XML documentation to explain what your extension does, its parameters, and return values.

  3. Handle edge cases: Check for null references and validate inputs.

  4. Consider performance: Use deferred execution with yield return when appropriate.

  5. Keep methods focused: Each extension method should do one thing well.

  6. Test thoroughly: Extension methods should be well-tested with various inputs.

Here's an example that follows these best practices:

csharp
public static class LinqExtensions
{
/// <summary>
/// Returns elements from the input sequence until a condition is met.
/// </summary>
/// <typeparam name="T">The type of the elements in the sequence.</typeparam>
/// <param name="source">The source sequence.</param>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <returns>Elements until the condition is met.</returns>
/// <exception cref="ArgumentNullException">Thrown if source or predicate is null.</exception>
public static IEnumerable<T> TakeUntil<T>(
this IEnumerable<T> source,
Func<T, bool> predicate)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));

foreach (var item in source)
{
yield return item;
if (predicate(item))
break;
}
}
}

Usage:

csharp
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var result = numbers.TakeUntil(n => n > 5).ToList();
Console.WriteLine(string.Join(", ", result));

// Output: 1, 2, 3, 4, 5, 6
// Takes numbers until we reach one greater than 5, including that number

Summary

LINQ extensions provide a powerful way to extend the built-in LINQ functionality with custom operations specific to your application's needs. By creating well-designed extension methods, you can:

  • Improve code readability and maintainability
  • Reduce code duplication
  • Implement common patterns in a reusable way
  • Create domain-specific query operations

We've covered:

  • Basic principles of creating LINQ extension methods
  • Simple extensions like DistinctBy and ForEach
  • Advanced extensions like Batch and ToDataTable
  • Real-world applications in e-commerce and data processing
  • Best practices for creating your own extensions

With these techniques, you can build a powerful toolkit that makes your LINQ queries more expressive and your code cleaner.

Practice Exercises

To solidify your understanding of LINQ extensions, try implementing these methods:

  1. Create a Shuffle<T>() extension that randomly reorders elements in a sequence
  2. Implement MinBy<T, TKey>() and MaxBy<T, TKey>() that return the element with the minimum or maximum value of a selected property
  3. Create a Paginate<T>(int pageSize, int pageNumber) extension for pagination
  4. Implement ToCSV<T>() that converts a collection to a CSV string

Additional Resources

Happy coding!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)