How to Use LINQ in C# – Practical Examples for .NET Developers
Part 1 – LINQ Fundamentals
When working with collections in C#, it’s very common to use foreach loops to filter or transform data. This approach works well, but as the logic grows, the code often becomes more verbose and harder to read. The intent of the operation is mixed with the mechanics of iteration.
LINQ, which stands for Language Integrated Query, was introduced to make data manipulation more expressive and readable. Instead of describing step by step how to iterate over a collection, you describe what you want to retrieve.
For example, imagine you need to select only adult students from a list. Using a traditional loop, you might write something like this:
var adults = new List<Student>();
foreach (var student in students)
{
if (student.Age >= 18)
{
adults.Add(student);
}
}
The logic is correct, but the intention is slightly hidden inside the loop.
With LINQ, the same operation becomes more direct:
var adults = students
.Where(s => s.Age >= 18)
.ToList();
Now the code clearly communicates what it is doing: filter students by age and return the result as a list.
This is the core idea behind LINQ. It promotes a more declarative style of programming, where the focus shifts from “how do I iterate?” to “what data do I need?”. This change improves readability, reduces boilerplate code, and makes complex data operations easier to maintain.
In this article, we’ll explore the fundamentals of LINQ using in-memory collections only, so you can follow along in a simple console application. We’ll cover the most important operators and learn how to write clean, readable queries that you can use immediately in real-world .NET projects.
What Is LINQ and Why Was It Created?
LINQ stands for Language Integrated Query. It is a feature introduced in C# 3.0 and .NET Framework 3.5 that brings query capabilities directly into the C# language.
Before LINQ, working with data meant using different technologies depending on the data source. If you were querying a database, you wrote SQL. If you were working with XML, you used a specific XML API. If you were filtering in-memory collections, you wrote loops manually.
Each data source had its own syntax and its own way of querying data.
This created a fragmentation problem. Developers had to switch mental models depending on where the data came from.
LINQ was designed to unify this experience.
The idea was simple but powerful:
bring querying capabilities into the language itself, so that the same syntax can be used to query different types of data.
With LINQ, you can query:
- In-memory collections (
IEnumerable<T>) - Databases (through Entity Framework and
IQueryable<T>) - XML documents
- Other data providers that implement the LINQ pattern
The key innovation behind LINQ is that it integrates query expressions directly into C#. Instead of writing external query languages like SQL separately from your code, you write strongly-typed queries that are compiled and checked at compile time.
Under the hood, LINQ is built on top of:
- Extension methods
- Lambda expressions
- Delegates
- Generics
- Expression trees (for providers like Entity Framework)
When you write:
var adults = students.Where(s => s.Age >= 18);
You are actually calling an extension method defined in the System.Linq namespace. LINQ is not magic; it is a set of well-designed abstractions built into the language and the .NET runtime.
This design allows LINQ to work seamlessly across different data sources while keeping your code consistent and type-safe.
In the next section, we’ll look at how LINQ actually works with collections and why IEnumerable<T> plays such a central role.
How LINQ Works with IEnumerable
o really understand LINQ, you need to understand one central concept: IEnumerable<T>.
Most of the time, when you use LINQ in everyday C# development, you are working with collections that implement IEnumerable<T>, such as:
- List<T>
- Array
- HashSet<T>
- Dictionary<TKey, TValue> (when iterating over it)
IEnumerable<T> represents a sequence of elements that can be iterated over. In simple terms, it means: this collection can be enumerated.
When you write:
var adults = students.Where(s => s.Age >= 18);
You are not calling a method defined on List<T> itself.
You are calling an extension method defined in the System.Linq namespace.
This is a key point.
LINQ works by adding extension methods to any type that implements IEnumerable<T>. That’s why you need:
using System.Linq;
Under the hood, Where is just a static method that takes an IEnumerable<T> and returns another IEnumerable<T>.
Conceptually, it looks like this:
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source,
Func<T, bool> predicate)
There is no magic. LINQ builds new sequences based on the original one.
Another very important concept is that most LINQ operations use deferred execution.
This means that the query is not executed immediately.
For example:
var adults = students.Where(s => s.Age >= 18);
At this point, nothing has actually been evaluated yet. The filtering logic will run only when you iterate over adults, for example:
foreach (var student in adults)
{
Console.WriteLine(student.Name);
}
Or when you call a method like:
.ToList()
.Count()
.First()
These methods force execution and produce a concrete result.
This behavior has two important implications:
- LINQ queries are efficient because they don’t compute results until necessary.
- If the original collection changes before execution, the result may also change.
Understanding IEnumerable<T> and deferred execution is essential before moving on to more advanced operators. Without this mental model, LINQ can sometimes behave in ways that surprise beginners.
In the next section, we’ll compare method syntax and query syntax, and see why method syntax is more common in modern .NET applications.
Method Syntax vs Query Syntax
LINQ provides two different syntaxes to write queries:
- Method syntax
- Query syntax
Both achieve the same result. The difference is mainly stylistic.
Method Syntax
This is the most common form in modern .NET applications. It uses extension methods and lambda expressions.
Example:
var adults = students
.Where(s => s.Age >= 18)
.OrderBy(s => s.Name)
.Select(s => s.Name)
.ToList();
This style chains method calls together. It is concise, readable, and integrates naturally with lambda expressions.
Most real-world .NET codebases use this approach.
Query Syntax
Query syntax looks closer to SQL. It uses keywords like from, where, and select.
The same example written using query syntax:
var adults =
(from s in students
where s.Age >= 18
orderby s.Name
select s.Name)
.ToList();
This style can feel more familiar if you have a SQL background.
However, it is important to understand something crucial: Query syntax is translated by the compiler into method syntax.
In other words, LINQ always runs using the method-based operators under the hood.
Which One Should You Use?
In modern C# development, method syntax is generally preferred.
Reasons:
- It is more flexible.
- It supports all LINQ operators.
- It integrates better with lambda expressions.
- It is the dominant style in real-world projects.
Query syntax is sometimes useful for complex joins because it can look more readable in those scenarios. But for everyday filtering, projection, and sorting, method syntax is cleaner and more consistent.
If you are just getting started with LINQ, I recommend focusing on method syntax. It will prepare you better for working with Entity Framework and advanced LINQ scenarios later.
Core LINQ Operators
Now that we understand how LINQ works, let’s look at the core operators you’ll use most often in real-world .NET applications.
These operators allow you to filter, transform, sort, and retrieve data from collections. They are the foundation of LINQ and can be combined together to create clear and expressive queries.
In the following sections, we’ll go through the essential operators you need to master when working with LINQ in C#.
To keep the examples consistent and easy to follow, we’ll use a simple Student model throughout the article. All LINQ operations will be applied to an in-memory list, so you can run everything in a basic console application without setting up a database.
Here is the model:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
public double Grade { get; set; }
}
And here is the sample dataset we’ll use in the next sections:
var students = new List<Student>
{
new Student { Name = "Alice", Age = 20, Grade = 28 },
new Student { Name = "Bob", Age = 17, Grade = 24 },
new Student { Name = "Charlie", Age = 22, Grade = 30 },
new Student { Name = "David", Age = 20, Grade = 18 }
};
With this small dataset, we can demonstrate filtering, projection, sorting, and element retrieval in a clear and practical way.
Filtering Data with Where
Filtering is one of the most common operations when working with collections. The Where operator allows you to return only the elements that satisfy a specific condition. It does not modify the original collection; instead, it produces a new filtered sequence.
- Where(predicate) – Returns all elements that match the given condition.
var adults = students
.Where(s => s.Age >= 18)
.ToList();
In this example, only students aged 18 or older are included in the result.
Transforming Data with Select
Projection means transforming each element of a collection into something else. The Select operator allows you to reshape data, extract specific properties, or create new objects based on the original elements.
- Select(selector) – Transforms each element into a new form.
var names = students
.Select(s => s.Name)
.ToList();
You can also project into anonymous types:
var studentInfo = students
.Select(s => new { s.Name, s.Grade })
.ToList();
Select does not filter data. It changes its shape.
Sorting Data with OrderBy and OrderByDescending
Sorting allows you to order elements based on a key. LINQ provides ascending and descending sorting operators.
- OrderBy(keySelector) – Sorts elements in ascending order.
- OrderByDescending(keySelector) – Sorts elements in descending order.
var orderedByAge = students
.OrderBy(s => s.Age)
.ToList();
var topGrades = students
.OrderByDescending(s => s.Grade)
.ToList();
These methods return a new ordered sequence without changing the original list.
Retrieving Single Elements
Sometimes you don’t need a collection; you need a single element. LINQ provides methods that return exactly one item, depending on the condition.
First(predicate) – Returns the first element that matches the condition. Throws an exception if no element matches.
var firstAdult = students
.First(s => s.Age >= 18);
FirstOrDefault(predicate) – Returns the first matching element, or null (for reference types) if none is found.
var maybeStudent = students
.FirstOrDefault(s => s.Name == "Emma");
Single(predicate) – Expects exactly one matching element. Throws an exception if zero or more than one element matches.
var uniqueStudent = students
.Single(s => s.Name == "Alice");
SingleOrDefault(predicate) – Returns the single matching element, or null if none is found. Throws if more than one match exists.
These methods are useful when the logic of your application expects a specific number of results.
Boolean Checks with Any and All
Sometimes you only need to know whether a condition is true for some or all elements.
Any(predicate) – Returns true if at least one element matches the condition.
var hasMinors = students
.Any(s => s.Age < 18);
All(predicate) – Returns true if all elements satisfy the condition.
var allAdults = students
.All(s => s.Age >= 18);
These methods are efficient and often replace manual loops with flags.
Immediate Execution Methods
Most LINQ operators use deferred execution. However, some methods immediately execute the query and produce a concrete result.
- ToList(), Executes the query and returns a List<T>.
- ToArray(), Executes the query and returns an array.
- Count(), Executes the query and returns the number of elements.
var adultCount = students
.Where(s => s.Age >= 18)
.Count();
Calling one of these methods forces the evaluation of the query.
Practical Example: Building a Simple Student Report with LINQ
Disclaimer: This example is purely for educational purposes. There are better ways to write code and applications that can optimize this example. Use this as a starting point for learning, but always strive to follow best practices and improve your implementation.
A common scenario in backend or utility tools is generating a quick report from in-memory data. Instead of writing multiple loops and temporary collections, we can compose the logic using LINQ.
Prerequisites
Before starting, make sure you have the following installed:
- .NET SDK: Download and install the .NET SDK if you haven’t already.
- Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
- C# Dev Kit for VSCode: Install the C# Dev Kit for VSCode to enable C# support.
Step 1 – Create a Console App
Open your terminal and run:
dotnet new console -n LinqStudentReport
cd LinqStudentReport

This will generate a minimal console project.
Step 2 – Open the Project in VS Code
From the same terminal, run:
code .
This opens the project folder in Visual Studio Code.
Step 3 – Implement the Data Processing Logic
Replace the content of Program.cs with the following:

var students = new List<Student>
{
new Student { Name = "Alice", Age = 20, Grade = 28},
new Student { Name = "Bob", Age = 17, Grade = 24},
new Student { Name = "Otto", Age = 22, Grade = 30},
new Student { Name = "David", Age = 20, Grade = 18},
};
Console.WriteLine("=== Student Data Processing Utility ===");
Console.WriteLine();
// Validate data (Any)
var hasMinors = students.Any(s => s.Age < 18);
Console.WriteLine($"Contains minors: {hasMinors}");
// Filter adults students (Where)
var adults = students
.Where(s => s.Age >= 18)
.ToList();
// Sort by grade descending (OrderByDescending)
var orderedAdults = adults
.OrderByDescending(s => s.Grade)
.ToList();
// Extract names (Select)
var adultNames = orderedAdults
.Select(s => s.Name)
.ToList();
Console.WriteLine();
Console.WriteLine("Adult students ordered by grade:");
foreach (var name in adultNames)
{
Console.WriteLine($"- {name}");
}
// Retrieve the top student safely (FirstOrDefault)
var topStudent = orderedAdults.FirstOrDefault();
Console.WriteLine();
Console.WriteLine($"Top student: {topStudent?.Name}");
// Count high performers (Count)
var highPerformers = students.Count(s => s.Grade >= 25);
Console.WriteLine($"Students with grade >= 25: {highPerformers}");
Console.WriteLine();
Console.WriteLine("=== End of Processing ===");
public class Student
{
public string Name {get; set; } = "";
public int Age {get; set; }
public double Grade {get; set; }
}

Step 4 – Run the Application
Open your terminal inside the project folder and run:
dotnet run
If everything is configured correctly, the console application will execute and print the generated report.
Expected Output
You should see something similar to this:
=== Student Data Processing Utility ===
Contains minors: True
Adult students ordered by grade:
- Otto
- Alice
- David
Top student: Otto
Students with grade >= 25: 2
=== End of Processing ===
What Happened?
Let’s briefly interpret the result:
- Contains minors: True
Because Bob is 17. - Adult students are filtered (Age >= 18) and ordered by grade descending:
- Otto (30)
- Alice (28)
- David (18)
- The top student is Otto because he has the highest grade among adults.
- There are 2 students with grade >= 25 (Alice and Charlie).
This confirms that our LINQ pipeline works exactly as intended.

Conclusion
LINQ fundamentally changes how you work with data in C#. Instead of writing manual loops and managing temporary collections, you can express your intent directly in the code. This makes data manipulation more readable, more maintainable, and easier to reason about.
One of the biggest shifts LINQ introduces is moving from how to iterate to what you want to retrieve. By focusing on the result instead of the mechanics, your code becomes clearer and closer to the problem you are solving.
The core operators covered in this article are enough for the majority of everyday scenarios. Filtering, transforming, sorting, and retrieving elements will cover a large percentage of real-world use cases in backend services, APIs, and utility tools.
In the next part, we’ll explore more advanced operators such as grouping and joins, and see how LINQ works beyond in-memory collections
If you think your friends or network would find this article useful, please consider sharing it with them. Your support is greatly appreciated.
Thanks for reading!
Discover CodeSwissKnife Bar, your all-in-one, offline Developer Tools from Your Menu Bar
