Mastering LINQ Query Keywords in C#

SQL-Like Queries in C#

Picture writing a customer dashboard that needs to filter orders, group by status, and sort by date. You could chain Where().GroupBy().OrderBy() calls, but LINQ query syntax lets you write it more like SQL, making the logic clearer for anyone familiar with databases.

Query keywords like from, where, select, join, and group form a declarative syntax that compiles to the same method chains you'd write manually. The compiler transforms your query into method calls, so there's zero performance difference.

You'll learn the essential query keywords, when query syntax beats method syntax for readability, and how to combine both approaches. By the end, you'll write LINQ that clearly expresses intent without sacrificing power.

The Foundation: from, where, select

Every LINQ query starts with from to declare a range variable and the data source. The where clause filters items, and select projects the final result. These three form the backbone of query syntax.

The from clause introduces a variable that represents each element as LINQ iterates the source. You reference this variable in where conditions and select projections. The compiler translates this into lambda expressions behind the scenes.

BasicQueries.cs
var products = new[]
{
    new { Id = 1, Name = "Laptop", Price = 1200m, InStock = true },
    new { Id = 2, Name = "Mouse", Price = 25m, InStock = true },
    new { Id = 3, Name = "Keyboard", Price = 75m, InStock = false },
    new { Id = 4, Name = "Monitor", Price = 300m, InStock = true }
};

// Query syntax
var affordableInStock = from p in products
                        where p.Price < 500 && p.InStock
                        select p.Name;

// Equivalent method syntax
var affordableMethod = products
    .Where(p => p.Price < 500 && p.InStock)
    .Select(p => p.Name);

foreach (var name in affordableInStock)
    Console.WriteLine(name);

Both queries produce identical results. Query syntax reads top-to-bottom like SQL, while method syntax chains left-to-right. For simple filters, method syntax is often terser, but query syntax shines with complex conditions.

Sorting with orderby, ascending, descending

The orderby keyword sorts results. By default it orders ascending, but you can specify ascending or descending explicitly. You can order by multiple fields by listing them with commas.

Multi-level sorting reads naturally in query syntax: order by category, then by price within each category. Method syntax requires chaining OrderBy().ThenBy() which is less intuitive.

Sorting.cs
var products = new[]
{
    new { Name = "Mouse", Category = "Input", Price = 25m },
    new { Name = "Keyboard", Category = "Input", Price = 75m },
    new { Name = "Monitor", Category = "Display", Price = 300m },
    new { Name = "Webcam", Category = "Input", Price = 50m }
};

// Query syntax with multi-level sort
var sorted = from p in products
             orderby p.Category, p.Price descending
             select new { p.Name, p.Category, p.Price };

// Equivalent method syntax
var sortedMethod = products
    .OrderBy(p => p.Category)
    .ThenByDescending(p => p.Price)
    .Select(p => new { p.Name, p.Category, p.Price });

foreach (var item in sorted)
    Console.WriteLine($"{item.Category}: {item.Name} - ${item.Price}");

The query syntax version reads naturally: order by category, then by price descending. Method syntax achieves the same result but requires knowing the OrderBy/ThenBy distinction. Use query syntax when sorting by multiple fields.

Joining Data with join, on, equals

The join keyword combines two sources based on matching keys. You specify the keys with on and equals. This inner join pattern is common when relating data from different collections.

Query syntax for joins mirrors SQL syntax closely. You declare both sources, specify the join condition, and project the combined result. Method syntax uses Join() which has a more complex lambda signature.

Joins.cs
var customers = new[]
{
    new { Id = 1, Name = "Alice" },
    new { Id = 2, Name = "Bob" },
    new { Id = 3, Name = "Carol" }
};

var orders = new[]
{
    new { OrderId = 101, CustomerId = 1, Total = 250m },
    new { OrderId = 102, CustomerId = 2, Total = 175m },
    new { OrderId = 103, CustomerId = 1, Total = 300m }
};

// Query syntax join
var customerOrders = from c in customers
                     join o in orders on c.Id equals o.CustomerId
                     select new { c.Name, o.OrderId, o.Total };

// Equivalent method syntax
var ordersMethod = customers.Join(
    orders,
    c => c.Id,
    o => o.CustomerId,
    (c, o) => new { c.Name, o.OrderId, o.Total });

foreach (var order in customerOrders)
    Console.WriteLine($"{order.Name}: Order {order.OrderId} = ${order.Total}");

Query syntax makes the join condition explicit with on and equals. Method syntax bundles everything into the Join() parameters. For joins, query syntax usually wins on readability.

Grouping with group, by, into

The group by keywords aggregate items into groups based on a key. The into keyword lets you continue the query with the grouped results, accessing the Key and the grouped items.

Grouping produces IGrouping objects where Key holds the grouping value and the object itself is an enumerable of items in that group. This pattern is powerful for aggregations and summaries.

Grouping.cs
var sales = new[]
{
    new { Product = "Laptop", Category = "Electronics", Amount = 1200m },
    new { Product = "Desk", Category = "Furniture", Amount = 400m },
    new { Product = "Mouse", Category = "Electronics", Amount = 25m },
    new { Product = "Chair", Category = "Furniture", Amount = 200m }
};

// Query syntax grouping
var grouped = from s in sales
              group s by s.Category into categoryGroup
              select new
              {
                  Category = categoryGroup.Key,
                  TotalSales = categoryGroup.Sum(x => x.Amount),
                  ItemCount = categoryGroup.Count()
              };

// Equivalent method syntax
var groupedMethod = sales
    .GroupBy(s => s.Category)
    .Select(g => new
    {
        Category = g.Key,
        TotalSales = g.Sum(x => x.Amount),
        ItemCount = g.Count()
    });

foreach (var group in grouped)
    Console.WriteLine(
        $"{group.Category}: {group.ItemCount} items, ${group.TotalSales}");

The into keyword captures the grouped results so you can aggregate them. Without into, group by must be the final clause. With into, you can continue with more where, orderby, or select operations.

Creating Variables with let

The let keyword introduces intermediate variables in your query. This avoids repeating complex expressions and makes queries more readable by naming intermediate calculations.

Use let to calculate values once and reference them in subsequent clauses. It's especially useful for computed fields you'll use in both where filters and select projections.

LetExamples.cs
var orders = new[]
{
    new { Id = 1, Subtotal = 100m, TaxRate = 0.08m },
    new { Id = 2, Subtotal = 250m, TaxRate = 0.08m },
    new { Id = 3, Subtotal = 75m, TaxRate = 0.08m }
};

// let creates intermediate variable 'total'
var expensiveOrders = from o in orders
                      let total = o.Subtotal * (1 + o.TaxRate)
                      where total > 150
                      select new { o.Id, Total = total };

// Without let, you'd repeat the calculation
var withoutLet = from o in orders
                 where o.Subtotal * (1 + o.TaxRate) > 150
                 select new
                 {
                     o.Id,
                     Total = o.Subtotal * (1 + o.TaxRate)
                 };

foreach (var order in expensiveOrders)
    Console.WriteLine($"Order {order.Id}: ${order.Total:F2}");

The let clause computes total once and uses it in both where and select. This keeps the query DRY and makes the logic clearer by naming the intermediate value.

Try It Yourself

Build a small program that demonstrates the key LINQ query keywords working together. You'll filter, sort, join, and group data using query syntax.

Steps

  1. Create: dotnet new console -n LinqQueries
  2. Navigate: cd LinqQueries
  3. Replace Program.cs with the code below
  4. Run: dotnet run
Program.cs
var students = new[]
{
    new { Id = 1, Name = "Alice", Grade = 'A' },
    new { Id = 2, Name = "Bob", Grade = 'B' },
    new { Id = 3, Name = "Carol", Grade = 'A' }
};

var scores = new[]
{
    new { StudentId = 1, Subject = "Math", Score = 95 },
    new { StudentId = 1, Subject = "English", Score = 88 },
    new { StudentId = 2, Subject = "Math", Score = 82 },
    new { StudentId = 3, Subject = "Math", Score = 92 }
};

var report = from s in students
             join sc in scores on s.Id equals sc.StudentId
             where sc.Score > 85
             orderby sc.Score descending
             select new { s.Name, sc.Subject, sc.Score };

Console.WriteLine("High Scores Report:");
foreach (var item in report)
    Console.WriteLine($"{item.Name} - {item.Subject}: {item.Score}");

var gradeGroups = from s in students
                  group s by s.Grade into g
                  select new { Grade = g.Key, Count = g.Count() };

Console.WriteLine("\nGrade Distribution:");
foreach (var g in gradeGroups)
    Console.WriteLine($"Grade {g.Grade}: {g.Count} students");
LinqQueries.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

Output

High Scores Report:
Alice - Math: 95
Carol - Math: 92
Alice - English: 88

Grade Distribution:
Grade A: 2 students
Grade B: 1 students

This demo shows join, where, orderby, group by, and into keywords working together. Query syntax makes the multi-step logic flow naturally from top to bottom.

Reader Questions

Should I use query syntax or method syntax?

Use query syntax for complex queries with multiple joins, grouping, or ordering—it reads more naturally. Use method syntax for simple filters or when chaining additional operations. Both compile to the same code, so choose based on readability.

Do query keywords impact performance vs method syntax?

No, query syntax is syntactic sugar that compiles to method calls. Performance is identical. The compiler transforms from x in items where x > 5 into items.Where(x => x > 5). Choose based on clarity, not speed.

Can I mix query and method syntax?

Yes, you can chain method calls after query expressions. Start with query syntax for readability, then append methods like Take(), Skip(), or ToList(). This combines the strengths of both approaches.

Why doesn't query syntax support all LINQ operators?

Query syntax covers the most common operations: filtering, projection, joining, grouping, and ordering. Operators like Take, Skip, Distinct, or Aggregate have no query keyword equivalent. Use method syntax for these or chain them after query blocks.

Back to Articles