Shai Cohen
Shai Cohen

Reputation: 6249

How to run a LINQ query on a collection of objects and a collection inside of each object

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?

Upvotes: 1

Views: 772

Answers (2)

ΩmegaMan
ΩmegaMan

Reputation: 31576

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; }
}

Upvotes: 1

Servy
Servy

Reputation: 203802

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();

Upvotes: 1

Related Questions