Language Integrated Query (LINQ) embeds query capabilities directly into the C# syntax, eliminating the impedance mismatch between programming languages and data formats. Prior to its introduction, developers relied on string-based query languages without compile-time validation or IDE assistance, requiring distinct syntaxes for SQL databases, XML structures, and in-memory collections. LINQ unnifies these approaches by treating queries as native language constructs comparable to methods or classes.
Every LINQ operation follows a three-phase pipeline: acquiring the data origin, constructing the expression tree, and materializing results. Importantly, defining a query variable does not trigger data retrieval; execution remains decoupled from declaration until explicitly invoked.
Queryable Data Origins
LINQ requires data sources implementing the generic IEnumerable<T> interface or derivatives such as IQueryable<T>. These contracts enable the iterator pattern required by language constructs like foreach. Types conforming to these interfaces are termed queryable.
When utilizing Object-Relational Mapping tools like Entity Framework, entity classes map to database tables, returning IQueryable<T> instances that extend IEnumerable<T>. The underlying provider translates expression trees into database-specific commands during execution.
var inventoryContext = new InventoryDbContext(connectionString);
IQueryable<Product> discontinuedItems =
from item in inventoryContext.Products
where item.IsDiscontinued && item.StockLevel == 0
select item;
Query Construcsion
A query definition specifies extraction criteria, projection shapes, and ordering rules without immediately processing data. C# query syntax utilizes declarative clauses including from, where, and select. Note the syntactic ordering differs from SQL: the from clause establishes scope first, followed by predicates in where, and finally projection via select.
The query variable serves as a blueprint containing the execution plan but performs no computation until enumerated.
Execution Strategies
Standard query operators exhibit two primary execution behaviors: eager (immediate) and lazy (deferred). Deferred operators further subdivide into streaming and non-streaming variants based on memory consumption patterns.
Eager Evaluation
Eager operators materialize results instantaneously, typically returning scalar values or cached collections. Operations such as Count(), Max(), First(), and ToList() force immediate execution against the data source.
Once executed, the results become static snapshots independent of subsequent source modifications. The following example calculates the total count of premium-tier products:
var premiumQuery = from product in inventoryContext.Products
where product.PriceTier == PriceCategory.Premium
select product;
int premiumCount = premiumQuery.Count();
To cache entire sequences for reuse, materialize results into concrete collections:
List<Product> cachedPremium = (from product in inventoryContext.Products
where product.PriceTier == PriceCategory.Premium
select product).ToList();
// Alternative array materialization:
Product[] premiumArray = (from product in inventoryContext.Products
where product.PriceTier == PriceCategory.Premium
select product).ToArray();
Lazy Evaluation
Lazy operators postpone execution until the query is iterated, such as within a foreach construct. The expression tree remains unexecuted, allowing the query to reflect the data source's state at enumeration time rather than definition time.
The following iteration demonstrates deferred execution:
foreach (Product item in premiumQuery)
{
Console.WriteLine($"{item.Sku}: {item.Description}");
}