last modified May 14, 2025
This article explores how to use the LINQ let clause to introduce
intermediate variables within query expressions, improving readability,
efficiency, and maintainability.
The let clause allows developers to define temporary
variables within a LINQ query. These variables store computed values,
reducing redundant calculations and enhancing the clarity of complex queries.
Instead of repeating expressions multiple times, you can assign results to a
let variable and reference it throughout the query.
Benefits of using let:
The let clause is commonly used in LINQ queries to store
intermediate results such as calculated values, transformed data, or formatted
output. It can simplify filtering conditions, aid in grouping operations, and
improve the overall clarity of data processing tasks.
While the let clause enhances query efficiency, it should be used
judiciously. Variables introduced via let are computed once per
iteration, meaning they are not stored persistently but recalculated for each
item in the collection. This makes it ideal for lightweight computations but may
not be suitable for scenarios requiring frequent value modification.
The simplest use of let creates a temporary variable to avoid
repeating calculations.
string[] words = ["apple", "banana", "cherry", "date", "elderberry"];
var query = from word in words
let length = word.Length
where length > 5
select new { Word = word, Length = length };
foreach (var item in query)
{
Console.WriteLine($"{item.Word} - {item.Length} letters");
}
We calculate word lengths once and reuse the value in both the where clause and the final projection.
let length = word.Length
The let clause creates a temporary variable length
that can be used throughout the rest of the query.
$ dotnet run banana - 6 letters cherry - 6 letters elderberry - 10 letters
let can store the results of more complex expressions for reuse.
List<Product> products =
[
new("Laptop", 999.99m, 5),
new("Phone", 699.99m, 10),
new("Tablet", 349.99m, 3),
new("Monitor", 249.99m, 7)
];
var query = from p in products
let totalValue = p.Price * p.Stock
where totalValue > 2000
orderby totalValue descending
select new { p.Name, TotalValue = totalValue.ToString("C") };
foreach (var product in query)
{
Console.WriteLine($"{product.Name}: {product.TotalValue}");
}
record Product(string Name, decimal Price, int Stock);
We calculate the total inventory value for each product and use it in multiple clauses.
let totalValue = p.Price * p.Stock
The intermediate calculation is performed once but used in both the filtering and sorting operations.
$ dotnet run Phone: 6,999ドル.90 Laptop: 4,999ドル.95 Monitor: 1,749ドル.93
let is particularly useful when working with string operations.
List<string> names =
[
"John Smith",
"Alice Johnson",
"Robert Brown",
"Emily Davis",
"Michael Wilson"
];
var query = from name in names
let parts = name.Split(' ')
let firstName = parts[0]
let lastName = parts[1]
let initials = $"{firstName[0]}{lastName[0]}"
select new { FullName = name, Initials = initials };
foreach (var person in query)
{
Console.WriteLine($"{person.Initials}: {person.FullName}");
}
We break down names into components and create initials without repeating string operations.
let parts = name.Split(' ')
let firstName = parts[0]
let lastName = parts[1]
let initials = $"{firstName[0]}{lastName[0]}"
Multiple let clauses create a pipeline of transformations, each
building on the previous ones.
$ dotnet run JS: John Smith AJ: Alice Johnson RB: Robert Brown ED: Emily Davis MW: Michael Wilson
let can store the results of method calls to avoid repeated
execution.
List<DateTime> dates =
[
new DateTime(2023, 1, 15),
new DateTime(2023, 3, 22),
new DateTime(2023, 6, 8),
new DateTime(2023, 9, 30),
new DateTime(2023, 12, 5)
];
var query = from date in dates
let quarter = GetQuarter(date)
where quarter > 2
group date by quarter into dateGroup
select new { Quarter = dateGroup.Key, Dates = dateGroup };
foreach (var group in query)
{
Console.WriteLine($"Quarter {group.Quarter}:");
foreach (var date in group.Dates)
{
Console.WriteLine($" {date:d}");
}
}
static int GetQuarter(DateTime date) => (date.Month - 1) / 3 + 1;
We calculate the quarter for each date once and use it for both filtering and grouping.
let quarter = GetQuarter(date)
The method result is cached in the quarter variable, ensuring the
calculation happens only once per element.
$ dotnet run Quarter 3: 30. 9. 2023 Quarter 4: 5. 12. 2023
You can use let to create anonymous types with multiple computed
properties in a query.
string[] words = ["mountain", "river", "forest", "valley", "desert"];
var query = from word in words
let upper = word.ToUpper()
let reversed = new string([.. word.Reverse()])
select new { Original = word, Upper = upper, Reversed = reversed };
foreach (var item in query)
{
Console.WriteLine($"{item.Original} | {item.Upper} | {item.Reversed}");
}
This example uses let to store both the uppercase and reversed
versions of each word, then projects them into an anonymous type.
The let clause can help flatten and filter nested collections, such
as students and their grades.
var students = new[]
{
new { Name = "Anna", Grades = new[] { 90, 85, 92 } },
new { Name = "Ben", Grades = new[] { 78, 81, 86 } },
new { Name = "Cara", Grades = new[] { 88, 94, 91 } }
};
var query = from student in students
let highGrades = student.Grades.Where(g => g >= 90)
where highGrades.Any()
select new { student.Name, HighGrades = highGrades };
foreach (var s in query)
{
Console.WriteLine($"{s.Name}: {string.Join(", ", s.HighGrades)}");
}
Here, let is used to extract high grades for each student, and only
students with at least one high grade are included in the result.
let differs from multiple from clauses in how it
affects the query structure.
List<List<int>> numberLists =
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Using multiple from clauses (cross join)
var query1 = from list in numberLists
from number in list
where number % 2 == 0
select number;
// Using let to reference the inner list
var query2 = from list in numberLists
let evenNumbers = list.Where(n => n % 2 == 0)
where evenNumbers.Any()
select new { List = list, Evens = evenNumbers };
Console.WriteLine("Multiple from clauses (flattened):");
foreach (var num in query1) Console.WriteLine(num);
Console.WriteLine("\nUsing let (structured):");
foreach (var item in query2)
{
Console.WriteLine($"List: [{string.Join(", ", item.List)}]");
Console.WriteLine($"Even numbers: [{string.Join(", ", item.Evens)}]");
}
let preserves the original structure while multiple
from clauses flatten the results.
let evenNumbers = list.Where(n => n % 2 == 0)
The let clause maintains the relationship between each list and its
even numbers.
$ dotnet run Multiple from clauses (flattened): 2 4 6 8 Using let (structured): List: [1, 2, 3] Even numbers: [2] List: [4, 5, 6] Even numbers: [4, 6] List: [7, 8, 9] Even numbers: [8]
In this article we showed how to use the LINQ let clause to create
intermediate variables that improve query readability and performance.
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.
List all C# tutorials.