Back to Blog
C#

Using Min and Max in C# LINQ

c# linq min max: Learn how to use LINQ Min and Max in C#: basic syntax, selector projections, empty sequence exceptions, nullable handling, and performance tradeoffs.

LINQIEnumerableSequence OperationsNull HandlingPerformance
Illustration of a bar chart with the shortest and tallest bars highlighted, representing LINQ Min and Max operations on a sequence.

c# linq min max requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The Min and Max methods in LINQ find the smallest and largest values in a sequence. They are extension methods on IEnumerable<T> in the System.Linq namespace and work on arrays, lists, and any other sequence type. Their behavior is straightforward for simple numeric collections, but the selector overloads, empty-sequence handling, and nullable-value semantics are where most mistakes happen.

Basic Syntax for Numeric Sequences

For a sequence of numbers, Min and Max take no arguments and return the smallest or largest element:

int[] scores = { 78, 92, 85, 64, 88 }; int lowest = scores.Min(); int highest = scores.Max(); Console.WriteLine(lowest); // 64 Console.WriteLine(highest); // 92

The return type matches the element type for numeric sequences. Both methods perform a single pass over the sequence and do not modify the source collection. This is the simplest usage and covers the common case of finding a boundary value in a dataset.

Projecting a Property with a Selector

When the sequence contains objects rather than primitive values, you pass a selector function that extracts the value to compare:

var employees = new List<Employee> { new Employee { Name = "Alice", Salary = 72000 }, new Employee { Name = "Bob", Salary = 64000 }, new Employee { Name = "Carol", Salary = 81000 } }; decimal maxSalary = employees.Max(e => e.Salary); decimal minSalary = employees.Min(e => e.Salary);

The selector is a Func<T, TResult> invoked once per element. The comparison happens on the projected value, not on the original object. This is the overload you reach for when working with domain objects, DTOs, or any collection where the interesting value is a property rather than the element itself.

Behavior on Empty Sequences

Calling Min or Max on an empty sequence of a non-nullable value type throws InvalidOperationException:

int[] empty = Array.Empty<int>(); // Throws InvalidOperationException int result = empty.Min();

There is no meaningful minimum or maximum for an empty set of non-nullable values, so the runtime refuses to guess. This is a deliberate design decision, not an oversight.

For nullable value types and reference types, the methods return null instead of throwing:

int?[] values = Array.Empty<int?>(); int? result = values.Min(); // null

This asymmetry catches many developers off guard. If your sequence can be empty and you are working with non-nullable types, DefaultIfEmpty provides a well-defined fallback:

int[] scores = Array.Empty<int>(); int lowest = scores.DefaultIfEmpty(0).Min(); // 0

DefaultIfEmpty inserts the fallback value into the sequence only when the source is empty, so the result is always defined.

Min and Max with Nullable Values

When the sequence contains nullable values, Min and Max ignore null entries and operate on the non-null values:

int?[] readings = { 12, null, 45, 8, null }; int? smallest = readings.Min(); // 8

If every element is null, the result is null. This behavior is convenient when data originates from a database or an external API where missing values are represented as null, but it means you should check the result before using it in arithmetic or comparisons.

Performance Considerations

Both Min and Max run in O(n) time with a single pass over the sequence. They do not sort and do not allocate a new collection, so for a single value they are as efficient as a hand-written loop.

The cost appears when you call both methods on the same collection. Each call is a separate O(n) pass:

int[] values = GetValues(); // Two passes over the array int min = values.Min(); int max = values.Max();

For small collections this is irrelevant. For very large sequences, a single foreach loop that tracks both values avoids the second pass:

int min = int.MaxValue; int max = int.MinValue; foreach (int value in values) { if (value < min) min = value; if (value > max) max = value; }

This manual loop is slightly more verbose but performs one pass instead of two. Use it when the sequence is large and you need both boundaries. For a single value, Min or Max remains the clearer choice.

Getting the Object, Not Just the Value

A common misconception is that Max returns the object with the largest projected value. It does not. The selector determines what is compared, but the return value is the projected value, not the source object.

To retrieve the employee with the highest salary, OrderByDescending followed by First is the most readable option:

Employee highestPaid = employees .OrderByDescending(e => e.Salary) .First();

This sorts the entire sequence, which is O(n log n), even though only one element is needed. For small collections this is fine. For large collections, Aggregate performs a single pass:

Employee highestPaid = employees.Aggregate( (best, current) => current.Salary > best.Salary ? current : best);

Aggregate is more verbose but avoids the sort. Choose based on collection size and how much readability matters relative to the extra work.

When Min and Max Are Not the Right Tool

Min and Max suit simple numeric comparisons and property projections. They are not the right choice when:

  • You need the element that contains the minimum or maximum value rather than the value itself. OrderBy or Aggregate is more appropriate.
  • The comparison logic is more complex than a single numeric comparison, such as culture-sensitive string ordering.
  • The sequence is already sorted, where the first or last element gives the answer without a scan.

For strings, Min and Max use the default string comparer, which is culture-sensitive in some contexts. If you need ordinal comparison, use the overload that accepts an IComparer<T>:

var earliest = names.Min(StringComparer.Ordinal);

This overload is less common but useful when the default comparison does not match your requirements. The same overload accepts custom comparers for your own types, letting you define ordering rules beyond the default IComparable<T> implementation.

c# linq min max: Practical Usage and Code Examples | RYUSLOG DEV