What are LINQ queries, and how do they work in .NET?
Understanding LINQ
Language Integrated Query (LINQ) is a set of features in .NET that provides a consistent, expressive, and type-safe way to query and manipulate data from different sources. LINQ bridges the gap between the world of objects and the world of data.
// Basic LINQ query example
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Query syntax
var evenNumbersQuery = from n in numbers
where n % 2 == 0
select n;
// Method syntax
var evenNumbersMethod = numbers.Where(n => n % 2 == 0);
foreach (var number in evenNumbersQuery)
{
Console.WriteLine(number); // Outputs: 2, 4, 6, 8, 10
}
LINQ Providers
LINQ can work with different data sources through various providers:
- LINQ to Objects: Query in-memory collections that implement
IEnumerable<T>
- LINQ to Entities: Query databases through Entity Framework
- LINQ to SQL: Query SQL Server databases (older technology)
- LINQ to XML: Query and manipulate XML documents
- LINQ to DataSet: Query ADO.NET DataSets
- Custom LINQ Providers: Query custom data sources
// LINQ to Objects
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
// LINQ to Entities (Entity Framework)
var youngCustomers = dbContext.Customers
.Where(c => c.Age < 30)
.OrderBy(c => c.LastName)
.ToList();
// LINQ to XML
XDocument doc = XDocument.Load("data.xml");
var elements = from e in doc.Descendants("Customer")
where (int)e.Attribute("Age") < 30
select e;
// LINQ to DataSet
var youngCustomers = dataSet.Tables["Customers"].AsEnumerable()
.Where(row => row.Field<int>("Age") < 30)
.CopyToDataTable();
Query Syntax vs. Method Syntax
LINQ provides two syntax options for writing queries:
Query Syntax
// Query syntax (SQL-like)
var result = from c in customers
where c.City == "London"
orderby c.Name
select new { c.Name, c.Phone };
Method Syntax
// Method syntax (using extension methods)
var result = customers
.Where(c => c.City == "London")
.OrderBy(c => c.Name)
.Select(c => new { c.Name, c.Phone });
Both syntaxes are functionally equivalent, but:
- Query syntax is often more readable for complex queries
- Method syntax provides more functionality and flexibility
Key LINQ Operations
Filtering
// Where - filter elements based on a predicate
var adults = people.Where(p => p.Age >= 18);
// OfType - filter elements based on type
var strings = mixedList.OfType<string>();
Sorting
// OrderBy - sort in ascending order
var orderedByName = people.OrderBy(p => p.LastName);
// OrderByDescending - sort in descending order
var orderedByAgeDesc = people.OrderByDescending(p => p.Age);
// ThenBy - secondary sort
var orderedPeople = people
.OrderBy(p => p.LastName)
.ThenBy(p => p.FirstName);
Projection
// Select - transform elements
var names = people.Select(p => p.Name);
// SelectMany - flatten nested collections
var allPhoneNumbers = customers.SelectMany(c => c.PhoneNumbers);
// Anonymous types
var customerInfo = customers.Select(c => new { c.Name, c.City, IsAdult = c.Age >= 18 });
Joining
// Join - inner join
var query = from c in customers
join o in orders on c.CustomerId equals o.CustomerId
select new { c.Name, o.OrderDate, o.Amount };
// Method syntax join
var result = customers.Join(
orders,
customer => customer.CustomerId,
order => order.CustomerId,
(customer, order) => new { customer.Name, order.OrderDate, order.Amount }
);
// GroupJoin - left outer join with grouping
var customerOrders = from c in customers
join o in orders on c.CustomerId equals o.CustomerId into customerOrders
select new { Customer = c, Orders = customerOrders };
Grouping
// GroupBy - group elements by a key
var customersByCity = customers.GroupBy(c => c.City);
// Using grouping
foreach (var cityGroup in customersByCity)
{
Console.WriteLine($"City: {cityGroup.Key}");
foreach (var customer in cityGroup)
{
Console.WriteLine($" {customer.Name}");
}
}
// Query syntax with grouping
var ordersByCustomer = from o in orders
group o by o.CustomerId into customerOrders
select new
{
CustomerId = customerOrders.Key,
OrderCount = customerOrders.Count(),
TotalAmount = customerOrders.Sum(o => o.Amount)
};
Aggregation
// Count - count elements
int count = numbers.Count();
int evenCount = numbers.Count(n => n % 2 == 0);
// Sum - sum values
decimal totalAmount = orders.Sum(o => o.Amount);
// Min/Max - find minimum/maximum values
int minAge = people.Min(p => p.Age);
int maxAge = people.Max(p => p.Age);
// Average - calculate average
double averageAge = people.Average(p => p.Age);
// Aggregate - custom aggregation
int product = numbers.Aggregate(1, (acc, n) => acc * n);
Element Operations
// First/FirstOrDefault - get first element
var firstCustomer = customers.First();
var firstLondonCustomer = customers.First(c => c.City == "London");
var firstOrDefault = customers.FirstOrDefault(c => c.City == "Paris"); // null if none found
// Last/LastOrDefault - get last element
var lastCustomer = customers.Last();
var lastOrDefault = customers.LastOrDefault(c => c.City == "Paris");
// Single/SingleOrDefault - get single element (throws if multiple)
var uniqueCustomer = customers.Single(c => c.CustomerId == 12345);
var uniqueOrDefault = customers.SingleOrDefault(c => c.CustomerId == 12345);
// ElementAt/ElementAtOrDefault - get element at index
var thirdCustomer = customers.ElementAt(2);
var tenthOrDefault = customers.ElementAtOrDefault(9); // default if out of range
Set Operations
// Distinct - remove duplicates
var uniqueNames = names.Distinct();
// Union - combine sets, remove duplicates
var allCustomers = ukCustomers.Union(usCustomers);
// Intersect - common elements
var customersInBothLists = listA.Intersect(listB);
// Except - elements in first set but not in second
var customersOnlyInA = listA.Except(listB);
Quantifiers
// Any - check if any element satisfies condition
bool hasAdults = people.Any(p => p.Age >= 18);
// All - check if all elements satisfy condition
bool allAdults = people.All(p => p.Age >= 18);
// Contains - check if collection contains element
bool containsJohn = names.Contains("John");
Partitioning
// Take - get first n elements
var topFive = customers.OrderByDescending(c => c.Purchases).Take(5);
// Skip - skip first n elements
var afterTopFive = customers.OrderByDescending(c => c.Purchases).Skip(5);
// TakeLast - get last n elements (EF Core 2.1+)
var bottomFive = customers.OrderByDescending(c => c.Purchases).TakeLast(5);
// SkipLast - skip last n elements (EF Core 2.1+)
var notBottomFive = customers.OrderByDescending(c => c.Purchases).SkipLast(5);
// TakeWhile - take elements while condition is true
var takeWhileUnder40 = ages.TakeWhile(age => age < 40);
// SkipWhile - skip elements while condition is true
var skipWhileUnder40 = ages.SkipWhile(age => age < 40);
Deferred Execution
LINQ queries use deferred execution, meaning they’re not executed until the results are actually needed.
// Query definition (not executed yet)
var query = numbers.Where(n => n > 5);
// Add more numbers to the source collection
numbers.Add(10);
numbers.Add(20);
// Query execution (includes the newly added numbers)
foreach (var n in query)
{
Console.WriteLine(n);
}
Forcing Immediate Execution
// ToList - execute query and return results as List<T>
var results = query.ToList();
// ToArray - execute query and return results as array
var resultsArray = query.ToArray();
// ToDictionary - execute query and return results as Dictionary<TKey, TValue>
var customerDict = customers.ToDictionary(c => c.CustomerId);
// ToLookup - execute query and return results as ILookup<TKey, TElement>
var customersByCity = customers.ToLookup(c => c.City);
// Count, Sum, Average, etc. - execute query and return a scalar result
int count = query.Count();
LINQ and IQueryable
When working with LINQ to Entities (Entity Framework), queries are translated to SQL:
// LINQ to Entities query
var londonCustomers = dbContext.Customers
.Where(c => c.City == "London")
.OrderBy(c => c.Name)
.Select(c => new { c.Name, c.Email });
// The query above is translated to SQL like:
// SELECT [c].[Name], [c].[Email]
// FROM [Customers] AS [c]
// WHERE [c].[City] = N'London'
// ORDER BY [c].[Name]
IQueryable vs. IEnumerable
IQueryable<T>
: Represents a query that will be executed at the data sourceIEnumerable<T>
: Represents a query that will be executed in memory
// IQueryable - filtering happens at the database
IQueryable<Customer> queryable = dbContext.Customers.Where(c => c.City == "London");
// IEnumerable - all data is loaded, then filtered in memory
IEnumerable<Customer> enumerable = dbContext.Customers.AsEnumerable().Where(c => c.City == "London");
LINQ Query Execution Pipeline
- Data source: The collection or data source to query
- Query creation: Define the query using LINQ operators
- Query execution: Iterate over the results or force execution
- Result processing: Process the query results
// 1. Data source
List<Customer> customers = GetCustomers();
// 2. Query creation
var query = from c in customers
where c.Orders.Count > 10
orderby c.Name
select new { c.Name, OrderCount = c.Orders.Count };
// 3. Query execution
// 4. Result processing
foreach (var item in query) // Execution happens here
{
Console.WriteLine($"{item.Name}: {item.OrderCount} orders");
}
Advanced LINQ Techniques
Parallel LINQ (PLINQ)
// Parallel query execution
var parallelQuery = numbers.AsParallel()
.Where(n => IsPrime(n))
.OrderBy(n => n);
// Force parallel execution with specific options
var customParallelQuery = numbers.AsParallel()
.WithDegreeOfParallelism(4)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.Where(n => IsPrime(n));
Custom LINQ Operators
// Extension method to create custom LINQ operator
public static class LinqExtensions
{
public static IEnumerable<T> WhereNot<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return source.Where(item => !predicate(item));
}
}
// Usage
var nonAdults = people.WhereNot(p => p.Age >= 18);
Expression Trees
// Creating an expression tree manually
Expression<Func<Customer, bool>> expr = c => c.City == "London";
// Examining the expression tree
var parameter = expr.Parameters[0]; // c
var body = expr.Body; // c.City == "London"
// Building expression trees dynamically
ParameterExpression param = Expression.Parameter(typeof(Customer), "c");
MemberExpression property = Expression.Property(param, "City");
ConstantExpression constant = Expression.Constant("London");
BinaryExpression equality = Expression.Equal(property, constant);
var lambdaExpr = Expression.Lambda<Func<Customer, bool>>(equality, param);
var compiledFunc = lambdaExpr.Compile();
// Using the compiled expression
var result = customers.Where(compiledFunc);
Interview Tips
Define LINQ clearly: LINQ is a set of features in .NET that provides a consistent way to query and manipulate data from different sources.
Syntax options: Explain the difference between query syntax and method syntax, and when each might be preferred.
Deferred execution: Emphasize that LINQ queries use deferred execution, meaning they’re not executed until the results are needed.
Common operations: Be familiar with filtering, sorting, projection, joining, and grouping operations.
LINQ providers: Understand the different LINQ providers (LINQ to Objects, LINQ to Entities, LINQ to XML) and how they translate queries.
Performance considerations: Discuss how to optimize LINQ queries, especially when working with databases.
IQueryable vs. IEnumerable: Explain the difference and when to use each interface.
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.