LINQ is the default way to filter, shape, and group data in C#. These entries cover the decisions behind a query that does what it says: when it runs, where it runs, and how much it costs.
The source is read when the query runs, not when it is written
A query holds the steps, not the rows. It runs when it is enumerated, so it sees the source as it is at that point, and removing elements while the query reads them throws because the query is reading the live collection.
Preferred
foreach (var order in orders.Where(o => o.IsOpen).ToList())
{
orders.Remove(order);
}Avoid
foreach (var order in orders.Where(o => o.IsOpen))
{
orders.Remove(order);
}Materialise the query when the value must not change
The same expression can produce a query or a result, and only one of them stops changing. ToList or ToArray runs the pipeline once and stores the values, so materialise when the sequence is read more than once or when a later change to the source must not be visible; keep the query when the caller wants the latest data.
Preferred
var open = orders.Where(o => o.IsOpen).ToList();
orders.CloseAll();
Report(open);Avoid
var open = orders.Where(o => o.IsOpen);
orders.CloseAll();
Report(open);Corpus: Query expression basics (LINQ) · Introduction to LINQ Queries
Know which side of the wire the query runs on
Operators on IEnumerable<T> run in your process. Operators on IQueryable<T> build an expression tree that the provider translates, so Where on a DbSet becomes SQL. AsEnumerable or ToList ends the translation, and everything after it runs in memory.
Preferred
var londoners = db.Customers.Where(c => c.City == "London").ToList();Avoid
var londoners = db.Customers.ToList().Where(c => c.City == "London");Corpus: Standard query operators overview · How to Build LINQ Queries based on run-time state · Language Integrated Query (LINQ)
Pass the predicate instead of filtering firstIDE0120
Any, Count, First, and Single take a predicate themselves, so a Where in front of one builds a sequence that is discarded. Any also asks the cheaper question: presence needs one element, while Count() walks the whole source.
Preferred
bool hasMatch = words.Any(w => w == "hello");Avoid
bool hasMatch = words.Where(w => w == "hello").Count() > 0;Corpus: IDE0120: Simplify LINQ expression · CA1827: Do not use Count/LongCount when Any can be used
Use Single when a duplicate would be a bug
Single asserts that the source has exactly one match and fails when the data disagrees. First returns the first match and quietly ignores the rest. Use FirstOrDefault when a match may be absent, and give it the fallback through the overload that takes the default value.
Preferred
var id = users.Single(u => u.Email == email).Id;Avoid
var id = users.First(u => u.Email == email).Id;Filter before you project
Where drops elements and Select transforms them. With Where first the transform runs only for the elements that survive, while projecting everything so that a later step can filter it pays the transform cost for every element and then discards most of the results.
Preferred
var names = students.Where(s => s.Score > 90).Select(s => s.Name);Avoid
var names = students.Select(s => (s.Name, s.Score))
.Where(t => t.Score > 90)
.Select(t => t.Name);Corpus: Filtering Data with LINQ · Projection operations in LINQ
Use clauses for shape, method calls for the rest
Query syntax and method syntax compile to the same calls. Clauses pay off when a query filters, orders, joins, and shapes in one expression; operators with no clause, such as Count, Any, and Distinct, are method calls, and a query expression wrapped around one of them says nothing extra.
Preferred
var count = students.Count(s => s.Score > 90);Avoid
var count = (from s in students
where s.Score > 90
select s).Count();Corpus: Write LINQ queries · LINQ queries in C#
Flatten with SelectMany, not a sequence of sequences
SelectMany projects each element into a sequence and concatenates the results into one sequence. Select alone leaves a sequence of sequences, and the query syntax for the same flattening is a second from clause.
Preferred
var cities = countries.SelectMany(c => c.Cities);Avoid
var cities = countries.Select(c => c.Cities);Corpus: Projection operations in LINQ · Query expression basics (LINQ)
Sort once with OrderBy and ThenBy
OrderBy sets the primary sort key and ThenBy adds the next one, which is what orderby a, b compiles to. A second OrderBy starts a new sort instead of refining the first one.
Preferred
var sorted = teachers.OrderBy(t => t.City).ThenBy(t => t.LastName);Avoid
var sorted = teachers.OrderBy(t => t.City).OrderBy(t => t.LastName);Corpus: Sorting Data
Look up by key with ToLookup, not a repeated GroupBy
GroupBy builds its groups when the query is enumerated, so a query that filters for one key re-groups the source on every call. ToLookup runs once and returns a one-to-many dictionary that answers by key.
Preferred
var byDepartment = students.ToLookup(s => s.DepartmentId);
var members = byDepartment[departmentId];Avoid
var members = students.GroupBy(s => s.DepartmentId)
.First(g => g.Key == departmentId);Corpus: Grouping Data · Converting Data Types
Match on keys with Join, not nested loops
Join correlates two sequences on equal keys and keeps the pairs, the shape of an inner join. GroupJoin keeps every element of the left sequence together with its matches, which is how a left outer join is built. Nested loops do the same matching by hand and grow with the product of the two lengths.
Preferred
var pairs = students.Join(
departments,
s => s.DepartmentId,
d => d.Id,
(s, d) => (s.Name, d.Title));Avoid
var pairs = new List<(string, string)>();
foreach (var s in students)
foreach (var d in departments)
if (s.DepartmentId == d.Id)
pairs.Add((s.Name, d.Title));Corpus: Join operations in LINQ
Keep side effects out of the query body
A lambda in Where or Select runs when the query runs, and the query runs every time it is enumerated. A print, a write, or a loop inside the lambda therefore happens at times the caller does not control, so the work belongs in the foreach that consumes the query.
Preferred
var names = students.Select(s => s.Name);
foreach (var name in names)
{
Console.WriteLine(name);
}Avoid
var names = students.Select(s =>
{
foreach (var score in s.Scores) { Record(score); }
return s.Name;
});Reach for the keyed operators before sorting or grouping
MaxBy and MinBy take a key selector and return the element with the extreme key, DistinctBy and the other ...By set operators compare on a key rather than the whole element, and Chunk splits a sequence into batches. CountBy counts each key without the intermediate groupings that GroupBy allocates.
Preferred
var best = users.MaxBy(u => u.Score);Avoid
var best = users.OrderByDescending(u => u.Score).First();Corpus: What's new in .NET 6 · What's new in .NET libraries for .NET 9 · Partitioning data
Keep queries off the hot path
A deferred operator returns an object that captures its arguments, and enumerating it runs the whole chain again through delegate calls. On a path that executes per request or per frame, write the loop, and prefer the member a collection already has over a LINQ call that answers the same question; the query earns its place where it runs once and reads clearly.
Preferred
foreach (var item in items)
{
if (item.IsActive) total += item.Value;
}Avoid
total = items.Where(i => i.IsActive).Sum(i => i.Value);Corpus: Standard query operators overview · CA1860: Avoid using 'Enumerable.Any()' extension method