locked
LINQ Single throw exception even with Nullable return value RRS feed

  • Question

  • I would like to perform a LINQ operation on two objects:

    I have a Dictionary object, categoryProducts, where I have as Key a Enum, CategoryEnum, that specifies a Category and as Values the list of alphanumerical IDs of products, List<string>, that belong to that Category:

    public enum CategoryEnum { Tools = 0, Households = 1, Clothing = 2 }; List<string> productIdList1, productIdList2; productIdList1 = new List<string>(){"p1", "p2", "p4"}; productIdList2 = new List<string>(){"p3"}; productIdList2 = new List<string>(){"p5", "p6"};

    Dictionary<CategoryEnum, List<string>> categoryProducts = new Dictionary<CategoryEnum, List<string>> categoryProducts.Add(CategoryEnum.Tools, productIdList1); categoryProducts.Add(CategoryEnum.Clothing, productIdList2); categoryProducts.Add(CategoryEnum.HouseHolds, productIdList3);

    Then I have a List of Products with no duplicates, List<Product>:

    public class Product { string Id {get; set;} string Name {get; set;} }

    List<string> product1, productId2, product3;

    product1 = new <Product>(){Id=p1, Name="Product1"};
    product2 = new <Product>(){Id=p2, Name="Product2"};
    product7 = new <Product>(){Id=p7, Name="Product7"};

    I want to extract from categoryProducts, the KeyValuePair<CategoryEnum, List<string>> items where the Id of a Product matches any in in the List<string>. Product1 has Id, p1, matching at  productIdList1, hence

    KeyValuePair<CategoryEnum.Tools, productIdList1>

    In the case of Product7, which is not matching any id, I want to proceed without extracting any pair. So I use the following code:

    foreach (var product in Products) { KeyValuePair<CategoryEnum, List<string>> category;

    category categoryProducts.Single(cp => cp.Value.Any(pID => pID == product.ID));

    }

    Which throws an exception "Sequence contains no matching element" when it does not find a matching element. So I used SingleOrDefault  that gives back the pair with Key CategoryEnum.Tools, which is not the behaviour I want. I also tried to make the return value nullable

    KeyValuePair<CategoryEnum, List<string>>?
    
    or
    
    Nullable<KeyValuePair<CategoryEnum, List<string>>>

    or replace Single with First but it still throws the "Sequence contains no matching element" exception. How can I achieve this result? I know I can do it by using foreach but I would like to knwo the LINQ version. Thanks!


    • Edited by Trinakriae Thursday, April 3, 2014 8:44 PM
    Wednesday, April 2, 2014 2:45 PM

Answers

  • Ok. There are some compilation errors but I believe I've got it set up the way you want. Here's the direct answer to your question.

    static void Main ( string[] args )
    {
        var productIdList1 = new List<string>(){"p1", "p2", "p4"};
        var productIdList2 = new List<string>(){"p3"};
        var productIdList3 = new List<string>(){"p5", "p6"};
    
        var categoryProducts = new Dictionary<CategoryEnum, List<string>>();
    
        categoryProducts.Add(CategoryEnum.Tools, productIdList1);
        categoryProducts.Add(CategoryEnum.Clothing, productIdList2);
        categoryProducts.Add(CategoryEnum.Households, productIdList3);
    
        var products = new[] { 
                            new Product() { Id="p1", Name="Product1" },
                            new Product() { Id="p2", Name="Product2" },
                            new Product() { Id="p7", Name="Product7" }
        };
    
        foreach (var product in products)
        {
            var categories = GetCategories(categoryProducts, product);
    
            Console.WriteLine("Product {0} has categories: {1}", product.Name, categories.Key);                
        };
    }
    
    static KeyValuePair<CategoryEnum, List<string>> GetCategories ( IDictionary<CategoryEnum, List<string>> categories, Product product )
    {
        return (from c in categories
                where c.Value.Contains(product.Id)
                select c).SingleOrDefault();            
    }

    But I question if this is actually what you're wanting.  Given your categoryProducts dictionary a category can have multiple products.  Can a product have multiple categories?  If so then the above code would return either the only category or it will blow up.  If you want to get just the first one then you can use FirstOrDefault.

    The second issue is your return value.  KeyValuePair is a value type so there is no easy way to distinguish between a default value and a real value.  But I don't believe you really want to return KVP anyway.  For a product don't you just care about the category(ies)?  If so then returning a CategoryEnum (or IEnumerable) is more appropriate. 

    The final issue is that the default value of an enum is 0.  In your case you're actually using 0 as a valid value.  In most cases 0 should represent an undefined/unknown/unset value.  This makes it very easy in code to know whether you're dealing with a valid value or a default one.  If you really want 0 to be a valid category then you're only option is to use a nullable return type (CategoryEnum?).  But that just adds complexity to the caller (in my opinion).

    I've taken a little liberty with this final block code.  I assume that a product can have multiple categories and that all you want are the categories.  By making this assumption we resolve the issue of products without any categories (you just get back an empty list) and you don't have to worry about the default enum value.

    //Add a product with 2 categories
    var productIdList2 = new List<string>(){"p3", "p2" };
    
    foreach (var product in products)
    {
        var categories = GetCategories(categoryProducts, product);
    
        Console.WriteLine("Product {0} has categories: {1}", product.Name, String.Join(", ", categories));                 
    }
    
    static IEnumerable<CategoryEnum> GetCategories ( IDictionary<CategoryEnum, List<string>> categories, Product product )
    {
        return from c in categories
                where c.Value.Contains(product.Id)
                select c.Key;
    }

    For completeness, here's the version if you want only a single category and you want to know whether it is the default or not. 

    var category = GetCategory(categoryProducts, product);
    Console.WriteLine("Product {0} has category: {1}", product.Name, category.HasValue ? category.ToString() : "(none)");
    
    static CategoryEnum? GetCategory ( IDictionary<CategoryEnum, List<string>> categories, Product product )
    {
        var item = (from c in categories
                    where c.Value.Contains(product.Id)
                    select c).SingleOrDefault();
    
        //No way to know whether the item is defaulted or not...
        return (item.Value != null) ? (CategoryEnum?)item.Key : null;
    }
    

    Michael Taylor
    http://msmvps.com/blogs/p3net

    • Marked as answer by Anne Jing Thursday, April 10, 2014 5:31 AM
    Thursday, April 3, 2014 9:34 PM

All replies

  • Single is designed to either return back the only result in the list or throw.  If there are no items or more than 1 then it'll throw.

    SingleOrDefault is designd to return back either the only result in the list or null.  If there are more than 1 items then it'll throw.

    First/FirstOrDefault work similar but support lists with more than 1 item.

    I don't see the relationship between the various items that you have.  What is CategoryProduct?  Is that how you're mapping a CategoryEnum to Product?  Can a product have more than one category?  What does the definition look like?

    Michael Taylor
    http://msmvps.com/blogs/p3net

    Wednesday, April 2, 2014 5:11 PM
  • Thanks for your reply, I am sorry I didn't put much context on my question. In, short the Dictionary maps the parent-child (one to many) relationship of Category - Products (by their Ids). The products Product whose Id is not in the Dictionary are to be considered as parent as well.

    I riformulated my question, now it should be more clear. Thanks a lot!

    Thursday, April 3, 2014 8:52 PM
  • Ok. There are some compilation errors but I believe I've got it set up the way you want. Here's the direct answer to your question.

    static void Main ( string[] args )
    {
        var productIdList1 = new List<string>(){"p1", "p2", "p4"};
        var productIdList2 = new List<string>(){"p3"};
        var productIdList3 = new List<string>(){"p5", "p6"};
    
        var categoryProducts = new Dictionary<CategoryEnum, List<string>>();
    
        categoryProducts.Add(CategoryEnum.Tools, productIdList1);
        categoryProducts.Add(CategoryEnum.Clothing, productIdList2);
        categoryProducts.Add(CategoryEnum.Households, productIdList3);
    
        var products = new[] { 
                            new Product() { Id="p1", Name="Product1" },
                            new Product() { Id="p2", Name="Product2" },
                            new Product() { Id="p7", Name="Product7" }
        };
    
        foreach (var product in products)
        {
            var categories = GetCategories(categoryProducts, product);
    
            Console.WriteLine("Product {0} has categories: {1}", product.Name, categories.Key);                
        };
    }
    
    static KeyValuePair<CategoryEnum, List<string>> GetCategories ( IDictionary<CategoryEnum, List<string>> categories, Product product )
    {
        return (from c in categories
                where c.Value.Contains(product.Id)
                select c).SingleOrDefault();            
    }

    But I question if this is actually what you're wanting.  Given your categoryProducts dictionary a category can have multiple products.  Can a product have multiple categories?  If so then the above code would return either the only category or it will blow up.  If you want to get just the first one then you can use FirstOrDefault.

    The second issue is your return value.  KeyValuePair is a value type so there is no easy way to distinguish between a default value and a real value.  But I don't believe you really want to return KVP anyway.  For a product don't you just care about the category(ies)?  If so then returning a CategoryEnum (or IEnumerable) is more appropriate. 

    The final issue is that the default value of an enum is 0.  In your case you're actually using 0 as a valid value.  In most cases 0 should represent an undefined/unknown/unset value.  This makes it very easy in code to know whether you're dealing with a valid value or a default one.  If you really want 0 to be a valid category then you're only option is to use a nullable return type (CategoryEnum?).  But that just adds complexity to the caller (in my opinion).

    I've taken a little liberty with this final block code.  I assume that a product can have multiple categories and that all you want are the categories.  By making this assumption we resolve the issue of products without any categories (you just get back an empty list) and you don't have to worry about the default enum value.

    //Add a product with 2 categories
    var productIdList2 = new List<string>(){"p3", "p2" };
    
    foreach (var product in products)
    {
        var categories = GetCategories(categoryProducts, product);
    
        Console.WriteLine("Product {0} has categories: {1}", product.Name, String.Join(", ", categories));                 
    }
    
    static IEnumerable<CategoryEnum> GetCategories ( IDictionary<CategoryEnum, List<string>> categories, Product product )
    {
        return from c in categories
                where c.Value.Contains(product.Id)
                select c.Key;
    }

    For completeness, here's the version if you want only a single category and you want to know whether it is the default or not. 

    var category = GetCategory(categoryProducts, product);
    Console.WriteLine("Product {0} has category: {1}", product.Name, category.HasValue ? category.ToString() : "(none)");
    
    static CategoryEnum? GetCategory ( IDictionary<CategoryEnum, List<string>> categories, Product product )
    {
        var item = (from c in categories
                    where c.Value.Contains(product.Id)
                    select c).SingleOrDefault();
    
        //No way to know whether the item is defaulted or not...
        return (item.Value != null) ? (CategoryEnum?)item.Key : null;
    }
    

    Michael Taylor
    http://msmvps.com/blogs/p3net

    • Marked as answer by Anne Jing Thursday, April 10, 2014 5:31 AM
    Thursday, April 3, 2014 9:34 PM