Language Integrated Query or LINQ is a C# feature that allows you to query different data sources, such as:
- In memory collections (arrays, lists, dictionaries)
- SQL collections
- XML collections
using a unified language syntax.
There are two ways to use LINQ:
- Query syntax consist of a set of query keywords defined into the .NET Framework that resemble SQL-Like commands.
- Method syntax consist of operator functions (extension methods) chained together using a declarative programming paradigm.
For this tutorial we'll use LINQ Extension method syntax.
Where can we use LINQ?
Language Integrated Query syntax works with any type of collection in C# that implements the IEnumerable
interface, such as arrays, lists, dictionaries, etc. Simply put, whenever you see a type IEnumerable
or type that implements the IEnumerable
, you can use LINQ.
Setup
To speed things up, I've already created a collection of students in the JSON file. The Gist I wrote will explain how to import the JSON data into a C# collection.
Each student is a class containing properties:
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Country { get; set; }
}
var student = new LINQTutorial();
List<Student> students = student.GetStudents();
The method GetStudents()
on the student
instance retrieves a collection of students populated using the JSON file. More in the Gist.
Now let's make use of various LINQ Operators to query the students data.
FILTERS
First & Last
The First()
operator returns the first record in the collection:
var firstStudent = students.First();
Console.WriteLine($"Name: {firstStudent.Name}, Age: {firstStudent.Age}, Country: {firstStudent.Country}");
// Name: Mirza, Age: 18, Country: Bosnia
The opposite of First()
is the Last()
operator that returns the last record:
var lastStudent = students.Last();
Console.WriteLine($"Name: {lastStudent.Name}, Age: {lastStudent.Age}, Country: {lastStudent.Country}");
// Name: Amy, Age: 21, Country: USA
This is one way of using First()
and Last()
operators. The LINQ operators have an overload method that accepts a predicate and return the record/s that match the condition inside.
var firstStudent = students.First(x => x.Age == 19);
Console.WriteLine($"Name: {firstStudent.Name}, Age: {firstStudent.Age}, Country: {firstStudent.Country}");
// Name: Alan, Age: 19, Country: UK
var lastStudent = students.Last(x => x.Age == 20);
Console.WriteLine($"Name: {lastStudent.Name}, Age: {lastStudent.Age}, Country: {lastStudent.Country}");
// Name: Raj, Age: 20, Country: India
First/Last Or Default
What would happen if we'd query for first student with the age of 100?
var firstStudent = students.First(x => x.Age == 100);
Because there is no record of a student with age 100, the C# program execution will stop and throw an exception:
System.InvalidOperationException: Sequence contains no matching element
To prevent this we can use the FirstOrDefault()
operator that works just the same as the First()
, with an exception that it returns the default value (null for strings and objects, 0 for numbers) if no record meets the condition.
var firstStudent = students.FirstOrDefault(x => x.Age == 19);
// {...}
var nonExistingStudent = students.FirstOrDefault(x => x.Age == 100);
// null
var nonExistingStudentName = students
.Select(s => s.Name)
.FirstOrDefault(name => name == "Homer");
// null
var nonExistingStudentAge = students
.Select(s => s.Age)
.FirstOrDefault(age => age == 100);
// 0
The same applies for the Last()
and LastOrDefault()
operators.
ElementAt
This method is used to retrieve an element at a specific index.
var fifthStudent = students.ElementAt(4);
Console.WriteLine($"Name: {fifthStudent.Name}, Age: {fifthStudent.Age}");
// Name: Farook, Age: 18
That said, if you use an index that is not present, the exception will be thrown:
var nonExistingStudent = students.ElementAt(100);
System.ArgumentOutOfRangeException: Index was out of range.
So it's safer to use ElementAtOrDefault
:
var nonExistingStudent = students.ElementAtOrDefault(100); // null
Where
The Where()
operator is used to filter multiple elements. The output is an array of all elements that meet a specified criteria.
var studentsFromBosnia = students.Where(x => x.Country == "Bosnia");
foreach (var student in studentsFromBosnia)
{
Console.WriteLine($"Name: {student.Name}, Age: {student.Age}");
}
// Name: Mirza, Age: 18
// Name: Armin, Age: 20
One thing to note about studentsFromBosnia
is that is not of type List<Student>
, but rather IEnumerable<Student>
. The LINQ works in either way.
However, if you need to pass this collection to a method that takes an array or a list as a parameter, you can cast an IEnumerable
to derived types.
BUILDERS
To convert an IEnumerable
to an array simply do:
var studentsArray = studentsFromBosnia.ToArray();
// Array<Student>
To convert an IEnumerable
to a list simply do:
var studentsList = studentsFromBosnia.ToList();
// List<Student>
To convert an IEnumerable
collection to a dictionary, first restructure the data into an anonymous key-value pair object and then covert the object to dictionary.
var studentsDictionary = studentsFromBosnia
.Select((stud, index) => new { index, stud }) // creating an anonymous object
.ToDictionary(x => x.index, x => x.stud); // converting object to dictionary
// Dictionary<int, Student>
PROJECTION
Select
The Select
operator is used to retrieve a list of specific properties from an IEnumerable, in this case List. For example, let's say we want to pick names of all students:
IEnumerable<string> names = students.Select(s => s.Name);
foreach (var name in names)
{
Console.WriteLine(name);
// Mirza, Armin, Alan, Seid...
}
Or a combination of properties (Name & Country):
var customList = students.Select(s => new { Name = s.Name, Country = s.Country });
foreach (var student in customList)
{
Console.WriteLine($"Name: {student.Name}, Country: {student.Country}");
}
SORTING
OrderBy
The OrderBy
operator is used to order elements in the collections. For example, if we'd take ages of all students and put them into a collection:
var ages = students.Select(s => s.Age);
The outcome would be a collection sorted in the order the students were created. Applying the OrderBy
, the ages are shown from smallest to largest:
var agesInOrder = students.OrderBy(s => s.Age).Select(s => s.Age);
// [18, 18, 18, 19, 19, 20, 20, 21, 22, 24]
It's clear that we have duplicate elements. We can fix that using the Distinct()
operator:
var agesInOrder = students.OrderBy(s => s.Age).Select(s => s.Age).Distint();
// [18, 19,20, 21, 22, 24]
The OrderBy
operator can also be applied after the Select
:
var agesInOrder = students
.Select(s => s.Age)
.OrderBy(s => s) // sort by values in the collection (ages)
.Distinct();
// [18, 19,20, 21, 22, 24]
In this instance, the ordering by ages is still hapening, but because we've already filtered the ages in the previous line, the OrderBy
has less work to do.
OrderByDescending
The OrderbyDescending
is doing the same but in the reversed order:
var agesInReverseOrder = students
.Select(s => s.Age)
.OrderByDescending(s => s)
.Distinct();
// [24, 22, 21, 20, 19, 18]
ThenBy, ThenByDescending
The ThenBy
operator is used to apply the additional sorting after the OrderBy
.
var orderedByAgeAndCountry = students
.OrderBy(s => s.Age)
.ThenBy(s => s.Country)
.Select(s => s.Name);
// ["Mirza", "Eddy", "Farook", "Abdurahman", "Alan"...]
var orderedByAgeAndCountry2 = students
.OrderBy(s => s.Age)
.ThenByDescending(s => s.Country)
.Select(s => s.Name);
// ["Farook", "Eddy", "Mirza", ...]
Next Chapters:
In the coming parts we'll dive deeper into LINQ.
- Part 2 - LINQ Set Operations, Quantifiers & Aggregation
- Part 3 - Generators, Partitions, Equality & Joins
Don't forget to hit the follow button. Also, follow me on Twitter to stay up to date with my upcoming content.
Bye for now 👋
Top comments (0)