C# LINQ Explained: Query and Transform Collections Efficiently

LINQ, or Language Integrated Query, provides a consistent way to search, filter, sort, transform, group, and analyze data in C#. It works with collections and many other data sources while keeping query logic concise and readable.

What is LINQ?

LINQ is a set of language and library features that allows developers to query data using C# syntax. Common LINQ operations include Where, Select, OrderBy, GroupBy, Any, Count, and Sum.

CSHARP
List<int> numbers = new() { 10, 15, 20, 25, 30 };

var result = numbers.Where(number => number > 20);

foreach (int number in result)
{
    Console.WriteLine(number);
}

Why Use LINQ?

  • Reduce repetitive collection-processing code
  • Make data operations easier to read
  • Filter and transform collections efficiently
  • Combine multiple operations into a pipeline
  • Work with objects using strongly typed expressions
  • Query different kinds of data sources with familiar patterns

The Where Method

Where filters a sequence according to a condition and returns the elements that satisfy that condition.

CSHARP
List<int> numbers = new() { 5, 10, 15, 20, 25 };

var result = numbers.Where(number => number >= 15);

foreach (int number in result)
{
    Console.WriteLine(number);
}

The Select Method

Select transforms each element into another value. It is commonly used to project objects into simpler data structures.

CSHARP
List<int> numbers = new() { 1, 2, 3, 4 };

var squares = numbers.Select(number => number * number);

foreach (int square in squares)
{
    Console.WriteLine(square);
}

Filtering and Projection Together

LINQ methods can be chained together to create readable data-processing pipelines.

CSHARP
List<int> numbers = new() { 5, 10, 15, 20, 25, 30 };

var result = numbers
    .Where(number => number > 10)
    .Select(number => number * 2);

foreach (int number in result)
{
    Console.WriteLine(number);
}

Sorting with OrderBy

OrderBy sorts elements in ascending order. OrderByDescending can be used for descending order.

CSHARP
List<int> numbers = new() { 40, 10, 30, 20 };

var ascending = numbers.OrderBy(number => number);
var descending = numbers.OrderByDescending(number => number);

foreach (int number in ascending)
{
    Console.WriteLine(number);
}

ThenBy for Secondary Sorting

ThenBy and ThenByDescending provide additional sorting rules when two or more elements have the same primary sort value.

CSHARP
var users = new List<User>
{
    new User("Alice", "Developer"),
    new User("Bob", "Admin"),
    new User("Charlie", "Developer")
};

var result = users
    .OrderBy(user => user.Role)
    .ThenBy(user => user.Name);

foreach (var user in result)
{
    Console.WriteLine($"{user.Role}: {user.Name}");
}

class User
{
    public string Name { get; }
    public string Role { get; }

    public User(string name, string role)
    {
        Name = name;
        Role = role;
    }
}

First and FirstOrDefault

First returns the first matching element and throws when no element exists. FirstOrDefault returns the default value when the sequence contains no matching element.

CSHARP
List<int> numbers = new() { 10, 20, 30 };

int first = numbers.First();
int? value = numbers
    .Where(number => number > 100)
    .Cast<int?>()
    .FirstOrDefault();

Console.WriteLine(first);
Console.WriteLine(value);

Any and All

Any checks whether at least one element satisfies a condition. All checks whether every element satisfies a condition.

CSHARP
List<int> numbers = new() { 10, 20, 30 };

bool hasLargeNumber = numbers.Any(number => number > 25);
bool allPositive = numbers.All(number => number > 0);

Console.WriteLine(hasLargeNumber);
Console.WriteLine(allPositive);

Count and Sum

LINQ provides aggregation methods such as Count, Sum, Average, Min, and Max for calculating values from a sequence.

CSHARP
List<int> numbers = new() { 10, 20, 30, 40 };

int count = numbers.Count();
int sum = numbers.Sum();
double average = numbers.Average();
int minimum = numbers.Min();
int maximum = numbers.Max();

Console.WriteLine(count);
Console.WriteLine(sum);
Console.WriteLine(average);
Console.WriteLine(minimum);
Console.WriteLine(maximum);

Grouping with GroupBy

GroupBy organizes elements into groups based on a selected key. It is useful for categories, departments, locations, and other grouped data.

CSHARP
var employees = new List<Employee>
{
    new Employee("Alice", "IT"),
    new Employee("Bob", "HR"),
    new Employee("Charlie", "IT"),
    new Employee("David", "HR")
};

var groups = employees.GroupBy(employee => employee.Department);

foreach (var group in groups)
{
    Console.WriteLine(group.Key);

    foreach (var employee in group)
    {
        Console.WriteLine(employee.Name);
    }
}

class Employee
{
    public string Name { get; }
    public string Department { get; }

    public Employee(string name, string department)
    {
        Name = name;
        Department = department;
    }
}

Distinct Values

Distinct removes duplicate values from a sequence.

CSHARP
List<string> categories = new()
{
    "Books",
    "Games",
    "Books",
    "Music",
    "Games"
};

var uniqueCategories = categories.Distinct();

foreach (string category in uniqueCategories)
{
    Console.WriteLine(category);
}

SelectMany

SelectMany combines nested collections into a single sequence. It is useful when each object contains its own collection of values.

CSHARP
var teams = new List<Team>
{
    new Team("Team A", new List<string> { "Alice", "Bob" }),
    new Team("Team B", new List<string> { "Charlie", "David" })
};

var members = teams.SelectMany(team => team.Members);

foreach (string member in members)
{
    Console.WriteLine(member);
}

class Team
{
    public string Name { get; }
    public List<string> Members { get; }

    public Team(string name, List<string> members)
    {
        Name = name;
        Members = members;
    }
}

LINQ Query Syntax

LINQ can also be written using query syntax. Method syntax is more common in many modern C# codebases, but query syntax can be useful for certain queries.

CSHARP
List<int> numbers = new() { 1, 2, 3, 4, 5, 6 };

var result = from number in numbers
             where number > 3
             select number;

foreach (int number in result)
{
    Console.WriteLine(number);
}

Method Syntax vs Query Syntax

FeatureMethod SyntaxQuery Syntax
StyleExtension methodsSQL-like syntax
Common OperatorsWhere, Select, OrderBywhere, select, orderby
ReadabilityConcise for pipelinesUseful for complex query expressions
Common UsageVery common in modern C#Useful for certain query scenarios

Deferred Execution

Many LINQ queries use deferred execution, meaning the query is not executed until its results are enumerated. This can be useful, but developers should understand when the underlying data is actually accessed.

CSHARP
List<int> numbers = new() { 1, 2, 3 };

var query = numbers.Where(number => number > 1);

numbers.Add(4);

foreach (int number in query)
{
    Console.WriteLine(number);
}

ToList and Immediate Execution

Methods such as ToList and ToArray force a LINQ query to execute immediately and store the resulting values.

CSHARP
var result = numbers
    .Where(number => number > 10)
    .ToList();

Console.WriteLine(result.Count);

LINQ with Objects

LINQ to Objects allows developers to query in-memory collections such as List, arrays, and other IEnumerable sequences.

CSHARP
var products = new List<Product>
{
    new Product("Laptop", 75000),
    new Product("Mouse", 1500),
    new Product("Monitor", 12000)
};

var expensiveProducts = products
    .Where(product => product.Price > 10000)
    .OrderBy(product => product.Price);

foreach (var product in expensiveProducts)
{
    Console.WriteLine(product.Name);
}

class Product
{
    public string Name { get; }
    public decimal Price { get; }

    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }
}

Common LINQ Mistakes

  • Enumerating the same expensive query repeatedly
  • Using First when no matching element is guaranteed
  • Forgetting that many LINQ operations use deferred execution
  • Calling ToList too early and creating unnecessary collections
  • Writing overly complex LINQ chains that are difficult to maintain
  • Using LINQ when a simple loop would be clearer or more efficient

LINQ Best Practices

  • Keep query pipelines readable
  • Use meaningful variable names
  • Materialize results only when necessary
  • Use Any when checking whether matching elements exist
  • Choose FirstOrDefault when an element may not exist
  • Avoid repeated enumeration of expensive sequences
  • Prefer clarity over overly clever one-line queries

Real-World Applications

  • Filtering products
  • Searching user records
  • Sorting application data
  • Generating reports
  • Grouping sales information
  • Transforming API responses
  • Processing collections in business applications

Practice Exercises

  • Filter a list of numbers using Where
  • Transform values using Select
  • Sort objects with OrderBy
  • Group employees by department
  • Calculate totals using Sum and Average
  • Remove duplicates using Distinct
  • Flatten nested collections using SelectMany
  • Build a complete LINQ query pipeline

Conclusion

LINQ provides a powerful and readable way to work with data in C#. By combining filtering, projection, sorting, grouping, aggregation, and other operations, developers can process collections with less repetitive code while keeping data-processing logic expressive.

Note: Note: LINQ is powerful, but readability and execution behavior still matter. Understand deferred execution and avoid unnecessarily complex query chains.