When working with collections, we often need to find a specific item that satisfies a certain condition—such as finding the highest, lowest, or most suitable element based on custom logic. While traditional loops can solve such problems, they are often verbose and harder to maintain. LINQ and extension methods provide a more elegant, efficient, and reusable way to solve these problems.
In this article, we’ll explore how to simplify such scenarios using LINQ’s Aggregate method and encapsulate the logic into a reusable extension method called WithMaximum. We will break down the solution step by step and explain every detail to make it easy to understand.
Scenario: Selecting the Best Employee
Imagine we have a system that assigns tasks to employees. Each employee has:
-
Name: Their name. -
SkillLevel: A measure of their proficiency. -
IsAvailable: Whether they are available for a task.
The goal is to assign a task to the most skilled available employee.
Step 1: Basic LINQ Approach
A common LINQ solution might look like this:
public Employee GetBestCandidate(IEnumerable<Employee> employees)
{
return employees
.Where(e => e.IsAvailable)
.OrderByDescending(e => e.SkillLevel)
.FirstOrDefault();
}
-
Where: Filters out unavailable employees. -
OrderByDescending: Sorts bySkillLevelin descending order. -
FirstOrDefault: Picks the first employee from the sorted list (the one with the highest skill level).
This approach works but is inefficient because it sorts the entire collection. Sorting has a time complexity of (O(N \log N)), which can be unnecessary if you only need the top result.
Step 2: Efficient Solution with Aggregate
We can improve efficiency by avoiding sorting. LINQ’s Aggregate method lets us scan the collection in a single pass ((O(N))), making it faster for this task.
public Employee GetBestCandidate(IEnumerable<Employee> employees)
{
return employees
.Where(e => e.IsAvailable)
.Aggregate((currentBest, next) =>
currentBest == null || next.SkillLevel > currentBest.SkillLevel ? next : currentBest);
}
-
Aggregate:- Reduces the collection to a single value (the most skilled employee).
- Compares each employee’s
SkillLevelto the current best.
However, Aggregate can be verbose and harder to read, especially for complex conditions. To improve readability, we’ll encapsulate this logic into a reusable extension method.
Step 3: Encapsulating Logic in WithMaximum
The WithMaximum extension method allows us to find the element with the highest value based on a custom criterion. Let’s break it down line by line to fully understand how it works.
Code
public static class EnumerableExtensions
{
public static T WithMaximum<T, TKey>(
this IEnumerable<T> source,
Func<T, TKey> selector)
where T : class
where TKey : IComparable<TKey>
{
var tuples = source.Select(item => (Item: item, Key: selector(item)));
var result = tuples.Aggregate(
seed: (Item: null as T, Key: default(TKey)),
(currentBest, next) =>
currentBest.Item == null || next.Key.CompareTo(currentBest.Key) > 0
? next
: currentBest
);
return result.Item;
}
}
Step-by-Step Breakdown
- Method Declaration
public static T WithMaximum<T, TKey>(
this IEnumerable<T> source,
Func<T, TKey> selector)
-
Extension Method: The
thiskeyword makes this an extension method forIEnumerable<T>. You can call it directly on any sequence (e.g.,employees.WithMaximum(...)). -
Generic Parameters:
-
T: The type of elements in the collection (e.g.,Employee). -
TKey: The type of the value used for comparison (e.g.,intforSkillLevel).
-
-
Func<T, TKey> selector:- A function that takes an element of type
Tand returns its key of typeTKey(e.g.,e => e.SkillLevel).
- A function that takes an element of type
- Generic Constraints
where T : class
where TKey : IComparable<TKey>
-
T : class:- Ensures
Tis a reference type, sonullcan be used as the initial seed.
- Ensures
-
TKey : IComparable<TKey>:- Ensures
TKeysupports comparisons (CompareTo) so we can determine the maximum.
- Ensures
-
Precomputing Keys with
Select
var tuples = source.Select(item => (Item: item, Key: selector(item)));
- Purpose: Precomputes the key for each element to avoid recalculating it multiple times.
-
Result: Transforms each element into a tuple containing:
-
Item: The original element. -
Key: The computed value (e.g.,SkillLevel).
-
Example:
If source contains:
Alice (SkillLevel: 85)
Bob (SkillLevel: 90)
And selector is e => e.SkillLevel, then tuples becomes:
[
(Item: Alice, Key: 85),
(Item: Bob, Key: 90)
]
-
Aggregating with
Aggregate
var result = tuples.Aggregate(
seed: (Item: null as T, Key: default(TKey)),
(currentBest, next) =>
currentBest.Item == null || next.Key.CompareTo(currentBest.Key) > 0
? next
: currentBest
);
-
seed:- Initial value:
(Item: null, Key: default(TKey)). -
Item: Starts asnull. -
Key: Starts as the default value forTKey(e.g.,0forint).
- Initial value:
-
Logic:
- Compare
next.KeywithcurrentBest.Key:- If
currentBest.Itemisnullornext.Keyis greater, updatecurrentBesttonext. - Otherwise, keep
currentBest.
- If
- Compare
Example:
- Iteration 1:
-
currentBest = (null, 0),next = (Alice, 85) -
currentBestisnull, socurrentBest = (Alice, 85).
-
- Iteration 2:
-
currentBest = (Alice, 85),next = (Bob, 90) -
90 > 85, socurrentBest = (Bob, 90).
-
- Return the Result
return result.Item;
- Extract the
Item(original object) from the final tuple. - In this case, it returns
Bob.
Step 4: Using WithMaximum
Now the main method becomes much simpler:
public Employee GetBestCandidate(IEnumerable<Employee> employees)
{
return employees
.Where(e => e.IsAvailable)
.WithMaximum(e => e.SkillLevel);
}
Full Example
Employee Class
public class Employee
{
public string Name { get; set; }
public int SkillLevel { get; set; }
public bool IsAvailable { get; set; }
public override string ToString()
{
return $"{Name} (Skill: {SkillLevel}, Available: {IsAvailable})";
}
}
Extension Method
public static class EnumerableExtensions
{
public static T WithMaximum<T, TKey>(
this IEnumerable<T> source,
Func<T, TKey> selector)
where T : class
where TKey : IComparable<TKey>
{
var tuples = source.Select(item => (Item: item, Key: selector(item)));
var result = tuples.Aggregate(
seed: (Item: null as T, Key: default(TKey)),
(currentBest, next) =>
currentBest.Item == null || next.Key.CompareTo(currentBest.Key) > 0
? next
: currentBest
);
return result.Item;
}
}
Main Program
class Program
{
static void Main(string[] args)
{
var employees = new List<Employee>
{
new Employee { Name = "Alice", SkillLevel = 85, IsAvailable = true },
new Employee { Name = "Bob", SkillLevel = 90, IsAvailable = false },
new Employee { Name = "Charlie", SkillLevel = 78, IsAvailable = true },
new Employee { Name = "Diana", SkillLevel = 92, IsAvailable = true }
};
var bestCandidate = employees.GetBestCandidate();
Console.WriteLine("Best Candidate:");
Console.WriteLine(bestCandidate
?.ToString() ?? "No available employees.");
}
}
Output
Best Candidate:
Diana (Skill: 92, Available: True)
Key Takeaways
-
Efficiency:
WithMaximumavoids unnecessary sorting and scans the collection in a single pass. - Reusability: The method can be used for any type of collection and comparison logic.
- Readability: Complex logic is encapsulated, making the consuming code clean and expressive.
This approach combines the power of LINQ and extension methods to produce code that is both efficient and maintainable.
Top comments (0)