Why LINQ Exists
By the end of this lesson
Replace hand-written loops with a declarative description of the result you want.
You have written loops that walk a list, test each item, keep the ones that matter, and add up a total. They work. They are also mostly bookkeeping: a counter here, a dictionary there, an if that continues early. The rule you actually care about is one line buried in the middle.
LINQ is a set of methods that let you state the result instead of the mechanics. The name stands for Language Integrated Query, and the word query is the useful part: you describe what you want from a collection, and the method works out the stepping and the accumulating.
This lesson puts the two side by side on one dataset. That dataset is used by every lesson in this module, so it is worth a minute now.
public record Employee(
string Name,
string Department,
string Role,
decimal AnnualSalary,
DateOnly JoiningDate);
public record Department(
string Name,
string Location,
string CostCentre);
List<Employee> employees = new List<Employee>
{
new Employee("Asha Mehta", "Engineering", "Senior Engineer", 1_800_000m, new DateOnly(2019, 4, 1)),
new Employee("Ravi Iyer", "Engineering", "Engineer", 1_200_000m, new DateOnly(2021, 7, 12)),
new Employee("Neha Kulkarni", "Engineering", "Engineering Manager", 2_400_000m, new DateOnly(2017, 1, 9)),
new Employee("Imran Sheikh", "Support", "Support Engineer", 720_000m, new DateOnly(2022, 2, 14)),
new Employee("Divya Rao", "Support", "Support Lead", 960_000m, new DateOnly(2020, 11, 2)),
new Employee("Tarun Bansal", "Sales", "Account Manager", 1_100_000m, new DateOnly(2018, 6, 18)),
new Employee("Meera Nair", "Sales", "Sales Director", 2_700_000m, new DateOnly(2016, 3, 21)),
new Employee("Sanjay Gupta", "Finance", "Financial Analyst", 950_000m, new DateOnly(2023, 9, 4)),
};
List<Department> departments = new List<Department>
{
new Department("Engineering", "Pune", "CC-100"),
new Department("Support", "Nagpur", "CC-200"),
new Department("Sales", "Mumbai", "CC-300"),
new Department("Legal", "Mumbai", "CC-400"),
};- Employee and Department are records, so each one is a short declaration of data with read-only properties. A class with the same properties would behave identically for everything in this module.
- Employee.Department holds a department name as text. The Department record holds facts about a department. The joining lesson connects the two on that name.
- There are four departments in the employees list and four in the departments list, and they are not the same four. Nobody works in Legal, and Sanjay Gupta works in Finance, which has no Department record. Those two gaps are deliberate, and the joining lesson uses them.
- Salaries are decimal, which is the right type for money because it does not carry the small rounding error that double does. JoiningDate is DateOnly, a date with no time of day.
- Copy this into a console project once and keep it. Every example that follows starts from these two lists.
Dictionary<string, decimal> totalsByDepartment = new Dictionary<string, decimal>();
foreach (Employee employee in employees)
{
if (employee.AnnualSalary <= 1_000_000m)
{
continue;
}
if (!totalsByDepartment.ContainsKey(employee.Department))
{
totalsByDepartment[employee.Department] = 0m;
}
totalsByDepartment[employee.Department] += employee.AnnualSalary;
}
foreach (KeyValuePair<string, decimal> pair in totalsByDepartment)
{
Console.WriteLine($"{pair.Key}: {pair.Value}");
}
// Engineering: 5400000
// Sales: 3800000- Most of that block is machinery. Three lines state the requirement: the salary test, the grouping key, and the addition.
- The dictionary exists only because grouping needs somewhere to put partial results. It is scaffolding, not part of the problem.
- The ContainsKey check is there to avoid a KeyNotFoundException on the first employee of each department. Forgetting it is a routine bug in code shaped like this.
- Support and Finance are absent from the output because nobody in them earns above 1,000,000. That is correct, and it is easy to miss when you read the loop.
- One more wrinkle: a Dictionary does not promise an enumeration order. The output above is what you will almost certainly see, but the code never asked for an order, so it has no right to expect one.
using System.Linq;
IEnumerable<IGrouping<string, Employee>> byDepartment = employees
.Where(e => e.AnnualSalary > 1_000_000m)
.GroupBy(e => e.Department);
foreach (IGrouping<string, Employee> group in byDepartment)
{
Console.WriteLine($"{group.Key}: {group.Sum(e => e.AnnualSalary)}");
}
// Engineering: 5400000
// Sales: 3800000- Where keeps the items that satisfy a condition. GroupBy gathers the survivors by a key. Sum adds a chosen number across a set of items. Three verbs, three lines, and they read in the order the requirement was written.
- Each method takes a lambda — the e => ... part — which is the rule applied to one item. The e is a parameter name; call it employee if you prefer.
- Where does not alter employees and GroupBy does not alter anything either. Each method returns a new sequence, which is why the list is still intact afterwards.
- A sequence here means anything you can step through item by item: that is what IEnumerable<T> describes, and a List<T> is one. IGrouping<string, Employee> is a sequence of employees that also carries a Key, the department name they share. The grouping lesson goes into it properly.
- These methods live in the System.Linq namespace. New console projects switch on implicit usings, so it is usually already available; older files need the using line shown at the top.
// Method syntax — chained method calls. This course uses it throughout.
IEnumerable<string> namesA = employees
.Where(e => e.Department == "Engineering")
.OrderBy(e => e.Name)
.Select(e => e.Name);
// Query syntax — keywords built into the language. Same query, same result.
IEnumerable<string> namesB =
from e in employees
where e.Department == "Engineering"
orderby e.Name
select e.Name;
foreach (string name in namesA)
{
Console.WriteLine(name);
}
// Asha Mehta
// Neha Kulkarni
// Ravi Iyer- The compiler rewrites query syntax into method calls, so namesA and namesB are the same query expressed two ways. Neither is faster.
- This course uses method syntax for two reasons: it composes without a wrapper when you add an operator like Count or ToList to the end, and it is what you will meet most often in .NET codebases.
- Query syntax is genuinely clearer for a few shapes — joins that also group, and queries that need an intermediate value through the let keyword. It is worth recognising when you see it.
- Mixing the two in one file is allowed and common. Consistency within a file matters more than which one you pick.
Summary
- LINQ is a set of methods over sequences that let you state the result instead of the steps
- A loop mixes the rule you care about with bookkeeping; a query keeps the rule visible
- Operators return new sequences and never modify the source collection
- Method syntax and query syntax compile to the same thing; this course uses method syntax
- The gain is readability, not speed, and the cost is that the mechanics are hidden
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Write the names of everyone who joined before 2020 and earns more than 1,000,000, oldest joiner first.
Write it as a loop first, then as a LINQ query. Keep both and read them next to each other.
Show solution
The query names the three requirements in the order the sentence stated them: the filter, the ordering, the field you want. The loop version needs a second list to hold the survivors and a sort step that has to be applied to that list rather than to the source, which is exactly the kind of detail that gets it wrong the first time.
The loop is not wrong. On a list of eight it is not slower either. The reason to prefer the query is that the next requirement — also exclude contractors, also cap at five names — is a line in the chain instead of a change to the plumbing.
IEnumerable<string> names = employees
.Where(e => e.JoiningDate < new DateOnly(2020, 1, 1))
.Where(e => e.AnnualSalary > 1_000_000m)
.OrderBy(e => e.JoiningDate)
.Select(e => e.Name);
foreach (string name in names)
{
Console.WriteLine(name);
}
// Meera Nair
// Neha Kulkarni
// Tarun Bansal
// Asha MehtaThink about it
Think about it
LINQ does not replace every loop. Name two situations where a foreach is the better choice, and say what it is about LINQ that makes it a poor fit there.
Show solution
Doing something to each item, rather than producing a value from them. Sending an email, writing a row, logging a warning: those are side effects, and LINQ operators are built to return results. A query used for its side effects also will not run until enumerated, which makes the timing of the effect unpredictable.
Building several results in one pass. If you need the total, the highest paid person and a count of exceptions from one walk of a large list, three separate LINQ calls read the list three times. A loop reads it once. On eight employees this does not matter; on two million rows it does.
A third answer is equally defensible: when the logic inside the loop is long enough that a lambda would hide it. A query is easier to read only while each step stays small.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.