Reputation: 6249
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
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;
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
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