1

I have a collection of objects where each object also has a collection. Like so:

public class Product { public int Id { get; set; } public List<Tuple<string, double>> Sales { get; set; } } 

I want to run a LINQ query to check if a Product entity exists and, if it does exist, check it's Sales collection to see if a specific string value (from the Tuple) also exists. If it does, I want to return the corresponding double (also from the Tuple).

I know I can do this in a few lines of code, like so:

saleAmount = String.Empty; product = Model.Products.SingleOrDefault(p => p.Id == product.Id); if(product != null) { productSale = product.Sales.SingleOrDefault(i => i.Item1 == sale.Id); if(productSale != null) { saleAmount = productSale.Item2.ToString(); } } 

Is it possible to do this in one line?

8
  • You'll be exposed to NullReferenceExceptions and I doubt your code would be any clearer. Commented Nov 14, 2014 at 21:57
  • You realize, I hope, that SingleOrDefault will throw an exception if more than one item in the list has that produce id. Are you sure you don't want FirstOrDefault here? Commented Nov 14, 2014 at 21:58
  • @JimMischel Given that he's dealing with primary keys, they shouldn't be duplicated. If they somehow were, it would be a bug in the data and should throw. Commented Nov 14, 2014 at 21:58
  • 1
    @Servy if they are primary keys, then (nickpicking here) FirstOrDefault is performance wise faster (because it's doesn't need to check to see if there are duplicates to throw the exception). Commented Nov 14, 2014 at 22:00
  • @Servy: I didn't see anything that said he was working with a primary key. But if that's the case, then of course. Commented Nov 14, 2014 at 22:00

2 Answers 2

1

The key here is to not actually materialize your query through the use of SingleOrDefault until you're actually done defining the entirety of it. Use Where instead and then use SingleOrDefault at the very end.

var query = (from product in Model.Products where product.Id == someProductId let sale = product.Sales.SingleOrDefault(i => i.Item1 == sale.Id) where sale != null select new { product, saleAmount = sale.Item2, }) .SingleOrDefault(); 
Sign up to request clarification or add additional context in comments.

5 Comments

That's a really interesting approach. Is this possible using lambda expressions?
@ShaiCohen This is using lambda expressions.
@ShaiCohen A lambda expression is the conditional expression which extrapolates a specific check grammar via the "goes to" => syntax. The lambda you are thinking is the linq expression which can be in a query syntax as Servy has done (technically he has a mixture) or as the method syntax which I used in my example. Both of ours use lambdas to express a check operation such as the i => i.Item1 == sale.Id. See Query Syntax and Method Syntax in LINQ (C#) for more info.
@OmegaMan: Thank you for the clarification. Just when you think you know something .... :)
@ShaiCohen We have all been there. Glad to help.
1

Is it possible to do it in one line.

I believe you can distill your code to less lines by combining the check into the second sales array such as

var products = Model.Products.Where(p => p.Id == product.Id && p.Sales.Any(i => i.Item1 == sale.Id) ); var saleAmount = (products != null && products.Any()) ? products.First().Sales.First().Item2.ToString() : string.Empty; 

Using a Default Value

This solution uses the help from a default faux pre-created Product to be used when one is not found. Using it in the extension method DefaultIfEmpty, that method determines if a empty projection has been returned and in that case it will instead return the faux instance. After that we can safely extract a the value which would be string.empty and assign it to the final string productSale.

Below I use a hardcoded 1.5 as the sale price for easier reading of the example.

// Our default will set saleAmount to string.Empty if nothing is found in Products. var defProduct = new Product() { Id = -1, Sales = new List<Tuple<string, double>>() { new Tuple<string,double>(string.Empty, 0.0) }}; var productSale = Products.Where(p => p.Id == product.Id && p.Sales.Any (s => s.Item2 == 1.5 ) ) .DefaultIfEmpty( defProduct ) .First () .Sales.First() .Item1; 

productSale is string.Empty if no value found or has the actual value to use.


Whole test project in LinqPad which simulates a fail by using 1.5. Use 1.6 to show success.

void Main() { var targetSalePrice = 1.5; var targetProductId = 2; var Products = new List<Product>() { new Product() { Id = 2, Sales = new List<Tuple<string, double>>() { new Tuple<string,double>("actual", 1.6) } } }; // Our default will set saleAmount to string.Empty if nothing is found in Products. var defProduct = new Product() { Id = -1, Sales = new List<Tuple<string, double>>() { new Tuple<string,double>("faux string.Empty", 0.0) }}; var productSale = Products.Where(p => p.Id == targetProductId && p.Sales.Any (s => s.Item2 == targetSalePrice ) ) .DefaultIfEmpty( defProduct ) .First () .Sales.First () .Item1; productSale.Dump(); // outputs the string "faux string.Empty" from the faux default. } // Define other methods and classes here public class Product { public int Id { get; set; } public List<Tuple<string, double>> Sales { get; set; } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.