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.