microsoft7 min read

LINQ Tutorial: Learn Data Querying from Scratch (2026)

LINQ Tutorial: Learn Data Querying from Scratch (2026)

Published:  |  Category: Microsoft  |  Reading time: ~15 min
LINQ Tutorial: Learn Data Querying from Scratch (2026)

The moment LINQ clicked for me was during a code review of a legacy project. There was this monstrous loop with nested conditions, temporary collections, and flag variables — about forty lines to filter, sort, and group a list of invoices. I rewrote the entire thing in five lines of LINQ. My colleague stared at the screen, asked if it was magic, and then asked to learn LINQ on the spot. That is the power of Language Integrated Query — it transforms how you think about data manipulation.

LINQ (Language Integrated Query) is a set of methods and query syntax built into .NET that lets you query any data source — collections, databases, XML, JSON — using a consistent, type-safe syntax. Whether you are filtering a list of users, joining orders to customers, or aggregating sales by region, LINQ makes the code cleaner, more readable, and less error-prone. This tutorial covers the essential operators and patterns I use in nearly every project.

LINQ Fundamentals: IEnumerable and IQueryable

At its core, LINQ operates on sequences that implement IEnumerable or IQueryable. IEnumerable represents an in-memory collection where LINQ operators execute locally via delegates. IQueryable represents a query against a remote data source like a database — operators build an expression tree that is translated into SQL (or another query language) and executed server-side. Understanding this distinction is crucial for performance: filtering with Where on an IQueryable happens in the database, while filtering on an IEnumerable loads everything into memory first.

LINQ operators are categorized as streaming or non-streaming. Streaming operators like Where and Select process one element at a time and can produce results without consuming the entire source. Non-streaming operators like OrderBy and GroupBy require reading all elements before producing any output. This affects memory usage and latency for large datasets.

List numbers = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
var squared = numbers.Select(n => n * n);
var sum = numbers.Aggregate((acc, n) => acc + n);
var firstMatch = numbers.FirstOrDefault(n => n > 7);

Filtering, Projection, and Sorting

The three most common LINQ operations are filtering (Where), projection (Select), and sorting (OrderBy/ThenBy). Where takes a predicate function that returns true for elements you want to keep. Select transforms each element into a new form — you can select a single property, compute a derived value, or project into an anonymous type. OrderBy sorts ascending, OrderByDescending sorts descending, and ThenBy/ThenByDescending add secondary sort criteria.

These operators compose naturally. A typical data pipeline might filter inactive users, project to a view model, sort by last name, and take the first 20 results. Method syntax (chaining methods) and query syntax (SQL-like expressions) are functionally equivalent. I prefer method syntax for its clarity with lambdas, but query syntax shines for complex joins and let clauses.

var result = users
    .Where(u => u.IsActive && u.LastLogin > DateTime.UtcNow.AddDays(-30))
    .OrderBy(u => u.LastName)
    .ThenBy(u => u.FirstName)
    .Select(u => new UserSummary
    {
        Id = u.Id,
        Name = $"{u.FirstName} {u.LastName}",
        Email = u.Email,
        LoginCount = u.LoginCount
    })
    .Skip(20)
    .Take(10)
    .ToList();

Joining and Grouping Data

LINQ provides Join and GroupJoin operators for combining related sequences. Join performs an inner join based on key selectors, similar to SQL's INNER JOIN. GroupJoin produces a hierarchical result — each element from the outer sequence paired with a collection of matching inner elements, analogous to a SQL left join with grouping. For in-memory joins, LINQ uses hash-based lookup for O(n+m) performance rather than nested loops.

GroupBy partitions a sequence into groups based on a key selector. Each group has a Key property and is itself an IEnumerable of elements. This is invaluable for aggregations like counting products per category or summing sales per month. The result of GroupBy can be further projected with Select to produce flat summaries.

var orderDetails = orders
    .Join(orderItems,
        order => order.Id,
        item => item.OrderId,
        (order, item) => new { order.CustomerName, item.Product, item.Price });

var salesByCategory = products
    .GroupBy(p => p.Category)
    .Select(g => new
    {
        Category = g.Key,
        TotalSales = g.Sum(p => p.Price * p.UnitsSold),
        AveragePrice = g.Average(p => p.Price),
        ProductCount = g.Count()
    });

Aggregation and Quantifier Operators

Aggregation operators reduce a sequence to a single value. Count, Sum, Average, Min, and Max handle numeric aggregations. Aggregate is the general-purpose fold operator — you supply a seed and an accumulator function, making it suitable for custom reductions like concatenating strings or running complex calculations. These operators throw on empty sequences; use the OrDefault variants (FirstOrDefault, SingleOrDefault) for safe handling.

Quantifier operators return boolean results. Any checks if at least one element satisfies a condition. All checks if every element satisfies a condition. Contains checks for the presence of a specific element. These operators are short-circuiting — Any stops at the first match, which is useful for early-out validation.

bool anyHighValue = transactions.Any(t => t.Amount > 10000);
bool allVerified = transactions.All(t => t.IsVerified);

var summary = new
{
    TransactionCount = transactions.Count(),
    TotalAmount = transactions.Sum(t => t.Amount),
    AverageAmount = transactions.Average(t => t.Amount),
    MaxAmount = transactions.Max(t => t.Amount),
    MinAmount = transactions.Min(t => t.Amount)
};

string tags = products.Select(p => p.Name).Aggregate((a, b) => $"{a}, {b}");

Deferred Execution and Materialization

Understanding deferred execution is the single most important LINQ concept. Most LINQ operators do not execute when they are defined — they build a query that executes when you enumerate the results. This has profound implications. If you define a Where filter and later modify the source collection before enumeration, the filter operates on the modified data. Multiple enumerations of the same LINQ query execute the pipeline multiple times, potentially causing performance issues or inconsistent results.

Materialization operators — ToList, ToArray, ToDictionary, ToHashSet — execute the query immediately and store the results. Use these to snapshot query results or to avoid repeated enumeration. The ToLookup operator is a materialized, read-only grouping that allows O(1) key-based access. Always cache results with ToList when you need to enumerate multiple times.

IEnumerable query = numbers.Where(n => n > 3);
numbers.Add(42);
var result = query.ToList();

var lookup = products.ToLookup(p => p.Category);
var electronics = lookup["Electronics"];

var cached = context.Products
    .Where(p => p.IsActive)
    .OrderBy(p => p.Name)
    .ToList();

LINQ to XML and LINQ to Entities

LINQ to XML provides a streamlined API for querying and manipulating XML documents. The XDocument and XElement classes support LINQ queries directly, letting you filter, project, and transform XML with the same operators you use on collections. Namespace handling is explicit and less error-prone than the legacy XmlDocument API. LINQ to XML is my go-to tool for configuration files, RSS feeds, and API responses in XML format.

LINQ to Entities is the LINQ provider for Entity Framework Core. Your LINQ queries are translated into SQL by the query pipeline. This translation has limitations — not all C# methods translate to SQL. EF Core throws a runtime exception for untranslatable expressions, or silently switches to client evaluation in older versions. Always review the generated SQL for complex queries, and use the EF Core logging to see exactly what hits the database.

XDocument doc = XDocument.Load("config.xml");
var settings = doc.Descendants("setting")
    .Where(s => (string)s.Attribute("environment") == "production")
    .Select(s => new
    {
        Key = (string)s.Attribute("name"),
        Value = (string)s.Element("value")
    });

var products = context.Products
    .Where(p => p.Price > 50 && p.Category.Name == "Electronics")
    .OrderByDescending(p => p.CreatedAt)
    .Select(p => new { p.Name, p.Price, Category = p.Category.Name })
    .ToList();

Frequently Asked Questions

What is the difference between IEnumerable and IQueryable in LINQ?

IEnumerable runs queries in-memory using delegates. IQueryable builds expression trees that are translated to a target query language like SQL by a provider such as Entity Framework. Always use IQueryable for database queries to ensure filtering happens server-side.

Should I use query syntax or method syntax?

Both are functionally identical. Method syntax with lambda expressions is more concise for filtering, projection, and aggregation. Query syntax is more readable for complex joins and let clauses. Use whichever is clearer for each specific query.

Why does my LINQ query run slowly?

Common causes: client-side evaluation where filtering happens in memory, missing indexes in the database, multiple enumerations causing repeated execution, or non-streaming operators like OrderBy on large datasets. Use ToList to materialize once and check the generated SQL.

How do I handle null values in LINQ queries?

Use the null-conditional operator (?.) before accessing properties, use Where to filter out nulls first, and prefer FirstOrDefault over First when the sequence may be empty. For database queries, EF Core translates null propagation correctly.

Originally published on Ayodhyyya. Last updated June 1, 2026.