Introduction to LINQ in C#

Gilad Bar Ilan
Level Up Coding
Published in
8 min readMay 28, 2021

--

LINQ is one of the most useful C# libraries for code optimization, it can make your code shorter, clearer and more readable. For understanding the concepts of LINQ you should first understand what are delegates and extended functions.

What is Linq?

LINQ is a System library that adds extensions for the IEnumerable interface. In LINQ, we can find a lot of delegated functions that provide us structures of useful functions that we can fit for our needs using the delegates.

An example of a LINQ extension is the Sum Function. Let’s take the Builder class as an example.

public class Builder
{
public string builderName { get; set; }
public double builderSalary { get; set; }
public int builderAge { get; set; }
}

Without LINQ, if we had wanted to Sum all the salaries of the builders, we had to do the following:

public static double GetFullSalary(Builder[] builders_)
{
double fullSalary = 0.0d;

for(int i = 0; i < builders_.Length; i++)
{
fullSalary += builders_[i].builderSalary;
}

return fullSalary;
}

However, Linq already has an extended function called Sum that has the same structure as GetFullSalary.

The following code is my suggestion for the real C# Sum function’s implementation and it’s not necessarily the real implementation

/*
Because the following code contains some advanced concepts here is a shortcut of what it means:
Step 1 - We create an extention for IEnumerable<T>.
Step 2 - We check to see if the struct we accepted is a numeric type or not. If it's not a numeric type we check if the class\struct supports the addition operator using operator overloading, if both cases fail then we return the default value of the type (Step 2 - Everything between the region "Check_If_Its_Number").
Step 3 - After we know it's a numeric type or a class\struct that supports addition we can sum all the elements and return the result.
*/
public static class LINQExtensions
{
public static T Sum<T>(this IEnumerable<T> li, Func<T, T> func_) where T: struct //we use T:struct because numerics are //structs
{
T full = default; //gives 'full' the default value of T
#region Check_If_Its_Number
try
{
sbyte mySByte = (sbyte)((dynamic)(default(T)));
//if we can cast to sbyte then it's a number type.
}
catch
{
//if the class overloads the addition operator we can also make a sum
if(!(typeof(T).GetMethods().Any(x => x.Name == "op_Addition"))) /*Any is a LINQ function that checks in that case if there is a function called op_Addition which means that the class/struct supports the addition operator.*/
{
Console.WriteLine("The following object cannot use addition operator.");
return default(T); /*if can't cast and the class/struct doesn't support addition return the default value of T*/
}
}
#endregion
for (int i = 0; i < li.Count(); i++)
{
full += (dynamic)(func_(li.ElementAt(i)));
//sums all the elements
}
return full;
}
}

This means that instead of writing the GetFullSalary function we could simply write.

using System.Linq;
...
double fullSalary = builders_.Sum(x => x.builderSalary);

As you can see in the example above, the LINQ extension made it easier for us because we could use that without having to build the GetFullSalary function.

More LINQ Structures

Select

The select function returns us a new IEnumerable with the size of the calling IEnumerable. The function makes the delegate operation for each element in the IEnumerable.

Builder[] builders = 
{
new Builder() { builderName = "David" },
new Builder() { builderName = "John" },
new Builder() { builderName = "Mikel"}
};
string[] builderNames = builders.Select(x => x.builderName).ToArray();
//builderName = ["David", "John", "Mikel"]
/*--Possible Implementation of Select method--public static class LINQExtensions
{
public static IEnumerable<TResult> Select<T,TResult>(this IEnumerable<T> li, Func<T, TResult> func_)
{
foreach(var item in li)
{
yield return func_(item);//adds current item to returned IEnumerable
}
}
}*/

In the example above for each Builder in the array, we’ll return the builder’s name.

Where

The where function filters the elements in the IEnumerable and returns a new IEnumerable that contains only the element that passed the condition.

Builder[] builders = 
{
new Builder() { builderName = "David", builderSalary = 45.0d },
new Builder() { builderName = "Mark", builderSalary = 450_000 },
new Builder() { builderName = "Smith", builderSalary = 100_000}
};
IEnumerable<Builder> a = builders.Where(x => x.builderSalary < 100).ToArray();
// a will contain only David because he is the only one with a salary that's below 100
/*--possible implementation of Where method--
public static class LINQExtensions
{
public static IEnumerable<T> Where<T>(this IEnumerable<T> li, Func<T, bool> func_)
{
foreach (var item in li)
{
if(func_(item))
yield return item;//adds current item to the returned IEnumerable
}
}
}*/

Any & All

Any function returns true if there is an element in the IEnumerable that upholds the condition. If Any is called without parameters it will determine if the length of the IEnumerable is 0.

The All function returns true if all the elements in the IEnumerable satisfy a condition.

int[] arr = {1,2,3,4};bool biggerThan5 = arr.Any(x => x > 5); //false
bool any = arr.Any(); //true
bool all = arr.All(x => x < 5); //true

Max & Min & Avrage

The max and min functions will return the maximum/minimum value in the IEnumerable

The Average returns the average value of the elements.

int[] arr = { 1, 2, 30, 100, 5, 10 };
int max_value = arr.Max(); //100
int min_value = arr.Min(); //1
int average_ = arr.Average();
int minSalaryBuilder = builders.Min(x => x.builderSalary); //45
int maxSalaryBuilder = builders.Max(x => x.builderSalary); //450,000
double averageSalary = builders.Average(x => x.builderSalary); //183348.333333

Last & First

Returns the last/first occurrence of a condition, if there is no condition the last/first element of the IEnumerable will be returned.

int[] arr = { 1, 2, 3, 4, 5, 100, 200, 3, 50 };
int last_ = arr.Last(x => x > 50); //200
int last_element = arr.Last(); // 50
int first_ = arr.First(x => x > 50); //100
int first_element = arr.First(); //1

Intersect

The Intersect function takes an IEnumerable as a parameter and returns a new IEnumerable where only the elements that are common for both of the lists stays.

The order of the common elements in the new list will be in the same order as the calling IEnumerable (not the parameter).

int[] arr = { 1, 2, 3, 4, 6 };
int[] other_ = { 1, 2, 3, 4, 5 };
arr = arr.Intersect(other_).ToArray(); //[1,2,3,4]arr = { 1, 2, 3, 5, 4, 6 };
other_ = { 6 , 2, 3, 4, 7 };
arr = arr.Intersect(other_); //[2,3,4,6]

Prepend & Append

The Prepend function accepts a parameter and returns a new IEnumerable where the parameter at the beginning of the IEnumerable.

The Append function accepts a parameter and returns a new IEnumerable where the parameter at the end of the IEnumerable.

int[] arr = { 1, 2, 3, 4 };arr = arr.Prepend(0).ToArray(); //arr = [0,1,2,3,4]
arr = arr.Append(5).ToArray(); //arr = [0,1,2,3,4,5]

GroupBy

The GroupBy function allows us to create groups of elements that share the same identifier. For example, if we have a list of jobs we can make a group of programmers and a group of builders.

Person[] arr =
{
new Person(){ job_ = Job.Builder, Name = "Dan"},
new Person(){ job_ = Job.Builder, Name = "Frank"},
new Person(){ job_ = Job.Programmer, Name = "Dave"},
};
var group_ = arr.GroupBy(x => x.job_).ToDictionary(x => x.Key);
/*group_ = [(Job.Builder):[Person Dan, Person Frank], (Job.Programmer):[Person Dave]]*/

OrderBy

The OrderBy function sorts the IEnumerable by the delegate parameter in ascending order.

int[] myArr = {4,3,2,1};
myArr = myArr.OrderBy(x => x).ToArray(); //[1,2,3,4]
Builder[] arr =
{
new Builder() { Name = "David", Salary = 180 },
new Builder() { Name = "Frank", Salary = 120 },
new Builder() { Name = "Jake", Salary = 150 },
};
arr = arr.OrderBy(x => x.Salary).ToArray(); /*[Builder Frank, Builder Jake, Builder David]*/

OrderByDescending

The OrderByDescending function sorts the IEnumerable by the delegate parameter in descending order.

int[] myArr = {1,2,3,4};
myArr = myArr.OrderByDescending(x => x).ToArray(); //[4,3,2,1]
Builder[] arr =
{
new Builder() { Name = "David", Salary = 180 },
new Builder() { Name = "Frank", Salary = 120 },
new Builder() { Name = "Jake", Salary = 150 },
};
arr = arr.OrderByDescending(x => x.Salary).ToArray();
//[Builder David, Builder Jake, Builder Frank]

LINQ functions without delegates

Distinct

The Distinct function removes all the duplicated elements.

int[] arr = {1,2,1};
arr = arr.Distinct().ToArray(); //[1,2]

Reverse

The Reverse function reverses the positions of the elements in the IEnumerable.

int[] arr = {1,5,6,7};
int[] reversed_ = arr.Reverse().ToArray(); //[7,6,5,1]

LINQ Queries

We can use the keywords LINQ offers to make Linq queries.

Let’s have an example:

int[] arr = { 1, 2, 3, 4, 5 };var get_element = from i in arr
select i;
//----------------------------------------------------
//Equivalent to
public static IEnumerable<T> Copy<T>(IEnumerable<T> arr)
{
foreach(var item in arr)
{
yield return item;
}
}

In the example above i is the item in the foreach loop, arr is the IEnumerable which we make the foreach loop on and we select the item on each iteration.

As a result, the example above will just copy the elements of arr into get_element.

The ‘where’ keyword can help us to filter the elements and put in the new IEnumerable only the elements that stand with the condition.

int[] arr = { 1, 2, 3, 4, 5 };var new_ = from i in arr
where i < 3
select i; //[1,2]

The ‘let’ keyword allows us to create an extra variable in the query.

int[] arr = {1, 2, 3, 4, 5};var new_ = from i in arr
where i < 3
let num_ = i + 1
select num_; //[2,3]

The ‘orderby’ keyword allows us to order the elements in the IEnumerable by ascending\descending order.

int[] arr = {1, 2, 3, 10, 5};var new_ = from i in arr
orderby i descending
select i; //[10,5,3,2,1]
new_ = from i in arr
orderby i ascending
select i + 1;//[2,3,4,6,11]
new_ = from i in arr
where i < 3
orderby i descending
select i + 1;//[3,2]

The ‘group & by’ keywords allows us to group IEnumerable by a key.

`Person[] myPerson =
{
new Person(){ job_ = Job.Builder, Name = "Mike" },
new Person(){ job_ = Job.Builder, Name = "Jack"},
new Person(){ job_ = Job.Teacher, Name = "David"},
new Person(){ job_ = Job.Programmer, Name = "Ben"}
};
var groups_ = (from i in myPerson
group i by i.job_).ToDictionary(x => x.Key);
/*Now we have a group of Builders, Programmers and teachers. [(Job.Builder):[Person Mike, Person Jack], (Job.Teacher):[Person David],(Job.Programmer):[Person Ben]]*/

We can use the ‘into’ keyword to save the value of the group to a variable in the query.

var groups_ = (from i in myPerson
group i by i.job_ into myProj //myProj stores group
where myProj.Count() == 1
select myProj).ToDictionary(x => x.Key);
//[(Job.Teacher):[Person David],(Job.Programmer):[Person Ben]]

Congrats! You’ve finished the introduction to LINQ! :)

--

--