44

How can I rewrite this linq query to Entity on with lambda expression?
I want to use let keyword or an equivalent in my lambda expression.

var results = from store in Stores
              let AveragePrice =  store.Sales.Average(s => s.Price)
              where AveragePrice < 500 && AveragePrice > 250

For some similar questions like what is commented under my question, it's suggested to

.Select(store=> new { AveragePrice = store.Sales.Average(s => s.Price), store})

which will calculate AveragePrice for each item, while in Query style I mentioned, let expression prevents to calculate average many times.

Reza Owliaei
  • 3,293
  • 7
  • 35
  • 55
  • 2
    possible duplicate of [Code equivalent to the 'let' keyword in chained LINQ extension method calls](http://stackoverflow.com/questions/1092687/code-equivalent-to-the-let-keyword-in-chained-linq-extension-method-calls) – Eranga Feb 11 '12 at 13:04
  • @Eranga: I that Question, Marc had select animalName.Length for each item. Here, I don't want to calculate Average of all items, for every item. – Reza Owliaei Feb 11 '12 at 14:24
  • 1
    @Reza: the average is computed just once per store object, exactly as in your query... – digEmAll Feb 11 '12 at 14:49

4 Answers4

48

So, you can use the extension method syntax, which would involve one lambda expression more than you are currently using. There is no let, you just use a multi-line lambda and declare a variable:

var results = Stores.Where(store => 
{
    var averagePrice = store.Sales.Average(s => s.Price);
    return averagePrice > 250 && averagePrice < 500;
});

Note that I changed the average price comparison, because yours would never return any results (more than 500 AND less that 250).

The alternative is

var results = Stores.Select(store => new { Store = store, AveragePrice = store.Sales.Average(s => s.Price})
    .Where(x => x.AveragePrice > 250 && x.AveragePrice < 500)
    .Select(x => x.Store);
Jay
  • 56,361
  • 10
  • 99
  • 123
  • 9
    Error: A lambda expression with a statement body cannot be converted to an expression tree. – Reza Owliaei Feb 11 '12 at 14:09
  • 5
    Your first offer just works on memory and could not be used in LINQ providers like EF. A lambda expression with a statement body cannot be converted to an expression tree. – Amir Karimi Feb 11 '12 at 14:12
  • @Jay: AvaragePrice of all items is being calculate for each item according to the second alternatice. I want use something like *let* to stop repeating this calculation. – Reza Owliaei Feb 11 '12 at 14:33
  • 2
    @amkh: I'm almost sure EF was not mentioned in the first version of the question. Or at least neither Jay nor me have noticed that... – digEmAll Feb 11 '12 at 14:44
  • 1
    @Reza: the average is computed just once per store object, exactly as in your query... – digEmAll Feb 11 '12 at 14:48
  • @digEmAll: I'd been tagged it as entity-framework, but sorry for my hidden message! – Reza Owliaei Feb 11 '12 at 14:54
  • 1
    @Jay: I test this solution to improve my query, but as result, it takes 3x longer! The query is very long and needs to explain my data model here. Are you sure that it will improve my queriy performance? – Reza Owliaei Feb 15 '12 at 16:37
  • 1
    WOW! I take a look at Sql profiler. Amazing! Linq query with _Let_ implementation contains 40 joins while the normal query already written contains 24 joins. – Reza Owliaei Feb 15 '12 at 16:58
  • 2
    @Reza No, I wouldn't expect it to necessarily improve EF query performance. The answer is just about how to do a `let`-like operation with the extension method syntax. Why not just stick with the query syntax for this? – Jay Feb 15 '12 at 17:01
  • Just to reduce joins and improve performance. I think it's optimal and just requires some indexes. – Reza Owliaei Feb 15 '12 at 17:12
  • Let is very useful for performance when you are using EF and selecting several elements out of a navigation property. If you don't use let, the subselect will be performed three times. If you use let to create an anonymous type with the navigation property, the subselect will only perform once. http://blog.falafel.com/you-can-use-linqs-let-keyword-to-tune-the-emitted-sql/ – Michael Blackburn Mar 16 '16 at 18:48
27

Basically, you need to use Select and an anonymous type to add the average to your object, followed by the rest of your statement.

Not tested but it should look like this:

Stores.Select(
    x => new { averagePrice = x.Sales.Average(s => s.Price), store = x})
.Where(y => y.averagePrice > 500 && y.averagePrice < 250)
.Select(x => x.store);

Warning: This works well for Linq-to-Entities, but be careful with these constructs in Linq-to-Objects. Using let creates a new anonymous type per object in your collection, it consumes a lot of memory with large collections.

Look here for details: Let in chained extension methods

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Yoeri
  • 1,876
  • 15
  • 24
4

Another option is to define this extension method:

public static class Functional
{
    public static TResult Pipe<T, TResult>(this T value, Func<T, TResult> func)
    {
        return func(value);
    }
}    

Then write your query like this:

var results = Stores
    .Where(store => store.Sales.Average(s => s.Price)
        .Pipe(averagePrice => averagePrice < 500 && averagePrice > 250));
Timothy Shields
  • 75,459
  • 18
  • 120
  • 173
1

We can avoid the overhead of the lambda used in all the other answers with an inline out declaration:

public static class FunctionalExtensions
{
    public static T Assign<T>(this T o, out T result) =>
        result = o;
}

And call it like this

var results = Stores
    .Where(store => store.Sales
        .Average(s => s.Price)
        .Assign(out var averagePrice) < 500 && averagePrice > 250);
daw
  • 1,959
  • 1
  • 20
  • 25