| name | dotnet-linq |
| description | Pure LINQ standards for .NET 10 projects. Covers language operators, query composition, projection patterns, filtering, grouping, and performance best practices over IEnumerable<T> and IQueryable<T>. Load when writing or reviewing LINQ expressions in any layer. DO NOT load expecting EF Core-specific methods (ToListAsync, AsNoTracking, Include, ExecuteUpdateAsync) — those belong to `backend/dotnet/orms/ef-core/query-best-practices/SKILL.md`.
|
| requires | ["backend-dotnet-csharp"] |
| produces | ["linq-expressions","query-compositions","in-memory-projections"] |
LINQ — TemperAI Standards
What this skill covers
LINQ operators and patterns that work over any IEnumerable<T> or IQueryable<T> —
regardless of whether the source is a database, a list, an array, or any other collection.
This skill does NOT cover:
ToListAsync, AnyAsync, FirstOrDefaultAsync — these are EF Core extensions, not LINQ
AsNoTracking, Include, AsSplitQuery, TagWith — EF Core query behavior
ExecuteUpdateAsync, ExecuteDeleteAsync — EF Core bulk operations
- Tracking vs no-tracking strategy — EF Core concern
For all of the above, load backend/dotnet/orms/ef-core/query-best-practices/SKILL.md.
🚨 NON-NEGOTIABLE RULES — ZERO TOLERANCE
- NEVER filter after
.ToList() or .AsEnumerable() — always filter before materializing
- NEVER use manual
.Join() — use navigation properties or SelectMany
- NEVER chain multiple
Where clauses when one suffices — compose predicates explicitly
- ALWAYS use
Any() instead of Count() > 0 — stops at first match
When NOT to load this skill
- Task is purely about EF Core query behavior (tracking, includes, async materialization) → load
backend/dotnet/orms/ef-core/query-best-practices/SKILL.md
- Task has no collection or query logic at all
Core operators — quick reference
| Operator | Use for | Returns |
|---|
Where | Filtering | IQueryable<T> / IEnumerable<T> |
Select | Projection | IQueryable<TResult> |
SelectMany | Flatten nested collections | IQueryable<TResult> |
OrderBy / ThenBy | Sorting | IOrderedQueryable<T> |
GroupBy | Grouping | IQueryable<IGrouping<TKey,T>> |
Any | Existence check | bool |
All | Universal check | bool |
Count | Count items (use Any for existence) | int |
First / FirstOrDefault | First match | T / T? |
Single / SingleOrDefault | Exactly one match | T / T? |
Skip / Take | Pagination | IQueryable<T> |
Distinct | Remove duplicates | IQueryable<T> |
Contains | IN-style check | bool |
Projection — always project to what you need
Never return more data than required. Project to DTOs or anonymous types as close to the source as possible.
IEnumerable<ProductSummaryDto> summaries = products
.Select(product => new ProductSummaryDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price
});
List<Product> all = products.ToList();
List<ProductSummaryDto> summaries = all
.Select(p => new ProductSummaryDto { ... })
.ToList();
Filtering — always before materialization
IEnumerable<Product> active = products
.Where(product => product.Status == ProductStatus.Active);
List<Product> all = products.ToList();
List<Product> active = all
.Where(product => product.Status == ProductStatus.Active)
.ToList();
Composable queries — build incrementally
When filters are conditional, compose the query step by step instead of duplicating predicates.
IQueryable<Product> query = source;
if (!string.IsNullOrWhiteSpace(searchTerm))
query = query.Where(product => product.Name.Contains(searchTerm));
if (status.HasValue)
query = query.Where(product => product.Status == status.Value);
if (minPrice.HasValue)
query = query.Where(product => product.Price >= minPrice.Value);
List<Product> results = query.ToList();
if (searchTerm != null && status.HasValue)
results = source.Where(p => p.Name.Contains(searchTerm) && p.Status == status.Value).ToList();
else if (searchTerm != null)
results = source.Where(p => p.Name.Contains(searchTerm)).ToList();
Existence check — always Any, never Count > 0
bool hasActive = products.Any(product => product.Status == ProductStatus.Active);
bool hasActive = products.Count(product => product.Status == ProductStatus.Active) > 0;
Flattening nested collections — SelectMany
IEnumerable<OrderItem> allItems = orders
.SelectMany(order => order.Items);
IEnumerable<(Order Order, OrderItem Item)> pairs = orders
.SelectMany(
order => order.Items,
(order, item) => (order, item));
Grouping
IEnumerable<CategorySummaryDto> grouped = products
.GroupBy(product => product.Category)
.Select(group => new CategorySummaryDto
{
Category = group.Key,
Count = group.Count(),
AveragePrice = group.Average(p => p.Price)
});
Pagination — always Skip then Take, always with OrderBy
Pagination without OrderBy produces non-deterministic results.
IEnumerable<Product> page = products
.OrderBy(product => product.Name)
.ThenBy(product => product.Id)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
IEnumerable<Product> page = products
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
IN-style checks — Contains on a collection
List<Guid> targetIds = [id1, id2, id3];
IEnumerable<Product> matching = products
.Where(product => targetIds.Contains(product.Id));
When used over EF Core IQueryable<T>, this translates to WHERE Id IN (...).
For in-memory collections larger than 10,000 items, use a HashSet<T> for O(1) lookup:
HashSet<Guid> targetIds = new HashSet<Guid>([id1, id2, id3, ]);
IEnumerable<Product> matching = products
.Where(product => targetIds.Contains(product.Id));
Null safety in LINQ expressions
if (products is null || !products.Any())
return [];
IEnumerable<string> names = products
.Where(product => product.Name is not null)
.Select(product => product.Name!);
IEnumerable<string> names = products.Select(p => p.Name!);
Performance rules summary
| Rule | Reason |
|---|
Always filter before .ToList() | Avoids loading unnecessary data into memory |
Always project with Select early | Reduces the data surface at the source |
Always use Any() over Count() > 0 | Stops evaluation at first match |
Always OrderBy before Skip/Take | Deterministic pagination |
Use HashSet<T> for large in-memory Contains | O(1) lookup vs O(n) on list |
Never manual .Join() | Use navigation properties or SelectMany |
Never chain .Where duplicates | Compose predicates with the composable pattern |