Unable to cast object of type 'WhereEnumerableIterator`1' to type 'System.Collections.Generic.ICollection`1

12.7k Views Asked by At

I have the following code (please note that this is stripped down to the relevant part, the actual query is a lot more complex):

public IQueryable<Menu> GetMenus(DateTime lastUpdate) {
    ...
    result = GetAll().Where(m => lastUpdate < m.LastModified)
                     .ForEach(m => m.Descriptions = m.Descriptions
                                                     .Where(d => lastUpdate < d.LastModified));
    ...
enter code here

This is an function within an update service routine for an App to get any menu, which either itself or any of its descriptions has changed since the update service was last called.

Clarification: The function needs to return every Menu, which has changed since the last call. Additionally, it needs to return every changed Description of every changed Menu. but it must leave out the unchanged Descriptions.

As an Example:

Menu menuA = new Menu() {
    LastModified = new DateTime(2014, 12, 24),
    Descriptions = new List<Description>() {
        new Description() { LastModified = new DateTime(2014, 12, 24) },
        new Description() { LastModified = new DateTime(2014, 12, 01) }
    }
};
Menu menuB = new Menu() {
    LastModified = new DateTime(2014, 12, 20),
    Descriptions = new List<Description>() {
        new Description() { LastModified = new DateTime(2014, 12, 01) }
    }
};

Now, when I call the update function with new DateTime(2014, 12, 15), this is the structure it needs to return:

List<Menu>: {
    menuA: {
        LastModified: DateTime(2014, 12, 24),
        Descriptions: List<Description> {
            Description: {
                LastModified: DateTime(2014, 12, 24),
            }
        }
    },
    menuB: {
        LastModified: DateTime(2014, 12, 20),
        Descriptions: List<Description> {}
    }
}

With ForEach() looking like this:

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action) {
        ... // Parameter check
        foreach (T item in source) {
            action(item);
        }
        return source;
    }

Menu and Description were automatically created by entity framework like this:

public partial class Menu {
    ...
    public System.DateTime LastModified { get; set; }
    public virtual ICollection<Description> Descriptions { get; set; }
    ...
}

public partial class Description {
    ...
    public System.DateTime LastModified { get; set; }
    public virtual Menu Menu { get; set; }
    ...
}

Unfortunately the Where function returns an IEnumerabley<Description>, which cannot be cast internally to the ICollection<Description> defined by entity framework.

When I try to cast it myself like this I get the runtime error within the title:

m => m.Descriptions = (ICollection<Description>)m.Descriptions.Where(...)

Now, I do understand as to why this error is thrown. The Description's Where expression has not yet been evaluated, so what is supposed to being cast to ICollection<Description> is not an IEnumerable<Description> yet, but a WhereEnumerableIterator. Right now I'm casting the Where expression to a list, which gets evaluated immediately and then cast to ICollection<Description>.

m => m.Descriptions = (ICollection<Description>)m.Descriptions.Where(...).ToList()

However, this is merely a workaround killing the benefits of the LINQ expression and besides, plain ugly. I could write an extension method WhereCollection<T>(...) calling Where<T> and returning an ICollection<T> but that wouldn't change much, I'd have to do the cast internally which either results in the same error or calls ToList() internally.

Is there an elegant solution to this problem, without forcing the Where expression to evaluate before the LINQ statement gets evaluated?

3

There are 3 best solutions below

3
On

"This is an function within an update service routine for an App to get any menu, which either itself or any of its descriptions has changed since the update service was last called."

So... wouldn't you have a slightly complex Where clause in this case instead of all that?

result = GetAll()
         .Where(m => lastUpdate < m.LastModified || 
                m.Descriptions.Any(d => lastUpdate < d.LastModified);

Your problem statement basically described the LINQ query. ;)

0
On

Since the Menu object you need is different then the records you are reading from the database (it will be missing some Description objects), you need to create new Menu objects:

Here is the full working version (direct from LinqPad)

void Main()
{
    GetMenus(new DateTime(2014,12,15)).Dump();
}

public IEnumerable<Menu> GetMenus(DateTime lastUpdate)
{
    var result = from m in GetAll()
                where lastUpdate < m.LastModified
                select new Menu
                {
                    LastModified = m.LastModified,
                    Descriptions = m.Descriptions.Where(d =>lastUpdate < d.LastModified ).ToList()
                };
                
    return result;
}

IEnumerable<Menu> GetAll() 
{
    Menu menuA = new Menu() {
    LastModified = new DateTime(2014, 12, 24),
    Descriptions = new List<Description>() {
        new Description() { LastModified = new DateTime(2014, 12, 24) },
        new Description() { LastModified = new DateTime(2014, 12, 01) }
    }
};
Menu menuB = new Menu() {
    LastModified = new DateTime(2014, 12, 20),
    Descriptions = new List<Description>() {
        new Description() { LastModified = new DateTime(2014, 12, 01) }
    }
};

    return new List<Menu>{menuA, menuB};
}


public partial class Menu {

    public System.DateTime LastModified { get; set; }
    public virtual ICollection<Description> Descriptions { get; set; }
}

public partial class Description
{
    public System.DateTime LastModified { get; set; }
    public virtual Menu Menu { get; set; }
}

I had to convert the IQueryables to IEnumerables since I didn't have your database, but I also did a similar version that did read from a database and it worked there as well.

0
On

Is there an elegant solution to this problem, without forcing the Where expression to evaluate before the LINQ statement gets evaluated?

The ForEach extension makes it not elegant and is likely the problem. There is a reason ForEach is not included in Linq. Linq uses functional "pure" methods but ForEach uses side effects.

Your GetMenus Method returns IQueryable<Menu>. Thus if your GetAll() also returns an IQueryable<>, then your ForEach is a performance problem. The reason is, when you call Linq methods on IQueryable<> like dc.Customers.Where(c => c.Age >= 18), when the Linq statement is converted into an SQL WHERE, so only some customers are loaded form the database. If you would write dc.Customers.ForEach(c => ...) and if ForEach would accept IEnumerable instead of IQueryable (as in your case), then you are querying in-memory from that point on and not on the database.