Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

LINQ Part 2: Standard Methods - Tools in the Toolbox

0.00/5 (No votes)
18 Apr 2018 4  
A very quick survey of the standard LINQ methods defined in the System.Linq.Enumerable class

Introduction

In the first part of this series, we took a deep dive into IEnumerable and began introducing some of the standard LINQ methods. These methods are all defined within the System.Linq.Enumerable class, mostly as extension methods to IEnumerable.

If you’re unclear on extension methods or IEnumerable, please review the previous article:

This article makes heavy use of Lambda Expressions and Anonymous Types. If you are unfamiliar with these concepts, a very brief description of each is provided at the end of this article.

Background

In this article, we’ll conduct a quick survey of the standard LINQ methods. We’ll provide sample code and output for each. This article is simply intended to introduce you to these methods. It is not intended to make you an expert in their use.

To facilitate skim reading, the examples are intentionally as short as I can make them. They use only the simplest of the available overloads for these methods. Think of this as a list of available ingredients. Instructions for baking the cake, with these ingredients, will come in later articles.

The best way to read this article is to take a very quick pass through it. Do not worry about remembering the details. You can always refer back to this article or to the relevant Microsoft documentation. Links to this documentation are included at the end of the article.

This is the second in a series of articles on LINQ. Links to other articles in this series are as follows:

New Sequences

Some of the methods in System.Linq.Enumerable create entirely new sequences. These include: Empty, Range, and Repeat.

Empty

The Empty method creates an empty sequence (with no items).

WriteSequence("Empty", Enumerable.Empty<int>());
// Empty:

Range

The Range method creates a sequence containing a range of integer values.

WriteSequence("Range", Enumerable.Range(1, 5));
// Range: 1, 2, 3, 4, 5

Repeat

The Repeat method creates a sequence containing repeated occurrences of a single item.

WriteSequence("Repeat", Enumerable.Repeat("ABC", 3));
// Repeat: ABC, ABC, ABC

Sequences in Original Order

Some methods return a new sequence where all or a portion of the original sequence is included, in its original order. These methods do not require materialization of the sequence to begin returning items. These methods include: Append, AsEnumerable, Cast, Concat, DefaultIfEmpty, OfType, Prepend, Range, Repeat, Select, SelectMany, Skip, SkipWhile, Take, TakeWhile, Where, and Zip.

Since they are the simplest to describe, we’ll tackle them first:

Append

The Append method adds a single item to the end of a sequence.

string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence(berries.Append("strawberry"));
// Append: blackberry, blueberry, raspberry, strawberry

AsEnumerable

The AsEnumerable method is a bit beyond the scope of this article. Basically, it allows a transition from the other flavor of LINQ (IQueryable) to the flavor described in this article (IEnumerable). We’ll revisit this method in later articles and explain in more detail.

string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("AsEnumerable", berries.AsEnumerable());
// AsEnumerable: blackberry, blueberry, raspberry

Cast

Sometimes, it is necessary to deal with a weakly-typed sequence, especially when dealing with older .NET data types. In these cases, the sequence may implement IEnumerable, but not IEnumerable<T>. When all of the data types in the sequence are known to be of the same data type, the Cast method allows you to cast all of the items to that data type.

The Cast method casts every item in the sequence to a different data type.

var intList = new ArrayList { 123, 456, 789 };
IEnumerable<int> ints = intList.Cast<int>();
WriteSequence("Cast", ints);
// Cast: 123, 456, 789

If any of items are of a different data type, than what is specified, an exception will occur. In this instance, the OfType method would be preferable.

var list = new ArrayList { "Alpha", 123, "Beta", "Gamma" };
WriteSequence("Cast", list.Cast<string>());
// Unhandled Exception: System.InvalidCastException: Specified cast is not valid.

Concat

The Concat method adds items, from another sequence, to the end of a sequence.

string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Concat", berries.Concat(citrus));
// Concat: blackberry, blueberry, raspberry, grapefruit, lemon, lime, orange

DefaultIfEmpty

The DefaultIfEmpty method gets either the original sequence or a sequence with a single default value, if the original sequence is empty (contains no items).

byte[] bytes = { 1, 2, 3, 4 };
WriteSequence("DefaultIfEmpty: ", bytes.DefaultIfEmpty());
// DefaultIfEmpty: 1, 2, 3, 4

byte[] emptyBytes = { };
WriteSequence("DefaultIfEmpty: ", emptyBytes.DefaultIfEmpty());
// DefaultIfEmpty: 0

WriteSequence("DefaultIfEmpty: ", emptyBytes.DefaultIfEmpty((byte)123));
// DefaultIfEmpty: 123

OfType

Sometimes, it is necessary to deal with a weakly-typed sequence, especially when dealing with older .NET data types. In these cases, the sequence may implement IEnumerable, but not IEnumerable<T>. The OfType method allows you to select items (of a homogenous type) from that sequence and produce an IEnumerable<T> instance.

var list = new ArrayList { "Alpha", 123, "Beta", "Gamma" };
WriteSequence("OfType", list.OfType<string>());
// OfType: Alpha, Beta, Gamma

Prepend

The Prepend method adds a single item to the start of a sequence.

string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Prepend", berries.Prepend("strawberry"));
// Prepend: strawberry, blackberry, blueberry, raspberry

Select

The Select method alters every item in a sequence.

string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Select", berries.Select(berry => berry + " more"));
// Select: blackberry more, blueberry more, raspberry more

SelectMany

The SelectMany method alters multiple sequences and flattens them into a single sequence.

int[][] manyNumbers = { new int[] { 1, 2 }, new int[] { 3, 4 }, new int[] { 5, 6 } };
WriteSequence("SelectMany", manyNumbers.SelectMany(numbers => numbers.Append(9)));
// SelectMany: 1, 2, 9, 3, 4, 9, 5, 6, 9

Skip

The Skip method skips a fixed number of items at the start of a sequence.

char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Skip", letters.Skip(1));
// Skip: B, C, D

SkipWhile

The SkipWhile skips items, at the start of a sequence, while they meet a specified condition.

char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("SkipWhile", letters.SkipWhile(letter => letter < 'C'));
// SkipWhile: C, D

Take

The Take method takes a fixed number of items from the start of a sequence.

char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Take", letters.Take(1));
// Take: A

TakeWhile

The TakeWhile method takes items, from the start of a sequence, while they meet a specified condition.

char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("TakeWhile", letters.TakeWhile(letter => letter < 'C'));
// TakeWhile: A, B

Where

The Where method takes all of the items that meet a specified condition.

string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
WriteSequence("Where", citrus.Where(name => name.Contains("a")));
// Where: grapefruit, orange

Zip

The Zip method combines a sequence with items from another sequence.

string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Zip", citrus.Zip(letters, (fruit, letter) => $"{fruit} {letter}"));
// Zip: grapefruit A, lemon B, lime C, orange D

Sequence in New Order

Some methods return a new sequence where all or a portion of the original sequence is included, but in a different order. While execution is still deferred until you begin consuming the sequence, this can be deceptive. In order to return the initial item in the sequence, these methods must first evaluate (and materialize) either a portion or all of the sequence. These methods include: Distinct, Except, GroupBy, GroupJoin, Intersect, Join, OrderBy, OrderByDescending, Reverse, ThenBy, ThenByDescending, and Union.

Distinct

The Distinct method selects unique items from a sequence. When the same item occurs multiple in a sequence, only one instance of the item is included.

string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("Distinct", stooges.Distinct());
// Distinct: Moe Howard, Larry Fine, Curly Howard

Except

The Except method gets the unique items from a sequence that are not present in another sequence.

string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Except: ", fruits.Except(citrus));
// Except: Banana

GroupBy

The GroupBy method groups items in a sequence by a unique trait (or key). It produces a sequence of groupings, where each grouping is itself a sequence, with an associated key value.

string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("GroupBy: ", stooges.GroupBy(name => GetLastWord(name)));
// GroupBy: [Key=Howard, Items=Moe Howard, Curly Howard, Moe Howard], [Key=Fine, Items=Larry Fine]

The code for the GetLastWord method is as follows:

private static string GetLastWord(string words) =>
  words.Substring(words.LastIndexOf(' ') + 1);

GroupJoin

The GroupJoin method, which is similar to a SQL LEFT OUTER JOIN, is unusually difficult to demonstrate. For this reason, its full description is provided later in this article, under the heading “GroupJoin and Join”.

var courseFaculty = courses.GroupJoin(faculty, course => course.Id, teacher => teacher.CourseId,
  (course, teachers) => new { Course = course.Name, Teachers = teachers });

Intersect

The Intersect method gets the distinct items common to two sequences.

string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Intersect: ", citrus.Intersect(fruits));
// Intersect: lemon, lime

Join

The Join method, which is similar to a SQL LEFT INNER JOIN, is unusually difficult to demonstrate. For this reason, its full description is provided later in this article, under the heading “GroupJoin and Join”.

var courseTeacher = courses.Join(faculty, course => course.Id, teacher => teacher.CourseId,
  (course, teacher) => new { Course = course.Name, Teacher = teacher.Name });

OrderBy

The OrderBy method sorts the sequence, in ascending order, by a key value obtained from each item in the sequence.

string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("OrderBy: ", stooges.OrderBy(name => GetFirstWord(name)));
// OrderBy: Curly Howard, Larry Fine, Moe Howard, Moe Howard

The code for the GetFirstWord method is as follows:

private static string GetFirstWord(string words) =>
  words.Substring(0, words.IndexOf(' '));

OrderByDescending

The OrderByDescending method sorts the sequence, in descending order, by a key value obtained from each item in the sequence.

string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("OrderByDescending: ", stooges.OrderByDescending(name => GetFirstWord(name)));
// OrderByDescending: Moe Howard, Moe Howard, Larry Fine, Curly Howard

The code for the GetFirstWord method is as follows:

private static string GetFirstWord(string words) =>
  words.Substring(0, words.IndexOf(' '));

Reverse

The Reverse method reverses the order of the sequence.

string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("Reverse: ", stooges.Reverse());
// Reverse: Moe Howard, Curly Howard, Larry Fine, Moe Howard

ThenBy

The ThenBy method is somewhat unique in that it can only be applied to an IOrderedEnumerable<T>, generally returned from one of the following methods: OrderBy, OrderByDescending, ThenBy, or ThenByDescending. This method provides the capability to sort a sequence, in ascending order, by an additional key value, also obtained from each item in the sequence.

var sortedStooges = stooges
  .OrderBy(name => GetLastWord(name))
  .ThenBy(name => GetFirstWord(name));
WriteSequence("ThenBy: ", sortedStooges);
// ThenBy: Larry Fine, Curly Howard, Moe Howard, Moe Howard

ThenByDescending

The ThenByDescending method is somewhat unique in that it can only be applied to an IOrderedEnumerable<T>, generally returned from one of the following methods: OrderBy, OrderByDescending, ThenBy, or ThenByDescending. This method provides the capability to sort a sequence, in descending order, by an additional key value, also obtained from each item in the sequence.

var sortedStooges = stooges
  .OrderBy(name => GetLastWord(name))
  .ThenByDescending(name => GetFirstWord(name));
WriteSequence("ThenByDescending: ", sortedStooges);
// ThenByDescending: Larry Fine, Moe Howard, Moe Howard, Curly Howard

Union

The Union method gets the distinct items resulting from the combination of two sequences.

string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Union: ", citrus.Union(fruits));
// Union: grapefruit, lemon, lime, orange, banana

Singleton Methods

The singleton methods force immediate materialization of at least a portion of the sequence. These methods include: Aggregate, All, Any, Average, Contains, Count, ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, LongCount, Max, Min, SequenceEqual, Single, SingleOrDefault, Sum, ToArray, ToDictionary, ToList, and ToLookup.

Aggregate

The Aggregate method accumulates the items in a sequence to return a singleton, aggregate value. The code below is just an example. It is a terrible implementation of String.Join, which is far more appropriate if you ever need to simply aggregate strings.

string[] berries = { "blackberry", "blueberry", "raspberry" };
string aggregate = berries.Aggregate((accumulator, item) => accumulator + " " + item);
Console.WriteLine($"Aggregate: {aggregate}");
// Aggregate: blackberry blueberry raspberry

Warning: If the sequence has zero items, an exception will occur. If the sequence has only one item, the first item is simply returned, without ever invoking the method you provide. For all other sequences, the first invocation of your method will be called with the first two items of the sequence.

All

The All method gets a Boolean value indicating if all items in a sequence match the specified condition. This method stops when it reaches the first item that does not match the specified condition.

char[] letters = { 'A', 'B', 'C', 'D'};
bool all = letters.All(item => Char.IsLetter(item));
Console.WriteLine($"All: {all}");
// All: True

Any

The Any method gets a Boolean value indicating if any items in a sequence match the specified condition. This method stops when it reaches the first item that matches the specified condition.

char[] letters = { 'A', 'B', 'C', 'D'};
bool any = letters.Any(item => Char.IsLetter(item));
Console.WriteLine($"Any: {any}");
// Any: True

Average

The Average method averages all of the items in the sequence and returns a floating point result.

byte[] bytes = { 1, 2, 3, 4 };
float average = bytes.Average(item => (float)item);
Console.WriteLine($"Average: {average}");
// Average: 2.5

Contains

The Contains method gets a Boolean value indicating if the sequence contains the specified item. This method stops on the first item that matches the specified item.

char[] letters = { 'A', 'B', 'C', 'D'};
bool contains = letters.Contains('C');
Console.WriteLine($"Contains {contains}");
// Contains: True

Count

The Count method gets the number of items in the sequence.

// The Length property would be more efficient here
byte[] bytes = { 1, 2, 3, 4 };
int count = bytes.Count();
Console.WriteLine($"Count: {count}");
// Count: 4

The Count method can also get the number of items in a sequence that match a specified condition.

count = bytes.Count(item => (item & 1) == 0);
Console.WriteLine($"Count: {count}");
// Count: 2

Warning: IEnumerable<T>.Count method must evaluate every item in the sequence and can be quite resource intensive. It should not be confused with the ICollection<T>.Count property or the Array.Length property. Both of these (most often) simply return a pre-existing count and are (generally) not resource intensive.

ElementAt

The ElementAt method gets the item at the specified zero-based index within the sequence.

byte[] bytes = { 1, 2, 3, 4 };
byte elementAt = bytes.ElementAt(1);
Console.WriteLine($"ElementAt: {elementAt}");
// ElementAt: 2

elementAt = bytes.ElementAt(10);
// Unhandled Exception: System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.

Warning: This should not be confused with indexing functionality provided by arrays and lists. Unlike true indexing, the ElementAt method must evaluate and skip all items prior to the specified index. It is functionally equivalent to the following LINQ: IEnumerable<T>.Skip(index).First().

ElementAtOrDefault

The ElementAtOrDefault method gets the item at the specified zero-based index within the sequence, if the specified item does not-exist, it returns a default value.

byte elementAtOrDefault = bytes.ElementAt(1);
Console.WriteLine($"ElementAtOrDefault: {elementAtOrDefault}");
// ElementAt: 2

elementAtOrDefault = bytes.ElementAtOrDefault(-1);
Console.WriteLine($"ElementAtOrDefault: {elementAtOrDefault}");
// ElementAt: 0

Warning: This should not be confused with indexing functionality provided by arrays and lists. Unlike true indexing, the ElementAtOrDefault method must evaluate and skip all items prior to the specified index.  It is functionally equivalent to the following LINQ: IEnumerable<T>.Skip(index).FirstOrDefault().

First

The First method gets the first item in the sequence or the first item matching a specified condition. If it is acceptable for the sequence to be empty, the FirstOrDefault method is preferable. If it is not acceptable for more than one item to be in the sequence, the Single/SingleOrDefault methods are preference.

string[] berries = { "blackberry", "blueberry", "raspberry" };
string first = berries.First();
Console.WriteLine("First: {first}");
// First: blackberry

first = berries.First(item => item.StartsWith("r"));
Console.WriteLine($"First: {first}");
// First: raspberry

byte[] emptyBytes = { };
byte firstEmpty = emptyBytes.First();
// Unhandled Exception: System.InvalidOperationException: Sequence contains no elements

FirstOrDefault

The FirstOrDefault method gets the first item in the sequence or the first item matching a specified condition. It will return a default value, if the sequence is empty. If it is not acceptable for more than one item to be in the sequence, the SingleOrDefault method is preference.

string[] berries = { "blackberry", "blueberry", "raspberry" };
string firstOrDefault = berries.FirstOrDefault();
Console.WriteLine($"FirstOrDefault: {firstOrDefault}");
// FirstOrDefault: blackberry

firstOrDefault = berries.FirstOrDefault(item => item.StartsWith("r"));
Console.WriteLine($"FirstOrDefault: {firstOrDefault}");
// FirstOrDefault: raspberry

byte[] emptyBytes = { };
byte firstEmpty = emptyBytes.FirstOrDefault();
Console.WriteLine($"FirstOrDefault: {firstEmpty}");
// FirstOrDefault: 0

Last

The Last method gets the last item in the sequence. If it is acceptable for the sequence to be empty, the LastOrDefault method is preferable.

string last = berries.Last();
Console.WriteLine($"Last: {last}");
// Last: raspberry

last = berries.Last(item => item.StartsWith("b"));
Console.WriteLine($"Last: {last}");
// Last: blueberry

byte[] emptyBytes = { };
byte lastEmpty = emptyBytes.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastEmpty}");
// Unhandled Exception: System.InvalidOperationException: Sequence contains no elements

Warning: Unlike the First method, the Last method must visit every item in the sequence. This can be very resource intensive for larger sequences.

LastOrDefault

The LastOrDefault method gets the last item in the sequence or the last item matching a specified condition. It will return a default value, if the sequence is empty.

string lastOrDefault = berries.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastOrDefault}");
// LastOrDefault: raspberry

lastOrDefault = berries.LastOrDefault(item => item.StartsWith("b"));
Console.WriteLine($"LastOrDefault: {lastOrDefault}");
// LastOrDefault: blueberry

byte[] emptyBytes = { };
byte lastEmpty = emptyBytes.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastEmpty}");
// LastOrDefault: 0

Warning: Unlike the FirstOrDefault method, the LastOrDefault must visit every item in the sequence. This can be very resource intensive for larger sequences.

LongCount

The LongCount method gets the number of items in the sequence (as a 64 bit count).

// The Length property would be more efficient here
byte[] bytes = { 1, 2, 3, 4 };
long count = bytes.LongCount();
Console.WriteLine($"LongCount: {count}");
// Count: 4

The LongCount method can also get the number of items in a sequence that match a specified condition (as a 64 bit count).

count = bytes.LongCount(item => (item & 1) == 0);
Console.WriteLine($"LongCount: {count}");
// Count: 2

Warning: IEnumerable<T>.LongCount method must evaluate every item in the sequence and can be quite resource intensive. It should not be confused with the ICollection<T>.Count property or the Array.Length property. Both of these (most often) simply return a pre-existing count and are (generally) not resource intensive.

Max

The Max method gets the greatest value in a sequence.

byte[] bytes = { 1, 2, 3, 4 };
byte max = bytes.Max();
Console.WriteLine($"Max: {max}");
// Max: 4

string[] textNumbers = { "1", "2", "3", "4" };
max = textNumbers.Max(item => byte.Parse(item));
Console.WriteLine($"Max: {max}");
// Max: 4

Min

The Min method gets the smallest value in a sequence.

byte[] bytes = { 1, 2, 3, 4 };
byte min = bytes.Min();
Console.WriteLine($"Min: {min}");
// Min: 1

string[] textNumbers = { "1", "2", "3", "4" };
min = textNumbers.Min(item => byte.Parse(item));
Console.WriteLine($"Min: {min}");
// Min: 1

SequenceEqual

The SequenceEquals method gets a Boolean value indicating if two sequences have an equal number of items that are equal and appear in the same order.

byte[] bytes = { 1, 2, 3, 4 };
bool sequenceEqual = bytes.SequenceEqual(bytes.Take(bytes.Length));
Console.WriteLine($"SequenceEqual: {sequenceEqual}");
// SequenceEqual: True

sequenceEqual = bytes.SequenceEqual(bytes.Take(bytes.Length - 1));
Console.WriteLine($"SequenceEqual: {sequenceEqual}");
// SequenceEqual: False

Single

The Single method gets the first item in the sequence or the first item matching a specified condition. This method throws an exception if there is not exactly one matching item. If it is acceptable for the sequence to be empty, the SingleOrDefault method is preferable.

string[] berry = { "blackberry" };
string single = berry.Single();
Console.WriteLine($"Single: {single}");
// Single: blackberry

byte singleEmpty = emptyBytes.Single();
// Unhandled Exception: System.InvalidOperationException: Sequence contains no elements

string[] berries = { "blackberry", "blueberry", "raspberry" };
single = berries.Single();
// Unhandled Exception: System.InvalidOperationException: Sequence contains more than one element

SingleOrDefault

The SingleOrDefault method gets the first item in the sequence and throws an exception if more than one item exists and returns a default value if the sequence is empty.

string[] berry = { "blackberry" };
string singleOrDefault = berry.SingleOrDefault();
Console.WriteLine($"SingleOrDefault: {singleOrDefault}");
// Single: blackberry

byte singleEmpty = emptyBytes.SingleOrDefault();
Console.WriteLine($"SingleOrDefault: {singleEmpty}");
// SingleOrDefault: 0

string[] berries = { "blackberry", "blueberry", "raspberry" };
string singleOrDefault = berries.SingleOrDefault();
// Unhandled Exception: System.InvalidOperationException: Sequence contains more than one element

Sum

The Sum method gets a sum of all of the values in the sequence.

byte[] bytes = { 1, 2, 3, 4 };
float sum = bytes.Sum(item => (float)item);
Console.WriteLine($"Sum: {sum}");
// Sum: 10

ToArray

The ToArray method converts the sequence to an array of TItem[].

byte[] bytes = { 1, 2, 3, 4 };
byte[] byteArray = bytes
  .Take(2)
  .ToArray();

Warning: This method should be avoided unless strictly necessary. Executing this method causes every item in the sequence to be evaluated. It also requires that all members of the sequence are present in memory at the same instant. For larger sequences, this can be very resource intensive.

ToDictionary

The ToDictionary method converts the sequence to a System.Collections.Generic.Dictionary<TKey, TITem>.

string[] numberFruits = { "1 Apple", "2 Orange", "3 Cherry" };
Dictionary<int, string> fruitDictionary = numberFruits.ToDictionary(
  item => int.Parse(GetFirstWord(item)), item => GetLastWord(item));

Warning: This method should be avoided unless strictly necessary. Executing this method causes every item in the sequence to be evaluated. It also requires that all members of the sequence are present in memory at the same instant. For larger sequences, this can be very resource intensive.

ToList

The ToList method converts the sequence to a System.Collections.Generic.List<TItem>.

byte[] bytes = { 1, 2, 3, 4 };
List<byte> byteList = bytes.ToList();

Warning: This method should be avoided unless strictly necessary. Executing this method causes every item in the sequence to be evaluated. It also requires that all members of the sequence are present in memory at the same instant. For larger sequences, this can be very resource intensive.

ToLookup

The ToLookup method converts the sequence to a System.Linq.Lookup<TKey, TItem> instance.

ILookup<int, string> fruitLookup = numberFruits.ToLookup(
  item => int.Parse(GetFirstWord(item)));

Warning: This method should be avoided unless strictly necessary. Executing this method causes every item in the sequence to be evaluated. It also requires that all members of the sequence are present in memory at the same instant. For larger sequences, this can be very resource intensive.

GroupJoin and Join

Both the GroupJoin and Join methods are unusually complex. Here, we will provide a brief description of each. In both cases, we will use the following data:

var courses = new[]
{
  new { Id = 1, Name = "Geometry" },
  new { Id = 2, Name = "Physics" },
  new { Id = 3, Name = "Parapsychology" },
};

var faculty = new[]
{
  new { CourseId = 1, Name = "Pythagoras" },
  new { CourseId = 1, Name = "Euclid" },
  new { CourseId = 2, Name = "Albert Einstein" },
  new { CourseId = 2, Name = "Isaac Newton"}
};

GroupJoin

The GroupJoin method is a bit complex to describe. It joins all of the items in one sequence with matching items from a second sequence. It also groups those matching items. For those familiar with SQL, it is similar to (but frustratingly different from) a LEFT OUTER JOIN. The simplest overload accepts the following parameters:

  • The sequence to join.
  • A method to get the join key from items in the original sequence.
  • A method to get the join key from items in the sequence to join.
  • A method to join an item from the original sequence with a matching item in the sequence to join (or null when no match exists).
var courseFaculty = courses.GroupJoin(faculty, course => course.Id, teacher => teacher.CourseId,
  (course, teachers) => new { Course = course.Name, Teachers = teachers });
Console.Write("GroupBy: ");
int index = 0;
foreach (var courseTeachers in courseFaculty)
{
  if (index++ >= 1)
    Console.Write(", ");
  WriteSequence($"[Course={courseTeachers.Course}, Teachers=", courseTeachers.Teachers, false);
  Console.Write("]");
}
Console.WriteLine();
// GroupBy: [Course = Geometry, Teachers ={ CourseId = 1, Name = Pythagoras }, { CourseId = 1, Name = Euclid }], [Course=Physics, Teachers={ CourseId = 2, Name = Albert Einstein }, { CourseId = 2, Name = Isaac Newton }], [Course=Parapsychology, Teachers=]

Because the results of a GroupJoin are a bit unwieldly, one common technique of dealing with this issue is to “flatten” the results to make them more closely resemble a LEFT OUTER JOIN.

var flattened = courseFaculty.SelectMany(
  courseTeachers => courseTeachers.Teachers.DefaultIfEmpty(),
  (courseTeachers, teacher) => new { courseTeachers.Course, Teacher = teacher?.Name });
WriteSequence("GroupBy/SelectMany: ", flattened);
// GroupBy / SelectMany: { Course = Geometry, Teacher = Pythagoras }, { Course = Geometry, Teacher = Euclid }, { Course = Physics, Teacher = Albert Einstein }, { Course = Physics, Teacher = Isaac Newton }, { Course = Parapsychology, Teacher =  }

Join

The Join method joins matching items from two different sequences. For those familiar with SQL, it is nearly identical to LEFT INNER JOIN. The simplest overload accepts the following parameters:

  • The sequence to join.
  • A method to get the join key from items in the original sequence.
  • A method to get the join key from items in the sequence to join.
  • A method to join an item from the original sequence with a matching item in the sequence to join.
var courseTeacher = courses.Join(faculty, course => course.Id, teacher => teacher.CourseId,
  (course, teacher) => new { Course = course.Name, Teacher = teacher.Name });
WriteSequence("Join: ", courseTeacher);
// Join: { Course = Geometry, Teacher = Pythagoras }, { Course = Geometry, Teacher = Euclid }, { Course = Physics, Teacher = Albert Einstein }, { Course = Physics, Teacher = Isaac Newton }

Anonymous Types

Anonymous types were introduced in .NET 3.0 to accompany LINQ. They provide a syntactical short cut so that a type can be both be declared and initialized in a single step. With anonymous types, the following is possible:

var myAnonymousType = new { Id = 1, Name = "Me" };
Console.WriteLine(myAnonymousType.Name);
// { Id = 1, Name = Me }

Prior to .NET 3.0, to achieve this same goal we would first need to define the type:

private class NotSoAnonymousName
{
  private string name;
  private int id;
  
  public int Id { get { return id; } set { id = value; } }
  public string Name { get { return name; } set { name = value; } }
  
  public override string ToString()
  {
    return string.Format("{{ Id = {0}, Name = {1} }}", Id, Name);
  }
}

And then reference the type by name:

NotSoAnonymousName notSoAnonymousType = new NotSoAnonymousName { Id = 1, Name = "Me" };
Console.WriteLine(myNotSoAnonymousType);
// { Id = 1, Name = Me }

The class declaration is especially verbose, because auto-implemented properties, expression bodied members, and string interpolation were also unavailable at that time. For reference, had they been available, the class declaration could have at least been shortened to:

private class NotSoAnonymousName
{
  public int Id { get; set; }
  public string Name { get; set; }   public override string ToString() =>
    $"{{ Id = {Id}, Name = {Name} }}";
}

Lambda Expressions

Lambda expressions were introduced in .NET 3.0 to accompany LINQ. They provide a syntactical short cut to creating and sharing anonymous method declarations. With Lambda expressions, the following is possible:

string[] berries = { "blackberry", "blueberry", "raspberry" };
string firstBerry = berries.First(item => item.StartsWith("r"));
Console.WriteLine($"First: {firstBerry}");
// First: blackberry

The Lambda expression consists of parameters (left side) and method logic (right side), separated by “=>”:

item => item.StartsWith("r")

Prior to Lambda expressions, to achieve the same goal we would be forced to declare a method:

private static bool StartswithAnR(string item)
{
  return item.StartsWith("r");
}

And pass its delegate as a parameter:

firstBerry = berries.First(StartswithAnR);
Console.WriteLine($"First: {firstBerry}");
// First: blackberry

With the previous Lambda method, we are essentially declaring an anonymous method that accepts one parameter (item) and returns a value. The type of the value (string) is inferred by the way that item is used. The type of the return value (bool) is similarly inferred.

C# also provides a whole bunch of pre-rolled delegates. This allows us to have type-safe method delegates for parameter and variable declarations. One of these is the Func delegate, its final type parameter (bool in this case) is the return value type, all previous type parameters (string in this case) indicate the type of parameters expected by the function/method.

Using the same Lamba expression from before we can assign it to a delegate and then use that delegate:

Func<string, bool> predicate = item => item.StartsWith("r");
bool startsWithAnR = predicate("right");
Console.WriteLine($"startsWithAnR = {startsWithAnR}");
// startsWithAnR = True Below is an example of a Lambda expression with multiple parameters.

Func<string, string, string> concatenate = (textA, textB) => textA + textB;
string result = concatenate("left ", "right");
Console.WriteLine($"result = {result}");
// result = left right

WriteSequence Methods

The following methods are used throughout this article.

private static void WriteSequence<TItem>(string prefix, IEnumerable<TItem> sequence,
  bool writeLine = true)
{
  Console.Write(prefix);
  int index = 0;

  foreach (TItem item in sequence)
  {
    if (index++ >= 1)
      Console.Write(", ");
    Console.Write(item);
  }

  if (writeLine)
    Console.WriteLine();
}
private static void WriteSequence<TKey, TItem>(string prefix,
  IEnumerable<IGrouping<TKey, TItem>> sequence)
{
  Console.Write(prefix);

  int index = 0;

  foreach (IGrouping<TKey, TItem> grouping in sequence)
  {
    if (index++ >= 1)
      Console.Write(", ");
    WriteSequence($"[Key={grouping.Key}, Items=", grouping, false);
    Console.Write("]");
  }
  Console.WriteLine();
}

Further Reading

For further reading, see the following:

Standard Query Operators Overview (C#)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/standard-query-operators-overview

Anonymous Types (C# Programming Guide)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/anonymous-types

Lambda Expressions (C# Programming Guide)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions

Extension Methods (C# Programming Guide)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods

String Interpolation (C# Reference)
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated

History

  • 4/18/2018 - The original version was uploaded
  • 4/18/2018 - Fixed incorrect comment on Join
  • 4/21/2018 - Added links to other articles in this series
  • 4/25/2018 - Added link to fourth article in series

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here