Introduction
In general, Let
allows to create a local variable at the LinQ query. Let
has the same operation as to build local variable in a loop.
This example has analogy between loop and LinQ query:
Let
is allowed only in LinQ queries, so that it can’t use in Lambdas.
Example Code
These are the classes we will use in our examples.
Shop
class:
using System.Collections.Generic;
namespace LetKeyword
{
public class Shop
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<Sale> Sales { get; set; }
}
}
Note: The Shop
class has an IEnumerable<Sale>
property with the associated sales.
Sale
class:
using System;
namespace LetKeyword
{
public class Sale
{
public int Id { get; set; }
public DateTime Date { get; set; }
public decimal Amount { get; set; }
}
}
The ShoppingDB
class is a virtual
DB process:
using System;
using System.Collections.Generic;
namespace LetKeyword
{
public static class ShoppingDB
{
public static IEnumerable<Shop> GetShops()
{
var result = new List<Shop>()
{
new Shop
{
Id = 1,
Name = "Shop 1",
Sales = new List<Sale>()
{
new Sale{ Id = 1, Date = new DateTime(2017,01,02), Amount = 1520m },
new Sale{ Id = 8, Date = new DateTime(2017,01,26), Amount = 500m },
new Sale{ Id = 25, Date = new DateTime(2017,02,15), Amount = 8900m },
new Sale{ Id = 26, Date = new DateTime(2017,02,28), Amount = 40000m },
new Sale{ Id = 39, Date = new DateTime(2017,03,02), Amount = 75000m }
}
},
new Shop
{
Id = 2,
Name = "Shop 2",
Sales = new List<Sale>()
{
new Sale{ Id = 2, Date = new DateTime(2017,01,06), Amount = 10m },
new Sale{ Id = 3, Date = new DateTime(2017,01,08), Amount = 3000m },
new Sale{ Id = 11, Date = new DateTime(2017,02,11), Amount = 100000m },
new Sale{ Id = 12, Date = new DateTime(2017,02,12), Amount = 515000m },
new Sale{ Id = 42, Date = new DateTime(2017,03,12), Amount = 25m },
new Sale{ Id = 43, Date = new DateTime(2017,03,12), Amount = 200m },
new Sale{ Id = 52, Date = new DateTime(2017,03,16), Amount = 300m }
}
},
new Shop
{
Id = 3,
Name = "Shop 3",
Sales = new List<Sale>()
{
new Sale{ Id = 13, Date = new DateTime(2017,02,12), Amount = 2500m },
new Sale{ Id = 14, Date = new DateTime(2017,02,12), Amount = 3000m }
}
},
new Shop
{
Id = 4,
Name = "Shop 4",
Sales = new List<Sale>()
{
new Sale{ Id = 15, Date = new DateTime(2017,01,13), Amount = 79000m },
new Sale{ Id = 16, Date = new DateTime(2017,01,13), Amount = 6000m },
new Sale{ Id = 53, Date = new DateTime(2017,03,17), Amount = 145000m },
new Sale{ Id = 54, Date = new DateTime(2017,03,17), Amount = 5000m },
new Sale{ Id = 55, Date = new DateTime(2017,03,18), Amount = 37800m },
new Sale{ Id = 56, Date = new DateTime(2017,03,19), Amount = 11200m },
new Sale{ Id = 57, Date = new DateTime(2017,03,26), Amount = 22580m },
new Sale{ Id = 58, Date = new DateTime(2017,04,01), Amount = 1000m },
new Sale{ Id = 59, Date = new DateTime(2017,04,02), Amount = 9000m },
new Sale{ Id = 60, Date = new DateTime(2017,04,03), Amount = 990000m },
new Sale{ Id = 61, Date = new DateTime(2017,04,04), Amount = 8000m },
new Sale{ Id = 62, Date = new DateTime(2017,04,05), Amount = 52580m },
new Sale{ Id = 63, Date = new DateTime(2017,04,06), Amount = 558900m },
new Sale{ Id = 64, Date = new DateTime(2017,04,07), Amount = 88900m }
}
}
};
return result;
}
}
}
Benefits of let
The main benefits of let
are:
- Reading compression code
- Encapsulate functionality
- Improvement performance
Reading Compression Code
The let
keyword provides the improvement reading and compression code, because unit calls and calculations transform it in more readable.
In this example, we build a query where the shop has sales in March and the number of sales each shop is pair.
Example without let
:
[TestMethod]
public void ReadingCompressionCode_WithoutLet()
{
var result = from shop in ShoppingDB.GetShops()
where shop.Sales.Any(s => s.Date.Month == 3)
&& shop.Sales.Count() % 2 == 0
select shop;
}
Example with let
:
[TestMethod]
public void ReadingCompressionCode_WithLet()
{
var result = from shop in ShoppingDB.GetShops()
let hasMarchSales = shop.Sales.Any(s => s.Date.Month == 3)
let hasPairSales = shop.Sales.Count() % 2 == 0
where hasMarchSales && hasPairSales
select shop;
}
Comparison:
Encapsulate Functionality
Another let
advantage is encapsulating functionality. If we do good work with let
in our linq queries, we need to only change code in one place in our code for refactorings.
Example without let
:
[TestMethod]
public void EncapsulateFunctionality_WithoutLet()
{
var result = from shop in ShoppingDB.GetShops()
where shop.Sales.Average(a => a.Amount) > 1000
&& shop.Sales.Average(a => a.Amount) < 100000
select shop;
}
Example with let
:
[TestMethod]
public void EncapsulateFunctionality_WithLet()
{
var result = from shop in ShoppingDB.GetShops()
let myAverage = shop.Sales.Average(a => a.Amount)
where myAverage > 1000
&& myAverage < 100000
select shop;
}
Comparison:
With the let
solution, in change case, only we will modify in one place, without let
, we will modify in 2 or N places.
Improvement Performance
In the previous without let
example, we have seen how we wrote the same code two times for the same goal. As we wrote the code two times, the code will be executed two times too with a lost performance.
This problem is greater if our query has a select
extender method, because add another execution.
Example without let
:
[TestMethod]
public void ImprovementPerformance_WithoutLet()
{
var result = from shop in ShoppingDB.GetShops()
where shop.Sales.Average(a => a.Amount) > 1000
&& shop.Sales.Average(a => a.Amount) < 100000
select new
{
Id = shop.Id,
Name = shop.Name,
Sales = shop.Sales,
SalesAverage = shop.Sales.Average(a => a.Amount)
};
}
Example with let
:
[TestMethod]
public void ImprovementPerformance_WithLet()
{
var result = from shop in ShoppingDB.GetShops()
let myAverage = shop.Sales.Average(a => a.Amount)
where myAverage > 1000
&& myAverage < 100000
select new
{
Id = shop.Id,
Name = shop.Name,
Sales = shop.Sales,
SalesAverage = myAverage
};
}
Comparison:
The example with let
has better performance than the without let
example, because the first executes one time and second two times for each item.
We have added two tests to show method performance:
[TestMethod]
public void ImprovementPerformance_WithoutLet3()
{
var stopWatch = new Stopwatch();
stopWatch.Start();
var data = Enumerable.Range(0, 30000 - 1).ToList();
var result = (from s in data
where data.Average(a => a) > s
&& data.Average(a => a) < 100
select new
{
Number = s,
Average = data.Average(a => a)
}).ToList();
stopWatch.Stop();
System.Diagnostics.Trace.WriteLine($"{stopWatch.Elapsed.TotalMilliseconds}");
}
Output --> 17803.3522 milliseconds
[TestMethod]
public void ImprovementPerformance_WithLet4()
{
var stopWatch = new Stopwatch();
stopWatch.Start();
var data = Enumerable.Range(0, 30000 - 1).ToList();
var result = (from s in data
let average = data.Average(a => a)
where average > s
&& average < 100
select new
{
Number = s,
Average = average
}).ToList();
stopWatch.Stop();
System.Diagnostics.Trace.WriteLine($"{stopWatch.Elapsed.TotalMilliseconds}");
}
Output --> 12497.8096 milliseconds
With Let
is faster.