C# LINQ Equivalents in JavaScript
When I’m working on GeoBlazor or other Blazor applications, I often have to switch between C# and JavaScript/TypeScript. While the two langauges have a lot in common (C family syntax, async/await, lambdas), one place I often get confused is when dealing with arrays or collections of items. In C#, the most straightforward way to do this is with LINQ
queries. Most of these query methods work on any collection type, including Array
, List
, ReadOnlyList
, HashSet
, and the related interfaces. The root interface necessary is IEnumerable
. With ES6 and later versions, JavaScript has adopted many of these same approaches as methods on the Array
class, using Array.prototype
(prototype is the JavaScript concept of inheritance).
In this post, I am going to discuss all of the methods of IEnumerable
in LINQ
and their JavaScript equivalents. Some of these methods are very common, while others, such as the new .NET 6 ...By
methods, are less well known. I’m also not going to deal with every possible parameter overload, as many LINQ methods allow for custom IComparer
implementations or other optional parameters. In most cases, you will see that JavaScript has fewer methods than C#, but that they can be used in many of the same ways. It is also important to note that all IEnumerable
methods return a new collection, and do not modify the original input collection. C# does have mutable collection methods, for example on the List
class, such as Add
, Remove
, Sort
, but these are kept distinct from the LINQ methods to make it clearer when you are mutating the original value. In JavaScript, both mutating and functional methods exist on Array.prototype
. In fact, there is a recent collection of new methods that only return a new array, such as toSorted
, toReversed
, and toSpliced
, but these are not yet implemented in all major browsers at the time of writing. In the comparison chart, I do use several mutating array methods, but only because there is no functional equivalent in JavaScript to compare to LINQ.
If you want to skip to the comparison table to look up a method, jump to the end of this post.
Creating a new Collection
In both languages, you can easily create a new collection. new List<T>()
in C#, or []
in JavaScript, for example. C# also has the following convenience static methods on IEnumerable
to generate a new collection.
C#
Empty
- Creates a new, emptyIEnumerable
instance.Range
- Creates a new collection of sequential intervals, with a given start index and count.Repeat
- Generates a new collection with the same item repeated a givenCount
times.
Finding a Single Record
C#
ElementAt
- Finds the element at a particular index. Similar to using an indexer (e.g.,array[index]
). Throws if the index is out of range.ElementAtOrDefault
- LikeElementAt
but returnsdefault
(null
for nullable types) if the index is out of range.First
- Finds the first item that matches the predicate. Throws on no match.FirstOrDefault
- LikeFirst
but returnsdefault
if no match is found.Last
- Finds the last item that matches the predicate. Throws on no match.LastOrDefault
- LikeLast
but returnsdefault
if no match is found.Max
- Returns the item with the highest numeric value.MaxBy
- Returns the item with the highestKey
value according to the givenIComparer
.Min
- Returns the item with the lowest numeric value.MinBy
- Returns the item with the lowestKey
value according to the givenIComparer
.Single
- LikeFirst
, but also throws if multiple matches are found.SingleOrDefault
LikeFirstOrDefault
but also throws if multiple matches are found.
As you can see, each method comes in two flavors, null
-forgiving, and null
-throwing. Like Nullable Reference Types, this is useful for explicitly declaring your intentions, and not accidentally returning a null
where you really expected there to be a value. The Single
methods also verify that you have exactly one match in your collection.
JavaScript
at
- Finds the element at a particular index.find
- Finds the first element that matches the predicate.findLast
- Finds the last element that matches the predicate.
In JavaScript, we have fewer methods that return a single value. Unlike in C#, JavaScript never throws errors if it fails to find an item. Instead, all of these methods return an undefined
value if a match is not found. There is also no equivalent of Single
in JavaScript. Instead, you would have to filter
and then throw
if you found more than one match.
Finding or Filtering Multiple Records
C#
Distinct
- Removes any duplicate entries and returns a collection in which each entry is unique.DistinctBy
- Removes entries with duplicateKey
values.Except
- Returns all items from the first collection that are not in the second collection.ExceptBy
- Returns all items from the first collection that do not have a matchingKey
value in the second collection.Intersect
- Compares two collections and returns all items that are present in both.IntersectBy
- Compares two collections and returns all items that have matchingKey
values in both.OfType<T>
- Filters the collection to only records of a particularType
.Skip
- Skips a specified number of records, and then returns the rest of the collection.SkipLast
- Returns the collection minus the specified number of records at the end.SkipWhile
- Skips forward over the collection until the predicate isfalse
, and then returns the rest of the collection.Take
- Returns the first specified number of elements in the collection.TakeLast
- Returns the specified number of elements from the end of the collection.TakeWhile
- Returns all elements from the start of the collection until the predicate isfalse
.Where
- Finds all matches to the predicate. If none found, returns an empty IEnumerable.
JavaScript
filter
- Finds all matches to the predicate. If none found, returns an empty array.slice
- Returns a sub-section of the collection by start index and optional end index.
Once again, C# has multipe methods that can filter a collection, whereas JavaScript has just the one. Yet you can easily filter
on class type or the contents of a second array.
Sorting Records
C#
Order
- Sorts the items by their default comparison in ascending order.OrderBy
- Sorts the items by aKey
value in ascending order.OrderByDescending
- Sorts the items by aKey
value in descending order.OrderDescending
- Sorts the items by their default comparison in descending order.Reverse
- Returns a collection in the opposite order from the original.ThenBy
- Used to chain sorting calls with anyOrder
,OrderBy
or otherThenBy
call. Sorts by a newKey
ascending.ThenByDescending
- Used to chain sorting calls with anyOrder
,OrderBy
or otherThenBy
call. Sorts by a newKey
descending.
JavaScript
sort
- Sorts the items by their default order or a comparison function. Mutates the original array.reverse
- Returns the array in the opposite order from the original. Mutates the original array.
Combining or Adding to Collections
C#
Append
- Adds a new item to the end of the collection.Concat
- Adds a new collection to the end of the first collection.Join
- Combines two collections, based on a definedKey
in each, and a custom function to join the two together.Prepend
- Adds a new item to the beginning of the collection.Union
- Combines two collections, excluding duplicates.UnionBy
- Combines two collections, limiting eachKey
to a single instance.
JavaScript
concat
- Adds a new array to the end of the first array. Does not alter the existing arrays.push
- Adds a new item to the end of the array, and returns the newlength
.unshift
- Adds a new item to the beginning of the array, and returns the newlength
.
Boolean Methods
These methods return a true
or false
depending on what is in the collection.
C#
All
- Returnstrue
if all items match the predicate.Any
- Returnstrue
if any item matches the predicate.Contains
- Returnstrue
if the item is in the collection.SequenceEqual
- Compares each item in two collections, and returnstrue
if they all match.
JavaScript
every
- Returnstrue
if all items match the predicate.includes
- Returnstrue
if the item is in the array.some
- Returnstrue
if any item matches the predicate.
Counting and Transforming Items
There are many transformations possible on a collection of items, especially on numeric types. These methods can be very powerful, and are in my opinion the hardest to keep straight in terms of different names. For example, Aggregate
, and reduce
seem like opposite terms, yet they are actually the same concept! Select
and map
are probably the most common transformation in each language, so it is important to know how to use them well.
C#
Aggregate
- Applies anAccumulator
function to the collection, where each item and the accumulation are passed as parameters. Also supports giving a startingseed
value.Average
- Finds the numeric average of the collection values.Count
- Returns the number of collection items as anint
.LongCount
- Returns the number of collection items as along
.Select
- Transforms each item in the collection via a custom function into a new value.SelectMany
- Transforms each item in the collection into a new collection of values, which are then flattened into a single new collection.Sum
- Adds all the values together for the collection.TryGetNonEnumeratedCount
- Attempts to count the items in the collection without actually enumerating the items. Returns a boolean to indicate success, and has anout
parameter with the count value.Zip
- Uses a custom function to combine each item in two collections together by index.
JavaScript
flatMap
- Transforms each item in the collection into a new array of values, which are then flattened into a single array.length
- Returns the number of items in the array.map
- Transforms each item in the array via a custom function into a new value.reduce
- Applies anaccumulator
function to the array, where each item and the accumulation are passed as parameters. Also supports aninitialValue
.
C#-Only Type Transformations
These methods do not have a JavaScript equivalent because JS is a loosely-typed language. Instead, you can simply treat one type like another, and if it has the correct properties and methods, it will work. In C#, not only do you need to cast to the appropriate collection type for some usages, but since many LINQ methods use deferred execution, this also forces the query to actually run and produce an in-memory collection.
AsEnumerable
- Returns the collection as anIEnumerable<T>
.Cast<T>
- Returns the collection with each item cast to theT
value in anIEnumerable<T>
.DefaultIfEmpty
- Returns the original collection, or if the collection was empty, returns a new collection with a single, default value (e.g.,null
).ToArray
- Returns a fixed-length array.ToDictionary
- Transforms a simple collection into a Key/Value pair Dictionary.ToHashSet
- Returns a collection with no duplicates, similar toDistinct
, but theHashSet
type prevents adding duplicates in the future as well.ToList
- Returns a mutable list that can be added to or removed from.ToLookup
- Groups the collection underkey
lookup values.
C#-Only Grouping Records
Chunk
- Creates a collection of arrays, of a fixed maximum size, from the original collection.GroupBy
- Creates a collection ofIGrouping
elements, each of which is a collection of items from the original collection grouped by a predicate.GroupJoin
- Creates a new collection, normally with a differentType
, that contains the joined results of two collections.
Comparison Chart
In the following chart, I aimed for a 1-1 comparison whenever possible. If there was no JavaScript Array method, I tried to find the most succinct way of achieving the same result in code.
C# LINQ IEnumerable Method |
JavaScript Array.prototype Method |
---|---|
Aggregate((acc, x) => function, seed) |
reduce((acc, x) => function, seed) |
All(x => predicate) |
every(x => predicate) |
Any(x => predicate) |
some(x => predicate) |
Append(item) |
push(item) |
AsEnumerable() |
N/A |
Average() |
reduce((acc, x) => acc + x) / array.length |
Cast<T>() |
N/A |
Chunk(size) |
no simple equivalent |
Concat(otherIEnumerable) |
concat(otherArray) |
Contains(item) |
includes(item) |
Count() |
length |
DefaultIfEmpty() |
some() ? array : undefined |
Distinct() |
[...new Set(array)] 2 |
DistinctBy(x => x.Key) |
no simple equivalent |
ElementAt(index) |
at(index) |
ElementAtOrDefault(index) |
at(index) |
Empty<T>() 1 |
[] 2 |
Except(otherIEnumerable) |
filter(x => !otherArray.includes(x)) |
ExceptBy(other, x => x.Key) |
no simple equivalent |
First(x => predicate) |
find(x => predicate) |
FirstOrDefault(x => predicate) |
find(x => predicate) |
GroupBy(x => predicate) |
no simple equivalent 3 |
GroupJoin(inner, o => o.Key, i => i.Key, func, comparer) |
no simple equivalent |
Intersect(other) |
filter(x => otherArray.includes(x)) |
IntersectBy(other, x => x.Key, comparer) |
no simple equivalent |
Join(inner, o => o.Key, i => i.Key, func) |
no simple equivalent |
Last(x => predicate) |
findLast(x => predicate) |
LastOrDefault(x => predicate) |
findLast(x => predicate) |
LongCount() |
length |
Max() |
Math.max(...array) 2 |
MaxBy(x => x.Key) |
sort((a, b) => b.Key - a.Key)[0] |
Min() |
Math.min(...array) 2 |
MinBy(x => x.Key) |
sort((a, b) => a.Key - b.Key)[0] |
OfType<T>() |
filter(x => x instanceof T) |
Order() |
sort() 4 |
OrderBy(x => x.Key) |
sort((a, b) => a.key - b.key) 4 |
OrderByDescending(x => x.Key) |
sort((a, b) => b.key - a.key) 4 |
OrderDescending() |
sort((a, b) => b - a) 4 |
Prepend(item) |
unshift(item) 4 |
Range(start, count) 1 |
[...Array(count + start).keys()].slice(start) 2 |
Repeat(item, count) 1 |
fill(item, startIndex, endIndex) 4 |
Reverse() |
reverse() 4 |
Select(x => function) |
map(x => function) |
SelectMany(x => function) |
flatMap(x => function) |
SequenceEqual(otherIEnumerable) |
no simple equivalent |
Single(x => predicate) |
filter(x => predicate); result.length > 1 ? throw error; |
SingleOrDefault(x => predicate) |
filter(x => predicate); result.length > 1 ? throw error; |
Skip(count) |
slice(count) |
SkipLast(count) |
slice(0, -count) |
SkipWhile(x => predicate) |
no simple equivalent |
Sum() |
reduce((acc, x) => acc + x) |
Take(count) |
slice(0, count) |
TakeLast(count) |
slice(-count) |
TakeWhile(x => predicate) |
no simple equivalent |
ThenBy(x => x.Key) |
no simple equivalent |
ThenByDescending(x => x.Key) |
no simple equivalent |
ToArray() |
N/A |
ToDictionary(x => function, x => function) |
reduce((acc, x, i) => acc[functionK] = functionV) |
ToHashSet() |
new Set(array) 2 |
ToList() |
N/A |
ToLookup(x => function, x => function) |
no simple equivalent |
TryGetNonEnumerated(out int count) |
no simple equivalent |
Where(x => predicate) |
filter(x => predicate) |
Union(otherIEnumerable) |
[...new Set(array.concat(otherArray))] |
UnionBy(other, x => x.Key) |
no simple equivalent |
Zip(other, (a, b) => function) |
map((a, i) => functionWithOther[i]) |
1 - static method on Enumerable
2 - not a method on Array.prototype
3 - group
method is in experimental stage
4 - mutates the original array
Conclusion
I hope this deep dive into LINQ functional methods and their JavaScript Array counterparts was useful. If you are a .NET Blazor developer or interested in Geospatial Information Systems (GIS), checkout GeoBlazor and the dymaptic blog for more content!