C# vs JavaScript: LINQ

This post belongs to the C# vs JavaScript series.
by Nicklas Envall

If you've used JavaScript for some time, then you probably have gotten used to using higher-order functions such as reduce, map, and filter. So it can be de-motivating to find that many C# examples on the web use loops or conventional static methods.

But don't give up hope on C# just yet, because we have System.Linq.

In LINQ, we have sequences and elements. Any object that implements IEnumerable<T> is a sequence, and any item within a sequence is an element. We use methods known as query operators to transform sequences, which take an input sequence and outputs a transformed output sequence. They return the new sequence without altering the input sequence.

There are three different types of query operators:

  1. Sequence in and sequence out.
  2. Sequence in and element or scalar value out.
  3. Nothing in and sequence out (generation methods)

LINQ provides us with so-called standard query operators that we may use. It provides us with methods for filtering, aggregation, concatenation, and much more. We'll now compare C# LINQ with JavaScript by looking at commonly used methods.

Sequence in and sequence out

Our map, filter, and slice functions are also available in LINQ. But with different names, so let us take a look.

Map in LINQ

Array.prototype.map calls a function on every element in an array, and the result of that gets returned as a new array:

const numbers = [1, 2, 3, 4, 5, 6];
const squaredNumbers = numbers.map(val => Math.pow(val, 2));
squaredNumbers; // [1, 4, 9, 16, 25, 36]

JavaScript's map function is equivalent to LINQ's Select method:

var numbers = new int[] { 1, 2, 3, 4, 5, 6 };
var squaredNumbers = numbers.Select(val => Math.Pow(val, 2));
Console.WriteLine(String.Join(", ", squaredNumbers)); // 1, 4, 9, 16, 25, 36

Filter in LINQ

Array.prototype.filter creates a new array and keeps all the elements that return true on the provided function when passed to it:

const numbers = [1, 2, 3, 4, 5, 6];
const oddNumbers = numbers.filter(val => val % 2 !== 0);
const evenNumbers = numbers.filter(val => val % 2 === 0);
oddNumbers; // [1, 3, 5]
evenNumbers; // [2, 4, 6]

JavaScript's filter function is equivalent to LINQ's Where method:

var numbers = new int[] { 1, 2, 3, 4, 5, 6 };
var oddNumbers = numbers.Where(el => el % 2 != 0);
var evenNumbers = numbers.Where(el => el % 2 == 0);
String.Join(", ", oddNumbers); // 1, 3, 5
String.Join(", ", evenNumbers); // 2, 4, 6

Slice in LINQ

Array.prototype.slice(start, end) takes a start or start and end index, which represents what portion of the array that we want to recreate:

const numbers = [1, 2, 3, 4, 5, 6];
numbers.slice(0, 3); // [1, 2, 3]
numbers.slice(3); // [4, 5, 6]
numbers.slice(2, 5); // [3, 4, 5]

In LINQ, we have both Take and Skip that we can use separately or in combination by chaining:

var numbers = new int[] { 1, 2, 3, 4, 5, 6 };
numbers.Take(3); // 1, 2, 3
numbers.Skip(3); // 4, 5, 6
numbers.Skip(2).Take(3); // 3, 4, 5

Sequence in and element or scalar value out

Our reduce, some, every, and includes functions are also available in LINQ. But yet again with different names.

Reduce in LINQ

To find the sum of all even numbers in an array, we could do the following in JavaScript with Array.prototype.reduce:

const numbers = [1, 2, 3, 4, 5, 6]; 
const sumOfEvenNumbers = numbers.reduce((sum, number) => number % 2 === 0 ? sum + number : sum, 0)
sumOfEvenNumbers; // 12

The equivalent solution in C# would be with the Aggregate method. As we see, it has an initial accumulator value which is 0, and that gets referred to as the seed value.

var numbers = new int[] { 1, 2, 3, 4, 5, 6 };
var sumOfEvenNumbers = numbers.Aggregate(0, (sum, number) => number % 2 == 0 ? sum + number : sum);
sumOfEvenNumbers; // 12

Some in LINQ

Array.prototype.some returns true if the provided predicate returns true on any element, else it returns false.

const numbers = [1, 2, 3, 4, 5, 6];
const oddNumberExists = numbers.some(el => el % 2 !== 0);
oddNumberExists; // true

JavaScript's some function is equivalent to LINQ's Any method:

var numbers = new int[] { 1, 2, 3, 4, 5, 6 };
bool oddNumberExists = numbers.Any(el => el % 2 != 0);
Console.WriteLine(oddNumberExists);

Every in LINQ

Array.prototype.every returns true if all functions returns true, else false:

const numbers = [1, 2, 3, 4, 5, 6];
const onlyOddNumbers = numbers.every(el => el % 2 !== 0);
onlyOddNumbers; // false

JavaScript's every function is equivalent to LINQ's All method:

var numbers = new int[] { 1, 2, 3, 4, 5, 6 };
bool onlyOddNumbers = numbers.All(el => el % 2 != 0);
Console.WriteLine(onlyOddNumbers);

Includes in LINQ

To see if an array contains something, we can use Array.prototype.includes in JavaScript:

const numbers = [1, 2, 3, 4, 5, 6];
numbers.includes(2); // true
numbers.includes(9); // false

JavaScript's includes function is equivalent to LINQ's Contains method:

var numbers = new int[] { 1, 2, 3, 4, 5, 6 };
numbers.Contains(2); // True
numbers.Contains(9); // False

Nothing in and sequence out (generation methods)

To create an array in JavaScript "out of thin air," may use Array.from:

const numbers = Array.from({ length: 5 }, (_, i) => i + 1);
numbers; // [1, 2, 3, 4, 5]

In C#, we have Enumerable.Range(Int32, Int32) which allow us to generate a sequence of numbers based on a range:

var numbers = Enumerable.Range(1, 5);
String.Join(", ", numbers); // 1, 2, 3, 4, 5

LINQ Gotchas

There are also some "gotchas" to be aware of when working with LINQ.

Deferred Execution

Be aware of deferred execution:

List<string> names = new List<string> { "james" };
var upperCasedNames = names.Select(name => name.ToUpper());
names.Add("jennifer");

String.Join(", ", upperCasedNames); // JAMES, JENNIFER

The reason for this deferred execution is that most query operators execute when they are enumerated, not constructed. Though, not in all cases, like with Count:

List<string> names = new List<string> { "james" };
int length = names.Where(name => name.StartsWith("j")).Count();
names.Add("jennifer");

length; // 1

You can also use methods like ToList() and ToArray() to get a collection while also executing the query right away.

Subqueries

Subqueries are queries contained within another query's lambda expression. Sometimes you'll want to use queries within queries (subqueries). Though be aware that the subquery .Max() in the example below gets recalculated each iteration of the .Select() query:

var numbers = new int[] { 1, 2, 3 };
var numbersMultipliedMaxValue = numbers.Select(x => x * numbers.Max());
Console.WriteLine(String.Join(", ", numbersMultipliedMaxValue)); // 3, 6, 9

You avoid this by moving the subquery out of that query (if applicable), so it's no longer a subquery:

var numbers = new int[] { 1, 2, 3 };
var numbersMax = numbers.Max();
var numbersMultipliedMaxValue = numbers.Select(x => x * numbersMax);
Console.WriteLine(String.Join(", ", numbersMultipliedMaxValue)); // 3, 6, 9