Skip to content

C# LINQ — IEnumerable, Where, Select, GroupBy, OrderBy, and Aggregation

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about C# LINQ. We cover key concepts, practical examples, and best practices to help you master this topic.

C# LINQ (Language Integrated Query) brings SQL-like query capabilities directly into C# code, enabling declarative data manipulation over any IEnumerable source through extension methods and query expressions.

What You'll Learn

You will master LINQ in C#: query syntax vs method syntax, filtering with Where, transforming with Select, sorting with OrderBy/ThenBy, grouping with GroupBy, aggregating with Sum/Average/Count, and understanding deferred execution in .NET.

Why It Matters

LINQ is one of the most transformative features in C#. It replaces verbose loops with declarative queries, reduces bugs, and makes code more readable. Any data source that implements IEnumerable — arrays, lists, dictionaries, files, databases (via EF Core), XML, JSON — can be queried with LINQ. Mastering LINQ is essential for productive C# development.

Real-World Use

EF Core queries translate LINQ to SQL for database access. REST APIs use LINQ to filter, sort, and project data before returning responses. Data processing pipelines chain LINQ operations for ETL workflows. Reporting systems aggregate data with LINQ. Background services filter and batch Process items using LINQ.

Learning Path

graph LR
    A["20: Exception Handling"] --> B["21: LINQ"]
    B --> C["22: LINQ Advanced"]
    C --> D["23: Delegates & Events"]
    D --> E["24: Lambdas"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style E fill:#4a90d9,stroke:#2c5f8a,color:#fff

Data Setup

var products = new List<Product>
{
    new() { Id = 1, Name = "Laptop", Category = "Electronics", Price = 999.99m, Stock = 15 },
    new() { Id = 2, Name = "Mouse", Category = "Electronics", Price = 29.99m, Stock = 100 },
    new() { Id = 3, Name = "Desk", Category = "Furniture", Price = 299.99m, Stock = 10 },
    new() { Id = 4, Name = "Chair", Category = "Furniture", Price = 199.99m, Stock = 25 },
    new() { Id = 5, Name = "Monitor", Category = "Electronics", Price = 399.99m, Stock = 8 },
    new() { Id = 6, Name = "Notebook", Category = "Stationery", Price = 4.99m, Stock = 500 },
    new() { Id = 7, Name = "Pen Set", Category = "Stationery", Price = 12.99m, Stock = 200 },
};

class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

Method Syntax vs Query Syntax

// Method syntax (fluent)
var cheapProducts = products
    .Where(p => p.Price < 100)
    .OrderBy(p => p.Price)
    .Select(p => p.Name);

// Query syntax (SQL-like)
var cheapQuery = from p in products
                 where p.Price < 100
                 orderby p.Price
                 select p.Name;

// Both produce the same result
Console.WriteLine("Products under $100:");
foreach (var name in cheapProducts)
    Console.WriteLine($"  {name}");

Where (Filtering)

// Multiple conditions
var filtered = products
    .Where(p => p.Category == "Electronics" && p.Stock > 10);

// Filter by index
var everyOther = products.Where((p, index) => index % 2 == 0);

// OfType<T> filters by type
var mixed = new List<object> { 1, "hello", 2, "world", 3 };
var strings = mixed.OfType<string>();  // "hello", "world"

Select (Projection)

// Simple projection
var names = products.Select(p => p.Name);

// Anonymous type projection
var summaries = products.Select(p => new
{
    p.Name,
    p.Price,
    p.Category
});

// Indexed projection
var numbered = products.Select((p, i) => $"{i + 1}. {p.Name}");

// SelectMany (flattening)
int[][] numbers = { new[] { 1, 2 }, new[] { 3, 4 }, new[] { 5, 6 } };
var all = numbers.SelectMany(n => n);  // 1, 2, 3, 4, 5, 6

OrderBy / ThenBy (Sorting)

var sorted = products
    .OrderBy(p => p.Category)           // Primary sort
    .ThenByDescending(p => p.Price)      // Secondary sort
    .ThenBy(p => p.Name);                // Tertiary sort

Console.WriteLine("Products sorted by category, then price descending:");
foreach (var p in sorted)
    Console.WriteLine($"  {p.Category}: {p.Name} (${p.Price})");

GroupBy

var grouped = products.GroupBy(p => p.Category);

foreach (var group in grouped)
{
    Console.WriteLine($"\n{group.Key} ({group.Count()} items):");
    Console.WriteLine($"  Total stock: {group.Sum(p => p.Stock)}");
    Console.WriteLine($"  Avg price: {group.Average(p => p.Price):C}");

    foreach (var product in group)
        Console.WriteLine($"    {product.Name} - ${product.Price}");
}

Aggregation

Console.WriteLine($"Total products: {products.Count()}");
Console.WriteLine($"Total stock: {products.Sum(p => p.Stock)}");
Console.WriteLine($"Average price: {products.Average(p => p.Price):C}");
Console.WriteLine($"Max price: {products.Max(p => p.Price):C}");
Console.WriteLine($"Min price: {products.Min(p => p.Price):C}");

// Aggregate with custom logic
var allNames = products.Aggregate("", (acc, p) => acc == "" ? p.Name : acc + ", " + p.Name);
Console.WriteLine($"All names: {allNames}");

Expected output:

Total products: 7
Total stock: 858
Average price: $278.28
Max price: $999.99
Min price: $4.99
All names: Laptop, Mouse, Desk, Chair, Monitor, Notebook, Pen Set

Deferred Execution

var query = products.Where(p =>
{
    Console.WriteLine($"  Checking: {p.Name}");
    return p.Price > 100;
});

Console.WriteLine("Query defined. No execution yet.");
Console.WriteLine("Materializing with ToList():");

var result = query.ToList();  // Execution happens here

Expected output:

Query defined. No execution yet.
Materializing with ToList():
  Checking: Laptop
  Checking: Mouse
  Checking: Desk
  Checking: Chair
  Checking: Monitor
  Checking: Notebook
  Checking: Pen Set

Immdediate Execution

// These methods execute immediately:
var list = products.Where(p => p.Price > 100).ToList();
var array = products.Select(p => p.Name).ToArray();
var dict = products.ToDictionary(p => p.Id);
var lookup = products.ToLookup(p => p.Category);
var hashSet = products.Where(p => p.Stock > 10).ToHashSet();

// Singleton results also execute immediately
var first = products.First(p => p.Name == "Laptop");
var last = products.Last();
var single = products.Single(p => p.Id == 3);
var count = products.Count();
var any = products.Any(p => p.Stock == 0);
var all = products.All(p => p.Price > 0);

Common LINQ Methods Reference

Method Purpose Example
Where Filter products.Where(p => p.Price > 100)
Select Transform products.Select(p => p.Name)
OrderBy Sort ascending products.OrderBy(p => p.Price)
OrderByDescending Sort descending products.OrderByDescending(p => p.Price)
ThenBy Secondary sort products.OrderBy(p => p.Category).ThenBy(p => p.Name)
GroupBy Group products.GroupBy(p => p.Category)
First First element products.First(p => p.Name == "Laptop")
FirstOrDefault First or null products.FirstOrDefault(p => p.Name == "Tablet")
Single Exactly one products.Single(p => p.Id == 3)
Any Check existence products.Any(p => p.Stock == 0)
All Check all products.All(p => p.Price > 0)
Count Count elements products.Count(p => p.Category == "Electronics")
Sum Sum values products.Sum(p => p.Stock)
Average Average values products.Average(p => p.Price)
Min/Max Extremum products.Min(p => p.Price)
Distinct Unique elements products.Select(p => p.Category).Distinct()
Take First N products.Take(3)
Skip Skip N products.Skip(3)
TakeWhile While condition products.TakeWhile(p => p.Price > 50)

Common Mistakes

Mistake 1: Multiple Enumeration

// BAD: enumerates twice
var query = products.Where(p => p.Price > 100);
var count = query.Count();
var first = query.First();

// GOOD: materialize once
var materialized = products.Where(p => p.Price > 100).ToList();
var count = materialized.Count;
var first = materialized.First();

Mistake 2: Confusing First and FirstOrDefault

First throws if no match. FirstOrDefault returns default (null for ref types, 0 for value types). Use First when you know the element exists.

Mistake 3: Using Count() > 0 Instead of Any()

Any() returns true as soon as it finds a match. Count() > 0 iterates the entire collection. For IEnumerable, Any() is more efficient.

Mistake 4: Order of Operations Matters

Where(p => ...).OrderBy(p => ...).Take(10) is more efficient than OrderBy(p => ...).Where(p => ...).Take(10) because ordering first on all elements is wasteful.

Mistake 5: Not Using Select for Performance

products.Select(p => new { p.Name, p.Price }) only transfers needed fields. Without Select, all columns/properties are transferred.

Mistake 6: Modifying Source While Iterating a LINQ Query

Changing the source collection while iterating a LINQ query throws InvalidOperationException. Materialize the query first.

Practice Questions

  1. What is the difference between method syntax and query syntax in LINQ?
  2. How does deferred execution affect when a query runs?
  3. When would you use FirstOrDefault instead of First?
  4. Write a LINQ query that groups products by category and finds the most expensive product in each category.
  5. What is the difference between Select and SelectMany?

Challenge

Given a list of orders, use LINQ to find the top 3 customers by total order value. Each order has a CustomerName, OrderDate, and List of OrderItems (each with ProductName, Quantity, UnitPrice). Return customer name and total spent.

FAQ

What is the difference between LINQ to Objects and LINQ to Entities?

LINQ to Objects works with in-memory collections (IEnumerable). LINQ to Entities (EF Core) translates queries to SQL and executes on the database server. The same query syntax works for both.

Is it better to use query syntax or method syntax?

Method syntax is more common and flexible. Query syntax is more readable for complex joins and grouping. Both compile to the same IL. Use whichever is clearer for the specific query.

Why does my LINQ query return different results on second iteration?

If the source is a live collection (like a database query) or a deferred IEnumerable, the query executes fresh each time. Materialize with ToList() for consistent results.

Can I use LINQ with other data sources besides collections?

Yes. LINQ providers exist for databases (EF Core), XML (LINQ to XML), datasets (LINQ to DataSet), and any source via custom IQueryable providers.

What is the performance impact of LINQ?

LINQ adds minor overhead compared to handwritten loops but is rarely a bottleneck. The productivity gain and reduced bug rate far outweigh the performance cost. Use handwritten loops only for hot paths identified by profiling.

Mini Project

Create a product analysis dashboard:

// Calculate various statistics
var stats = new
{
    TotalProducts = products.Count,
    Categories = products.Select(p => p.Category).Distinct().Count(),
    TotalValue = products.Sum(p => p.Price * p.Stock),
    AveragePrice = products.Average(p => p.Price),

    // Top 3 by stock value
    TopProducts = products
        .OrderByDescending(p => p.Price * p.Stock)
        .Take(3)
        .Select(p => $"{p.Name} (${p.Price * p.Stock:N0})"),

    // Category summary
    CategorySummary = products
        .GroupBy(p => p.Category)
        .Select(g => new
        {
            Category = g.Key,
            ProductCount = g.Count(),
            TotalStock = g.Sum(p => p.Stock),
            AvgPrice = g.Average(p => p.Price)
        }),

    // Products needing restock (stock < 10)
    LowStock = products
        .Where(p => p.Stock < 10)
        .OrderBy(p => p.Stock)
        .Select(p => $"{p.Name}: {p.Stock} units"),

    // Price tiers
    PriceTiers = products
        .GroupBy(p => p.Price switch
        {
            < 50 => "Budget",
            < 200 => "Mid-range",
            < 500 => "Premium",
            _ => "Luxury"
        })
        .Select(g => $"{g.Key}: {g.Count()} products (avg ${g.Average(p => p.Price):N2})")
};

Console.WriteLine("Product Dashboard");
Console.WriteLine("=================");
Console.WriteLine($"Total: {stats.TotalProducts} products");
Console.WriteLine($"Categories: {stats.Categories}");
Console.WriteLine($"Inventory value: {stats.TotalValue:C}");
Console.WriteLine($"Average price: {stats.AveragePrice:C}");

Console.WriteLine("\nTop 3 Products by Value:");
foreach (var p in stats.TopProducts)
    Console.WriteLine($"  {p}");

Console.WriteLine("\nCategory Summary:");
foreach (var c in stats.CategorySummary)
    Console.WriteLine($"  {c.Category}: {c.ProductCount} items, {c.TotalStock} stock, avg {c.AvgPrice:C}");

Console.WriteLine("\nLow Stock Products:");
foreach (var p in stats.LowStock)
    Console.WriteLine($"  {p}");

Console.WriteLine("\nPrice Tiers:");
foreach (var t in stats.PriceTiers)
    Console.WriteLine($"  {t}");

Expected output:

Product Dashboard
=================
Total: 7 products
Categories: 3
Inventory value: $21,343.65
Average price: $278.28

Top 3 Products by Value:
  Laptop ($14,999.85)
  Monitor ($3,199.92)
  Chair ($4,999.75)

Category Summary:
  Electronics: 3 items, 123 stock, avg $476.66
  Furniture: 2 items, 35 stock, avg $249.99
  Stationery: 2 items, 700 stock, avg $8.99

Low Stock Products:
  Monitor: 8 units

Price Tiers:
  Budget: 3 products (avg $15.99)
  Mid-range: 2 products (avg $249.99)
  Premium: 1 products (avg $399.99)
  Luxury: 1 products (avg $999.99)

What's Next

You have mastered basic LINQ operations. The next lesson covers advanced LINQ: Join, GroupJoin, Zip, Aggregation, and query syntax in depth.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro