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 source
  • IEnumerable<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

  1. Data source: The collection or data source to query
  2. Query creation: Define the query using LINQ operators
  3. Query execution: Iterate over the results or force execution
  4. 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

  1. Define LINQ clearly: LINQ is a set of features in .NET that provides a consistent way to query and manipulate data from different sources.

  2. Syntax options: Explain the difference between query syntax and method syntax, and when each might be preferred.

  3. Deferred execution: Emphasize that LINQ queries use deferred execution, meaning they’re not executed until the results are needed.

  4. Common operations: Be familiar with filtering, sorting, projection, joining, and grouping operations.

  5. LINQ providers: Understand the different LINQ providers (LINQ to Objects, LINQ to Entities, LINQ to XML) and how they translate queries.

  6. Performance considerations: Discuss how to optimize LINQ queries, especially when working with databases.

  7. 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.

Test Your .NET Knowledge

Ready to put your skills to the test? Take our interactive .NET quiz and get instant feedback on your answers.