Overview
LINQ, or Language Integrated Query, is a set of C# language features and .NET APIs that let developers query data using strongly typed C# code. Instead of writing separate query logic for arrays, lists, XML, databases, and other data sources, LINQ provides a common query model based on sequence operations such as filtering, projection, sorting, grouping, joining, and aggregation.
In C#, LINQ is used through two main styles:
- Query syntax, which looks similar to SQL.
- Method syntax, which uses extension methods such as
Where,Select,OrderBy,GroupBy, andJoin.
LINQ matters because modern .NET applications constantly work with collections and data sources. A backend service might filter domain entities, transform DTOs, query Entity Framework Core data, group report results, validate input collections, or compose business rules over in-memory objects. LINQ gives developers a concise and readable way to express those operations.
For interviews, LINQ is important because it tests more than syntax. Interviewers often use LINQ questions to evaluate whether a candidate understands:
- Functional-style transformations in C#.
- Deferred execution and when a query actually runs.
- The difference between
IEnumerable<T>andIQueryable<T>. - How LINQ behaves with in-memory collections versus database-backed queries.
- Performance risks such as repeated enumeration, unnecessary materialization, client-side evaluation, and inefficient grouping or joining.
- Practical query writing skills using filtering, projection, grouping, joining, ordering, and aggregation.
A strong answer should show that LINQ is not just a shortcut for loops. It is a query abstraction that can produce clean code when used correctly, but it can also create hidden performance or correctness problems when developers do not understand execution timing, provider translation, and materialization.
Core Concepts
What LINQ Is
LINQ is a language and library feature that allows query operations to be written directly in C#.
A LINQ query usually has three parts:
- A data source.
- A query definition.
- Query execution.
Example:
var numbers = new[] { 1, 2, 3, 4, 5, 6 };
var evenNumbers = numbers
.Where(number => number % 2 == 0)
.Select(number => number * 10);
foreach (var number in evenNumbers)
{
Console.WriteLine(number);
}
In this example:
numbersis the data source.Wherefilters the sequence.Selectprojects each matching value.- The query executes when
evenNumbersis enumerated byforeach.
LINQ commonly works with:
- Arrays
List<T>Dictionary<TKey, TValue>IEnumerable<T>IQueryable<T>- XML data
- Entity Framework Core queries
- Custom query providers
Query Syntax and Method Syntax
C# supports both query syntax and method syntax.
Query syntax:
var adults =
from person in people
where person.Age >= 18
orderby person.LastName, person.FirstName
select person;
Method syntax:
var adults = people
.Where(person => person.Age >= 18)
.OrderBy(person => person.LastName)
.ThenBy(person => person.FirstName);
Both forms are compiled into method calls. Query syntax can be easier to read for complex queries involving join, group, or multiple from clauses. Method syntax is often more common in modern C# code because it is concise, composable, and supports all LINQ operators.
Some operations have no direct query syntax equivalent and must use method syntax, such as:
int count = people.Count(person => person.Age >= 18);
Person? first = people.FirstOrDefault(person => person.IsActive);
bool anyInactive = people.Any(person => !person.IsActive);
Best practice is to use the syntax that makes the query easiest to understand. Many production codebases prefer method syntax for simple pipelines and query syntax for complex joins or groupings.
Standard Query Operators
Standard query operators are methods that perform common query operations over sequences.
Common categories include:
Example using several operators:
var topCustomers = customers
.Where(customer => customer.IsActive)
.OrderByDescending(customer => customer.TotalSpend)
.Take(10)
.Select(customer => new CustomerSummary(
customer.Id,
customer.Name,
customer.TotalSpend))
.ToList();
This query filters active customers, sorts them by spend, takes the top ten, projects them into a DTO, and materializes the result as a list.
IEnumerable<T> and LINQ to Objects
IEnumerable<T> represents a sequence that can be enumerated. LINQ to Objects works with in-memory sequences such as arrays and lists.
Example:
IEnumerable<string> names = new List<string>
{
"Alice",
"Bob",
"Charlie"
};
var shortNames = names.Where(name => name.Length <= 3);
For IEnumerable<T>, LINQ operators usually execute in memory using delegates such as Func<T, bool>.
Example:
Func<string, bool> predicate = name => name.StartsWith("A");
var result = names.Where(predicate);
This is useful for in-memory operations, but it means every element may need to be inspected by the application process. For very large collections, developers should consider memory usage and algorithmic complexity.
IQueryable<T> and Provider-Based Queries
IQueryable<T> represents a query that can be translated by a provider. The most common example is Entity Framework Core, where a LINQ query is translated into SQL.
Example:
IQueryable<Customer> query = dbContext.Customers
.Where(customer => customer.IsActive)
.OrderBy(customer => customer.Name);
With IQueryable<T>, the LINQ expression is represented as an expression tree. A provider, such as EF Core, analyzes that expression tree and translates it into another query language, commonly SQL.
This is a critical interview topic because the same-looking LINQ query can behave differently depending on whether it is running over IEnumerable<T> or IQueryable<T>.
Example:
// Database-side filtering when Customers is IQueryable<Customer>
var customers = await dbContext.Customers
.Where(customer => customer.Country == "US")
.ToListAsync();
The filter can be translated into SQL and executed by the database.
But if the query is materialized too early:
var allCustomers = await dbContext.Customers.ToListAsync();
var usCustomers = allCustomers
.Where(customer => customer.Country == "US")
.ToList();
The application loads all customers first, then filters in memory. This can be a serious performance problem.
Deferred Execution
Deferred execution means a LINQ query is not executed when it is defined. It executes when it is enumerated.
Example:
var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(number => number > 1);
numbers.Add(4);
foreach (var number in query)
{
Console.WriteLine(number);
}
Output:
2
3
4
The query includes 4 because it was not executed when it was assigned to query. It was executed later during enumeration.
Common operations that trigger execution include:
foreachToListToArrayToDictionaryCountAnyFirstSingleSumAverageMaxMin
Deferred execution is powerful because it allows query composition:
IQueryable<Order> query = dbContext.Orders;
if (status is not null)
{
query = query.Where(order => order.Status == status);
}
if (fromDate is not null)
{
query = query.Where(order => order.CreatedAt >= fromDate);
}
var results = await query
.OrderByDescending(order => order.CreatedAt)
.Take(50)
.ToListAsync();
The query is built step by step but only sent to the database once.
Immediate Execution and Materialization
Immediate execution happens when a LINQ operation must produce a final value or materialized collection.
Examples:
int count = customers.Count();
bool hasActiveCustomers = customers.Any(customer => customer.IsActive);
List<Customer> activeCustomers = customers
.Where(customer => customer.IsActive)
.ToList();
ToList, ToArray, ToDictionary, and ToHashSet are materialization methods. They execute the query and store the results in memory.
Materialization is useful when:
- You need a stable snapshot of data.
- You will enumerate results multiple times.
- You want to close a database query before further in-memory processing.
- You need collection-specific operations.
But materializing too early can hurt performance:
// Bad for large database tables
var customers = await dbContext.Customers.ToListAsync();
var activeCustomers = customers
.Where(customer => customer.IsActive)
.Take(100)
.ToList();
Better:
var activeCustomers = await dbContext.Customers
.Where(customer => customer.IsActive)
.Take(100)
.ToListAsync();
The better version allows the database to filter and limit the result.
Streaming and Non-Streaming Operators
Some deferred operators can stream results one at a time. Others must process more data before returning results.
Streaming operators include:
var result = numbers
.Where(number => number > 10)
.Select(number => number * 2);
Where and Select can process each item as it is requested.
Non-streaming operators may need to inspect the full source before returning the first result. Examples include:
var ordered = numbers.OrderBy(number => number);
var grouped = people.GroupBy(person => person.DepartmentId);
OrderBy needs to sort the input. GroupBy needs to build groups. These operations can require more memory and time.
This matters for large data sets because a query chain can look simple but still perform expensive work.
Filtering with Where
Where filters a sequence based on a predicate.
var activeProducts = products
.Where(product => product.IsActive);
Multiple Where calls can be composed:
var query = products.AsEnumerable();
query = query.Where(product => product.IsActive);
query = query.Where(product => product.Price > 100);
For IQueryable<T>, multiple filters are typically combined into one database query by the provider.
Common mistake:
var expensiveProducts = products
.Where(product => product.Price > 100)
.Where(product => product.Category.Name == "Electronics");
This is fine if products is in memory and all navigation properties are loaded. But with EF Core, navigation access and translation behavior must be considered. Developers should understand what gets translated and whether related data is needed.
Projection with Select
Select transforms each element into another shape.
Example:
var customerNames = customers
.Select(customer => customer.Name);
Projection into DTOs:
var summaries = await dbContext.Customers
.Where(customer => customer.IsActive)
.Select(customer => new CustomerSummaryDto
{
Id = customer.Id,
Name = customer.Name,
Email = customer.Email
})
.ToListAsync();
Projection is especially important with EF Core because it can reduce data transfer. Instead of loading full entities, you can select only the fields needed by the API response.
Common mistake:
var customers = await dbContext.Customers.ToListAsync();
var summaries = customers.Select(customer => new CustomerSummaryDto
{
Id = customer.Id,
Name = customer.Name
});
This loads full entities first, then maps in memory.
Better:
var summaries = await dbContext.Customers
.Select(customer => new CustomerSummaryDto
{
Id = customer.Id,
Name = customer.Name
})
.ToListAsync();
Flattening with SelectMany
SelectMany flattens nested collections.
Example:
var allOrderLines = orders
.SelectMany(order => order.Lines);
Without SelectMany, Select returns a sequence of sequences:
IEnumerable<IEnumerable<OrderLine>> linesByOrder = orders
.Select(order => order.Lines);
With SelectMany, the result is a single sequence:
IEnumerable<OrderLine> allLines = orders
.SelectMany(order => order.Lines);
SelectMany is useful for:
- Orders and order lines.
- Customers and addresses.
- Roles and permissions.
- Parent-child collections.
- Flattening nested DTOs.
Example with parent context:
var lineSummaries = orders
.SelectMany(
order => order.Lines,
(order, line) => new
{
OrderId = order.Id,
ProductId = line.ProductId,
Quantity = line.Quantity
});
Sorting with OrderBy and ThenBy
OrderBy starts a sort operation. ThenBy adds secondary sorting.
var sortedPeople = people
.OrderBy(person => person.LastName)
.ThenBy(person => person.FirstName);
Common mistake:
var sortedPeople = people
.OrderBy(person => person.LastName)
.OrderBy(person => person.FirstName);
The second OrderBy starts a new primary sort and can override the previous ordering. Use ThenBy for secondary sorting.
Descending sort:
var recentOrders = orders
.OrderByDescending(order => order.CreatedAt)
.ThenBy(order => order.Id);
For pagination, always use deterministic ordering:
var page = await dbContext.Orders
.OrderByDescending(order => order.CreatedAt)
.ThenByDescending(order => order.Id)
.Skip(pageIndex * pageSize)
.Take(pageSize)
.ToListAsync();
Adding a tie-breaker like Id helps avoid unstable pages when multiple records have the same date.
Grouping with GroupBy
GroupBy groups elements by a key.
var ordersByStatus = orders
.GroupBy(order => order.Status);
foreach (var group in ordersByStatus)
{
Console.WriteLine(group.Key);
foreach (var order in group)
{
Console.WriteLine(order.Id);
}
}
Projection after grouping:
var totalsByStatus = orders
.GroupBy(order => order.Status)
.Select(group => new
{
Status = group.Key,
Count = group.Count(),
Total = group.Sum(order => order.Total)
});
With EF Core, grouping can be translated when the shape maps cleanly to SQL aggregation. Complex group projections may not translate or may require different query design.
Common usage in APIs:
var report = await dbContext.Orders
.Where(order => order.CreatedAt >= start && order.CreatedAt < end)
.GroupBy(order => order.Status)
.Select(group => new OrderStatusReportDto
{
Status = group.Key,
Count = group.Count(),
TotalAmount = group.Sum(order => order.TotalAmount)
})
.ToListAsync();
Joining Sequences
Join combines two sequences based on matching keys.
var query = customers.Join(
orders,
customer => customer.Id,
order => order.CustomerId,
(customer, order) => new
{
CustomerName = customer.Name,
OrderId = order.Id,
order.Total
});
Query syntax is often more readable for joins:
var query =
from customer in customers
join order in orders
on customer.Id equals order.CustomerId
select new
{
CustomerName = customer.Name,
OrderId = order.Id,
order.Total
};
Group join:
var customersWithOrders =
from customer in customers
join order in orders
on customer.Id equals order.CustomerId
into customerOrders
select new
{
Customer = customer,
Orders = customerOrders
};
Left join pattern:
var query =
from customer in customers
join order in orders
on customer.Id equals order.CustomerId
into customerOrders
from order in customerOrders.DefaultIfEmpty()
select new
{
CustomerName = customer.Name,
OrderId = order?.Id
};
In EF Core, joins are often unnecessary if navigation properties are properly modeled. However, explicit joins are still useful for projections, reporting, and queries across non-navigation relationships.
Aggregation
Aggregation produces a single value from a sequence.
Examples:
int count = orders.Count();
decimal total = orders.Sum(order => order.TotalAmount);
decimal average = orders.Average(order => order.TotalAmount);
decimal max = orders.Max(order => order.TotalAmount);
Use Any instead of Count() > 0 when you only need to know whether at least one element exists:
bool hasOrders = orders.Any();
For EF Core, Any can translate to an efficient existence check, while Count counts matching rows.
Common mistake:
if (orders.Count() > 0)
{
// ...
}
Better:
if (orders.Any())
{
// ...
}
Aggregate is a general-purpose accumulator:
var csv = names.Aggregate((current, next) => $"{current}, {next}");
For strings, prefer string.Join because it is clearer and more efficient:
var csv = string.Join(", ", names);
Element Operators
Element operators return specific elements.
Common examples:
var first = people.First();
var firstOrDefault = people.FirstOrDefault();
var single = people.Single();
var singleOrDefault = people.SingleOrDefault();
Important differences:
Use Single when the business rule requires exactly one match. Use First when multiple matches are acceptable and you only need the first.
Example:
var user = await dbContext.Users
.SingleOrDefaultAsync(user => user.Email == email);
This communicates that email should be unique. If more than one record matches, an exception exposes a data integrity problem.
For high-traffic paths where exceptions are not desired, you may query with Take(2) and handle duplicates explicitly.
Set Operations
Set operations compare or combine sequences.
var uniqueTags = tags.Distinct();
var allIds = internalIds.Union(externalIds);
var sharedIds = internalIds.Intersect(externalIds);
var missingIds = expectedIds.Except(actualIds);
For custom types, set operations need correct equality behavior. You can use records, override equality, or pass an equality comparer.
Example with custom comparer:
var distinctCustomers = customers.DistinctBy(customer => customer.Email);
Common set-related operators include:
DistinctDistinctByUnionUnionByIntersectIntersectByExceptExceptBy
These are useful for deduplication, comparing IDs, and merging data from different sources.
Partitioning and Pagination
Partitioning operators return part of a sequence.
var firstTen = products.Take(10);
var nextTen = products.Skip(10).Take(10);
Pagination example:
var page = await dbContext.Products
.Where(product => product.IsActive)
.OrderBy(product => product.Name)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(product => new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price
})
.ToListAsync();
Best practices:
- Always order before using
SkipandTake. - Validate page number and page size.
- Avoid very large offsets for high-scale systems because offset pagination can become expensive.
- Consider keyset pagination for large data sets.
Keyset pagination example:
var nextPage = await dbContext.Products
.Where(product => product.Name.CompareTo(lastName) > 0)
.OrderBy(product => product.Name)
.Take(pageSize)
.ToListAsync();
Keyset pagination uses a known last value instead of skipping many rows.
Any, All, and Contains
Quantifier operators answer yes/no questions.
bool hasActiveUsers = users.Any(user => user.IsActive);
bool allUsersVerified = users.All(user => user.IsVerified);
bool containsId = selectedIds.Contains(user.Id);
Important detail:
bool allVerified = users.All(user => user.IsVerified);
If users is empty, All returns true. This is mathematically correct but can surprise developers. If you require at least one item and all must match:
bool valid = users.Any() && users.All(user => user.IsVerified);
With EF Core, Contains over a local list of primitive values is commonly translated into an SQL IN expression:
var selectedUsers = await dbContext.Users
.Where(user => selectedIds.Contains(user.Id))
.ToListAsync();
For very large ID lists, consider database-specific limits and alternative designs such as temporary tables, table-valued parameters, or batch processing.
Null Handling in LINQ
LINQ queries often interact with nullable data.
Example:
var names = people
.Where(person => person.Name is not null)
.Select(person => person.Name!);
Null-safe projection:
var cities = customers
.Select(customer => customer.Address?.City)
.Where(city => city is not null);
Default values:
var displayNames = users
.Select(user => user.DisplayName ?? user.Email);
Be careful with FirstOrDefault because default can be null for reference types and 0 for integers.
var firstNumber = numbers.FirstOrDefault();
If numbers is empty, firstNumber is 0, which may also be a valid number. In such cases, use nullable projection or check with Any.
Custom Types, Equality, and Comparers
LINQ operators such as Distinct, GroupBy, ToDictionary, Join, and Contains depend on equality.
For primitive types, equality usually works as expected.
For custom classes, default equality compares references unless equality is overridden.
Example:
public sealed class Customer
{
public string Email { get; init; } = "";
}
Two Customer objects with the same email are not equal by default if they are different instances.
Options:
Use a record:
public sealed record CustomerKey(string Email);
Use key selector operators:
var uniqueCustomers = customers.DistinctBy(customer => customer.Email);
Use a custom comparer:
public sealed class CustomerEmailComparer : IEqualityComparer<Customer>
{
public bool Equals(Customer? x, Customer? y)
{
return string.Equals(x?.Email, y?.Email, StringComparison.OrdinalIgnoreCase);
}
public int GetHashCode(Customer obj)
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Email);
}
}
Then:
var uniqueCustomers = customers.Distinct(new CustomerEmailComparer());
ToDictionary, ToLookup, and GroupBy
ToDictionary creates a dictionary and requires unique keys.
var customersById = customers.ToDictionary(customer => customer.Id);
If duplicate keys exist, ToDictionary throws an exception.
ToLookup creates a lookup where each key can have multiple values.
var ordersByCustomerId = orders.ToLookup(order => order.CustomerId);
var ordersForCustomer = ordersByCustomerId[customerId];
GroupBy creates grouped sequences and is often used for reporting or aggregation.
var groups = orders.GroupBy(order => order.CustomerId);
Comparison:
LINQ and Entity Framework Core
When LINQ is used with EF Core, the query is translated to database-specific SQL where possible.
Example:
var products = await dbContext.Products
.Where(product => product.IsActive && product.Price > 100)
.OrderBy(product => product.Name)
.Select(product => new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price
})
.ToListAsync();
This should execute filtering, ordering, and projection in the database.
Important EF Core LINQ principles:
- Keep queryable operations as
IQueryable<T>until the final materialization step. - Use
ToListAsync,FirstOrDefaultAsync,SingleOrDefaultAsync, and other async operators for database queries. - Project only the columns needed for API responses.
- Avoid calling custom C# methods inside filters because they may not translate to SQL.
- Be careful with
AsEnumerable; it switches from provider-based querying to in-memory LINQ. - Use
AsNoTrackingfor read-only entity queries when change tracking is not needed. - Avoid N+1 query patterns.
- Understand what runs on the server and what runs in memory.
Example of a bad query for EF Core:
var products = await dbContext.Products
.Where(product => IsExpensive(product.Price))
.ToListAsync();
static bool IsExpensive(decimal price) => price > 100;
The custom method may not translate to SQL.
Better:
var products = await dbContext.Products
.Where(product => product.Price > 100)
.ToListAsync();
Client Evaluation and Server Evaluation
Server evaluation means the database performs the filtering, sorting, grouping, or aggregation. Client evaluation means the application performs it after data is loaded.
Server-side filtering:
var activeUsers = await dbContext.Users
.Where(user => user.IsActive)
.ToListAsync();
Client-side filtering after materialization:
var users = await dbContext.Users.ToListAsync();
var activeUsers = users
.Where(user => user.IsActive)
.ToList();
The second version loads all users into memory first.
AsEnumerable can intentionally switch to client-side LINQ:
var results = dbContext.Users
.Where(user => user.IsActive)
.AsEnumerable()
.Where(user => CustomInMemoryRule(user))
.ToList();
This can be valid when:
- The database-side query already reduces the data to a small set.
- The remaining rule cannot be translated to SQL.
- The performance trade-off is acceptable.
But it should be used deliberately and not accidentally.
Multiple Enumeration
Multiple enumeration happens when the same deferred query is executed more than once.
Example:
var activeUsers = users.Where(user => user.IsActive);
int count = activeUsers.Count();
foreach (var user in activeUsers)
{
Console.WriteLine(user.Name);
}
If users is a database query, this can trigger multiple database queries. If users is an expensive iterator, it can repeat expensive work.
If the results are needed multiple times, materialize once:
var activeUsers = users
.Where(user => user.IsActive)
.ToList();
int count = activeUsers.Count;
foreach (var user in activeUsers)
{
Console.WriteLine(user.Name);
}
Be careful not to materialize too early. Materialization is useful when you need a snapshot or repeated access, but harmful when it prevents efficient query translation.
LINQ Performance Considerations
LINQ improves readability, but developers should understand performance.
Common performance concerns:
- Repeated enumeration of deferred queries.
- Materializing large data sets with
ToList. - Filtering after materialization instead of before.
- Using
Count() > 0instead ofAny(). - Calling
OrderBymore than needed. - Using nested LINQ queries that cause O(n²) behavior.
- Using
GroupByorToDictionarywithout considering memory usage. - Accidentally switching from
IQueryable<T>toIEnumerable<T>. - Using custom methods in EF Core query filters.
- Selecting full entities when a DTO projection would be enough.
Example of inefficient nested lookup:
var results = customers.Select(customer => new
{
Customer = customer,
Orders = orders.Where(order => order.CustomerId == customer.Id).ToList()
});
If both collections are in memory, this may scan orders for every customer.
Better:
var ordersByCustomerId = orders.ToLookup(order => order.CustomerId);
var results = customers.Select(customer => new
{
Customer = customer,
Orders = ordersByCustomerId[customer.Id].ToList()
});
This builds a lookup once and avoids repeated scanning.
LINQ Readability and Maintainability
LINQ is best when it makes intent clear.
Readable:
var overdueInvoices = invoices
.Where(invoice => invoice.Status == InvoiceStatus.Open)
.Where(invoice => invoice.DueDate < today)
.OrderBy(invoice => invoice.DueDate)
.Select(invoice => new OverdueInvoiceDto
{
Id = invoice.Id,
CustomerName = invoice.Customer.Name,
DueDate = invoice.DueDate,
Amount = invoice.Amount
})
.ToList();
Hard to read:
var result = invoices.Where(x => x.Status == InvoiceStatus.Open && x.DueDate < today)
.OrderBy(x => x.DueDate)
.Select(x => new OverdueInvoiceDto { Id = x.Id, CustomerName = x.Customer.Name, DueDate = x.DueDate, Amount = x.Amount })
.ToList();
Best practices:
- Use meaningful lambda parameter names in complex queries.
- Break long queries into steps when it improves clarity.
- Avoid mixing heavy business logic inside LINQ expressions.
- Prefer explicit DTO projection for API responses.
- Keep database queries translatable.
- Use method syntax and query syntax intentionally.
- Add tests for complex query behavior.
Common Mistakes
Common LINQ mistakes include:
Assuming the Query Runs Immediately
var query = users.Where(user => user.IsActive);
This defines a query. It does not necessarily execute it.
Materializing Too Early
var users = await dbContext.Users.ToListAsync();
var active = users.Where(user => user.IsActive);
This loads all rows before filtering.
Using OrderBy Twice Instead of ThenBy
var sorted = users
.OrderBy(user => user.LastName)
.OrderBy(user => user.FirstName);
Use:
var sorted = users
.OrderBy(user => user.LastName)
.ThenBy(user => user.FirstName);
Using Count() > 0
if (orders.Count() > 0)
{
}
Use:
if (orders.Any())
{
}
Ignoring First, Single, and Default Behavior
var user = users.First(user => user.Email == email);
This throws if there is no match. Use FirstOrDefault or SingleOrDefault depending on the business rule.
Accidentally Running Client-Side Logic for Database Queries
var users = dbContext.Users
.AsEnumerable()
.Where(user => ExpensiveCustomCheck(user))
.ToList();
This might load far more data than expected.
Reusing Deferred Queries Without Understanding Re-Execution
var query = dbContext.Users.Where(user => user.IsActive);
var count = await query.CountAsync();
var list = await query.ToListAsync();
This sends two database queries. That may be acceptable, but it should be intentional.