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.
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.
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.
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.
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.
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
- Create:
dotnet new console -n LinqQueries
- Navigate:
cd LinqQueries
- Replace Program.cs with the code below
- Run:
dotnet run
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");
<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.