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, empty- IEnumerableinstance.
- 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 given- Counttimes.
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- Like- ElementAtbut returns- default(- nullfor nullable types) if the index is out of range.
- First- Finds the first item that matches the predicate. Throws on no match.
- FirstOrDefault- Like- Firstbut returns- defaultif no match is found.
- Last- Finds the last item that matches the predicate. Throws on no match.
- LastOrDefault- Like- Lastbut returns- defaultif no match is found.
- Max- Returns the item with the highest numeric value.
- MaxBy- Returns the item with the highest- Keyvalue according to the given- IComparer.
- Min- Returns the item with the lowest numeric value.
- MinBy- Returns the item with the lowest- Keyvalue according to the given- IComparer.
- Single- Like- First, but also throws if multiple matches are found.
- SingleOrDefaultLike- FirstOrDefaultbut 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 duplicate- Keyvalues.
- 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 matching- Keyvalue 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 matching- Keyvalues in both.
- OfType<T>- Filters the collection to only records of a particular- Type.
- 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 is- false, 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 is- false.
- 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 a- Keyvalue in ascending order.
- OrderByDescending- Sorts the items by a- Keyvalue 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 any- Order,- OrderByor other- ThenBycall. Sorts by a new- Keyascending.
- ThenByDescending- Used to chain sorting calls with any- Order,- OrderByor other- ThenBycall. Sorts by a new- Keydescending.
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 defined- Keyin 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 each- Keyto 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 new- length.
- unshift- Adds a new item to the beginning of the array, and returns the new- length.
Boolean Methods
These methods return a true or false depending on what is in the collection.
C#
- All- Returns- trueif all items match the predicate.
- Any- Returns- trueif any item matches the predicate.
- Contains- Returns- trueif the item is in the collection.
- SequenceEqual- Compares each item in two collections, and returns- trueif they all match.
JavaScript
- every- Returns- trueif all items match the predicate.
- includes- Returns- trueif the item is in the array.
- some- Returns- trueif 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 an- Accumulatorfunction to the collection, where each item and the accumulation are passed as parameters. Also supports giving a starting- seedvalue.
- Average- Finds the numeric average of the collection values.
- Count- Returns the number of collection items as an- int.
- LongCount- Returns the number of collection items as a- long.
- 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 an- outparameter 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 an- accumulatorfunction to the array, where each item and the accumulation are passed as parameters. Also supports an- initialValue.
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 an- IEnumerable<T>.
- Cast<T>- Returns the collection with each item cast to the- Tvalue in an- IEnumerable<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 to- Distinct, but the- HashSettype 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 under- keylookup values.
C#-Only Grouping Records
- Chunk- Creates a collection of arrays, of a fixed maximum size, from the original collection.
- GroupBy- Creates a collection of- IGroupingelements, each of which is a collection of items from the original collection grouped by a predicate.
- GroupJoin- Creates a new collection, normally with a different- Type, 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 IEnumerableMethod | JavaScript Array.prototypeMethod | 
|---|---|
| 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!

 
        
       
        
      