LINQ in Action - LINQ Book & News

Use the power of let in your LINQ queries

Often, when you try to find out how to write the correct LINQ query you need, you end up being confused because it becomes too complex. In such situations, you should remember that the let clause is here to help you.

Let's see is an example from the official LINQ forum.
Someone asked how to query the following XML document:

<cars>
  <car name="Toyota Coupe">
    <profile name="Vendor" value="Toyota"/>
    <profile name="Model" value="Celica"/>
    <profile name="Doors" value="2"/>
    <support name="Racing" value="yes"/>
    <support name="Towing" value="no"/>
  </car>
  <car name="Honda Accord Aerodec">
    <profile name="Vendor" value="Honda"/>
    <profile name="Model" value="Accord"/>
    <profile name="Doors" value="4"/>
    <support name="Racing" value="no"/>
    <support name="Towing" value="yes"/>
  </car>
</cars>

Here is one way to do it:

from car in root.Elements("car")
let profiles =
  from profile in car.Elements("profile")
  select new {
    Name = profile.Attribute("name").Value,
    Value = profile.Attribute("value").Value
  }
let supports =
  from support in car.Elements("support")
  select new {
    Name = support.Attribute("name").Value,
    Value = support.Attribute("value").Value
  }
select new Car {
  Name = car.Attribute("name").Value,
  Vendor = profiles.Single(prof => prof.Name == "Vendor").Value,
  Model = profiles.Single(prof => prof.Name == "Model").Value,
  Doors = int.Parse(profiles.Single(prof => prof.Name == "Doors").Value),
  RacingSupport = supports.Single(sup => sup.Name == "Racing").Value == "yes"
};

It's easier to isolate the "profile" and "support" elements in separate sequences using let clauses, as above. Then the select clause becomes simple to write.

As Luke Hoban explains on his blog: 

With let query clauses, you can introduce a variable into scope and use it in the subsequent query clauses. Similar to local variables in a method body, this gives you a way to avoid evaluating a common expression multiple times by storing it in a variable. This can be very useful even in much simpler queries. Of course, in the query above - let is absolutely critical.

The "query above" he refers to is a ray tracer coded as a big LINQ query full of let clauses!

Published Wednesday, December 5, 2007 5:56 PM by Fabrice Marguerie

Comments

 

triag said:

Thank you very much for your help in this query. It really looks simply after you show it!

December 5, 2007 7:04 PM
 

Leon said:

I want to show 1 product per category from products and categories tables.

After using let, I got error:

Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.

Any way to do this?

Many thanks

December 19, 2007 2:14 PM
 

Fabrice Marguerie said:

Leon, what's your query?

Which product do you want to show?

December 19, 2007 3:00 PM
 

Leon said:

Here is my query. I want to show the first product under each category.

from c in this.Categories                              

let products =

from p in this.Products

where (p.CategoryID == c.CategoryID)    

select new

{

 ProductName = p.ProductName

}

select new ProductDetail

{

Name = c.CategoryName

ProductName = products.Single().ProductName

}

December 19, 2007 3:18 PM
 

Fabrice Marguerie said:

I think that this is not related to let. This may be because you use Single. Single throws an exception if more than one item is found. You should use First in your case.

Also, you could simplify your query:

1) You don't need to use an anonymous type for the first select:

from c in this.Categories

let productNames =

 from p in this.Products

 where p.CategoryID == c.CategoryID

 select p.ProductName

select new ProductDetail

{

 Name = c.CategoryName,

 ProductName = productNames.First()

}

2) If a category had a Products property, it would be even easier:

from c in this.Categories

select new ProductDetail

{

 Name = c.CategoryName,

 ProductName = c.Products.First().ProductName

}

3) If it's not the case, usually we'd use join:

from category in this.Categories

join product in this.Products on category.CategoryID equals product.CategoryID into categoryProducts

select new ProductDetail

{

 Name = c.CategoryName,

 ProductName = categoryProducts.First().ProductName

}

December 19, 2007 4:00 PM
 

Leon said:

It works like a magic!

Thanks a lot Fabrice :)

December 21, 2007 3:49 AM
New Comments to this post are disabled