C# LINQ let
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:
- Improved Readability: Simplifies complex expressions by breaking them into smaller, manageable components.
- Enhanced Performance: Reduces redundant calculations and improves query execution efficiency.
- Greater Flexibility: Enables further transformations, filtering, and grouping within queries.
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.
C# LINQ let basic example
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
C# LINQ let with complex expressions
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
C# LINQ let with string manipulation
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
C# LINQ let with method calls
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
C# LINQ let with anonymous types
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.
C# LINQ let with nested collections
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.
C# LINQ let vs multiple from clauses
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]
Source
In this article we showed how to use the LINQ let clause to create
intermediate variables that improve query readability and performance.
Author
List all C# tutorials.