Damiano
Damiano

Reputation: 71

C# Linq InvalidOperationException

I'm currently learning C# and trying to get the hang of LINQ. I am trying to create two classes : Store and Produces and I want to return the object in them based on the correspond ID. I planned to do this using LINQ but I run into this error.

System.InvalidOperationException: 'Failed to compare two elements in the array.' ArgumentException: At least one object must implement IComparable.

I of course tried to implement IComparable into the classes by adding something like this:

 public class Store : IComparable<Store>
 {
       public int CompareTo([AllowNull] Store other)
    {
        throw new NotImplementedException();
    }
 }
 

Unfortunately this did not fix the error. I have tried editing the CompareTo method a but but to no success, what should I do?

Here is my full code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;

namespace LINQ_Testing
{
    class Program
    {

        static void Main(string[] args)
        {
            List<Store> MarketPlace = new List<Store>();
            //Stores
            MarketPlace.Add(new Store { Name = "SuperMarket", ID = 1 });
            MarketPlace.Add(new Store { Name = "Electronics", ID = 2 });
            MarketPlace.Add(new Store { Name = "Clothing", ID = 3 });
            MarketPlace.Add(new Store { Name = "Appliance", ID = 4 });

            List<Produces> Products = new List<Produces>();

            //Products
            Products.Add(new Produces { Names = new string[] { "Vegetables", "Dairy", "Fruits" }, Prices = new double[] { 3.00, 2.00, 1.50 }, StoreID = 1 });
            Products.Add(new Produces { Names = new string[] { "Portable Electronics", "Stationary Electronics", "Medical Electronics" }, Prices = new double[] { 200.00, 750.00, 550.00 }, StoreID = 2 });
            Products.Add(new Produces { Names = new string[] { "Coats", "Dresses", "Leggings" }, Prices = new double[] { 35.00, 45.00, 25.00 }, StoreID = 3 });
            Products.Add(new Produces { Names = new string[] { "Home Appliances", "Kitchen Appliances", "Industrial Appliances" }, Prices = new double[] { 500.00, 350.00, 850.00 }, StoreID = 1 });

            var innerJoin = from market in MarketPlace
                            orderby market.ID
                            join product in Products
                            on market.ID
                            equals product.StoreID
                            into marketGroup
                            select new
                            {
                                market = market.Name,
                                product = from market2 in marketGroup
                                     orderby market2.Names
                                          select market2
                            };
            foreach (var item in innerJoin)
            {
                Console.WriteLine(item.market);
                foreach(var item2 in item.product)
                {
                    item2.ToString();
                }
            }
        }


    }











}
    

    public class Store
    {
        public string Name { get; set;  } 
        public int ID { get; set; }

        
    }

    public class Produces
    {
        public string[] Names = new string[3];

        public int StoreID { get; set; }

        public double[] Prices = new double[3];

    public override string ToString()
    {
        return "Contains Products such as: " /*NamesArray()*/ + " with prices such as : " /*+ PricesArray()*/;
    }

    private string NamesArray()
    {
        int i;
        for (i = 0; i <= 3 - 1; i++)
        {
            return Names[i]+",";
        }
        return "";
    }
    private string PricesArray()
    {
        int i;
        for(i = 0; i <= 3 - 1; i++)
        {
            var pr = Prices[i].ToString();
            return pr;

        }
        return "";
    }
    
}

Upvotes: 2

Views: 498

Answers (1)

Harald Coppoolse
Harald Coppoolse

Reputation: 30464

So you have two tables: Store and Products. Apparently every Product belongs to a Store, namely the Store that foreign key Product.StoreId refers to.

To debug, you already tried to simplify your LINQ, by removing the OrderBy statements, to see if the problem is in the Join or in the OrderBy, and of course you also tried to do only OrderBy without the Join. If not: go to the naughty spot for half an hour!

Does the following work?

Join only:

var result = Stores.Join(Products,   // Join Stores and Products
    store => store.Id,               // from every Store take the primary key
    product => product.StoreId,      // from every Product take the foreign key

    // parameter resultSelector: from every matching Store and Product make one new:
    (store, product) => new
    {
        Market = store.Name,
        Product = product,
    })
    .ToList();

I think it does, because the exception is in about IComparer, not about IEqualityComparer

2nd Attempt: Order the Stores and the Products:

var sortedStores = marketPlaces.OrderBy(store => store.ID).ToList();
var sortedProducts = products.OrderBy(product => product.Names).ToList();

Which one does not work? I think the 2nd one, because Product.Names is a string[]. You haven't told LINQ how it should compare two string[] objects, or in other words:

I have two string[] objects, which one should come first?

So to sort product names, we need an object that implements IComparable<string[]>. Making an IComparable is not difficult. The problem probably is more in the requirement: if you have two string arrays product names, which one becomes first?

public class ProductNamesComparer : IComparer<string[]>
{
    public int Compare(string[] x, string[] y)
    {
        // TODO: find out if x comes before y (return -1)
        // or after y                         (return +1)
        // or don't care                      (return 0)
    }
}

I haven't got a clue why {"Potato", "Cabbage"} should come before an {"Egg plant", "Carrot"}, so you have to implement this yourself.

Just for fun (and learn!): Stores with the product that they sell

Are you sure that you want in your final result the Product Names? Don't you want to know per Store which Products it sells and for what Prices?

Instead of:

"Store 1, wich is a supermarket, sells products
with Names { "Vegetables", "Dairy", "Fruits" },
and Prices = new double[] { 3.00, 2.00, 1.50 } }"

You want something like:

Store 1 a "Supermarket" sells the following Products:
    "Vegetables" with price 3.00
    "Dairy" with price 2.00
    "Fruits" with price 1.50
Store 2 "Electronics" sells the following Products:
    "Portable Electronics" with price 200.00,
    "Stationary Electronics" with price 750.00,
    "Medical Electronics" with price 550.00
Store 3 ... etc.

If you want this, you first combine the product names with their prices:

StoreId = 1 has the following products with their prices:
            Name = "Vegetables"; Price 3.00
            Name = "Dairy"; Price 2.00
            Name = "Fruits"; Price 1.50
StoreId = 2 has the following products with their prices:
            Name = "Portable Electronics"; Price 200.00
            ...

I assumed with your Product prices, that the first Price belongs to the first Product name, and the 2nd price is the price of the 2nd product name. We somehow have to match them into combinations: {Product Name; Product Price}.

If you have two sequences, A and B, and you want to combine them, such that you have combinations of {A[0], B[0]}, {A[1], B{1}}, {A[2], B[2]}, etc, Consider to use one of the overloads of Enumerable.Zip

var productsWithTheirPrices = products.Select(product => new
{
    StoreId = product.StoreId,
    NamePriceCombinations = product.Names.Zip(product.Prices,
    (name, price) => new
    {
        Name = name,
        Price = price,
    })
    .ToList(),
});

In words: from every product in your sequence of Products, make one new object with the following properties:

  • StoreId: the value of property StoreId of the product
  • NamePriceCombinations: a list that contains the combinations of {Names[0], Prices[0]}, {Names[1], Prices[1]}, etc.

Note: this only works if you have exactly one Price per Product Name.

So we have converted your original sequence of Products into:

StoreId = 1, NamePriceCombinations = { { Name = "Vegetables"; Price 3.00},
                                       { Name = "Dairy"; Price 2.00},
                                       { Name = "Fruits"; Price 1.50} },
StoreId = 2; NamePriceCombinations = { { Name = "Portable Electronics"; Price 200.00},
                                       ...

We only need to join this with the Stores. Use Enumerable.GroupJoin

Use GroupJoin if you want Items with their zero or more sub-items, like Schools with their Students, Customers with their Orders, or Stores with the products they sell.

If you want it the other way round: "item with the (exactly one) item that it belongs to", like Student with the School he attends, or Product with the Store that sells this Product, use Join

We want "Stores with the Products that they sell", so we use GroupJoin

var storesWithTheProductsTheySell = MarketPlaces.GroupJoin(productsWithTheirPrices,

    store => store.Id,             // from every store take the primary key
    products => product.StoreId,   // from every product, take the foreign key

    (store, productsOfThisStore) => new
    {
         // Select the Store properties that you plan to use:
         Market = store.Name,
         ...

         Products = productsOfThisStore.Select(product => new
         {
             Name = product.Name,
             Price = product.Price,
         })
         .ToList(),
    });

Of course you can do this in one big LINQ statement. Because the intermediate results are IEnumerable, this won't improve efficiency very much, however I think that readability would deteriorate a lot.

Upvotes: 1

Related Questions