1

If I try to cast an object of type EntityCollection<MyNamespace.Models.MyEntityClass> to ICollection<Object> I get an InvalidCastException.

Okay, according to the docs, EntityCollection<TEntity> implements ICollection<T>, and MyNamespace.Models.MyEntityClass must descend from Object, right? So why on earth does this fail?

FWIW, I'm trying to do this in a method that generally can add and remove items from what might be an EntityCollection or some other IList or ISet. I need to preserve the change tracking behavior of EntityCollection, because the object is to eventually be able to commit the changes if it's an EC.

Edit

Okay, here's some more specifics of what I'm doing. I'm deserializing JSON, and the target object can have properties that are collections--maybe they're EntityCollections, maybe not. For the sake of simplicity, lets say the members of the collection are always subclasses of EntityObject, whether it's an EntityCollection or not (if I understand the responses so far, I'd have no better luck casting to ICollection<EntityObject> than to ICollection<Object>…right?). This is the part where I run into trouble…

foreach (PropertyInfo prop in hasManys)
{
    // This is where I get the InvalidCastException...
    ICollection<Object> oldHms = (ICollection<Object>)prop.GetValue(parentObj, null);

    JEnumerable<JToken> hmIds = links[FormatPropName(prop.Name)].Children();
    if (hmIds.Count() == 0)
    {
        // No members! Clear it out!
        oldHms.Clear();
        continue; // breaking early!
    }

    relType = prop.PropertyType.GetGenericArguments()[0];

    // Get back the actual entities we'll need to put into the relationship...
    List<EntityObject> newHms = new List<EntityObject>();
    foreach (JToken jt in hmIds)
    {
        // ...populate newHms with existing EntityObjects from the context...
    }

        // first, delete any missing...
        /* Got to use ToList() to make a copy, because otherwise missings is 
         * still connected to the oldHms collection (It's an ObjectQuery)
         * and you can't modify oldHms while enumerating missings. 
         */
        // This cast will fail too, right? Though it's more easily fixable:
        IEnumerable<EntityObject> missings = ((ICollection<EntityObject>)oldHms).Except(newHms).ToList();
        foreach (EntityObject missing in missings)
        {
            oldHms.Remove(missing); // One of my mutable collection operations
        }

        // add new ones
        foreach (EntityObject child in newHms)
        {
            if (!oldHms.Contains(child)) // Skip if already in there
            {
                oldHms.Add(child); // another mutable collection operation
            }
        }

    }

}

That's a bit simplified, I have special cases for Arrays (implement ICollection, but aren't generics) and other stuff that I took out. Point is, I need to operate Clear, Add, and Remove on the EntityCollection itself--if that's what it is. Maybe there's another way to do this type of synchronization that I'm missing?

S'pht'Kr
  • 2,809
  • 1
  • 24
  • 43
  • This link might help you: [http://stackoverflow.com/questions/1360184/c-sharp-casting-generic-type][1] [1]: http://stackoverflow.com/questions/1360184/c-sharp-casting-generic-type – Onur Mar 06 '14 at 10:14
  • @Onur Hmm..interesting…but I am on .Net 4.0 and these are reference types…? – S'pht'Kr Mar 06 '14 at 10:16
  • Fundamentally, if `Y` descends from `X`, `Generic` is **not** a subtype of `Generic`, because you would violate the [Liskov Substitution Principle](http://en.wikipedia.org/wiki/Liskov_substitution_principle). I.e. pretty much the definition of the subtyping relationship in OO languages. What you're doing fails because it does not in fact make any sense for it to succeed. – millimoose Mar 06 '14 at 10:21
  • Also: can't you make the method generic over the type of the item? E.g.: `void AddEntity(ICollection items) { … }` – millimoose Mar 06 '14 at 10:25
  • @millimoose: This is only true if you have _both_ get and set methods. Otherwise covariance or contravariance will be fine. – Onur Mar 06 '14 at 10:34
  • @Onur – There are no "write-only" collection interfaces in the BCL, and the OP mentions he wants to add items. – millimoose Mar 06 '14 at 10:34
  • That's right. But your statement sounded more general. – Onur Mar 06 '14 at 10:46

2 Answers2

1

Since ICollection<T> hasn't variance, ICollection<MyEntityClass> and ICollection<object> are different types, unrelated to each other.

I'm trying to do this in a method that generally can add and remove items from what might be an EntityCollection or some other IList or ISet

So, why don't you work with IList? Looks like you don't care about real type of items in this method.

Dennis
  • 37,026
  • 10
  • 82
  • 150
  • Hmm… work with IList how so? Okay, so I can cast it to `IEnumerable`, but if I call `.ToList()` on that, it will be a copy--if I add and remove things, the `EntityCollection` will not know about it. – S'pht'Kr Mar 06 '14 at 10:32
  • @S'pht'Kr: may be, you'll post the code of your method? – Dennis Mar 06 '14 at 10:38
  • Done, perhaps that clarifies things a bit? – S'pht'Kr Mar 06 '14 at 12:50
1

read-write collections cannot be variant.

Take this example:

List<MyClass> list1 = new List<MyClass>();

// assume this would work
ICollection<object> list2 = list1;

list2.Add(new object()); // ooops. We added an object to List<MyClass>!

In principal this kind of casting is only possible for "read-only" interfaces (allowing covariance) or for "write-only" interfaces (allowing contravariance).

One "solution" would involve a wrapper class like this:

public class Wrapper<T> : ICollection<object>
{
    private readonly ICollection<T> collection;
    public Wrapper(ICollection<T> collection)
    {
        this.collection = collection;
    }


    public void Add(object item)
    {
        // maybe check if T is of the desired type
        collection.Add((T)item);
    }

    public void Clear()
    {
        collection.Clear();
    }

    public bool Contains(object item)
    {
        // maybe check if T is of the desired type
        return collection.Contains((T)item);
    }

    public void CopyTo(object[] array, int arrayIndex)
    {
        // maybe check if T is of the desired type
        collection.CopyTo(array.Cast<T>().ToArray(), arrayIndex);
    }

    public int Count
    {
        get { return collection.Count; }
    }

    public bool IsReadOnly
    {
        get { return collection.IsReadOnly; }
    }

    public bool Remove(object item)
    {
        // maybe check if T is of the desired type
        return collection.Remove((T)item);
    }

    public IEnumerator<object> GetEnumerator()
    {
        yield return collection;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return collection.GetEnumerator();
    }
}

Instead of

 EntityCollection<MyNamespace.Models.MyEntityClass> collection = ...;
 ICollection<Object> generic = collection ;

you would have to write:

 EntityCollection<MyNamespace.Models.MyEntityClass> collection = ...;
 ICollection<Object> generic = new Wrapper(collection);

And could adjust the wrapper class at the points marked by comments how to deal with type problems.

Onur
  • 5,017
  • 5
  • 38
  • 54
  • Ahhhh…quite right, I see why this would be bad. Still, I could live with that last line throwing a runtime error. We're in runtime at this point anyway, because the compiler can't see what I've got before I try to cast it (I am using `PropertyInfo.GetValue`). But ugh…then is there no way to write the generalized code I'm trying to? Without using `GetMethod` or something of that nature? I want to use `Clear`, `Add`, and `Remove` on the whatever-collection. – S'pht'Kr Mar 06 '14 at 10:27
  • @S'pht'Kr "Still, I could live with that last line throwing a runtime error." – the compiler can't. The only appropriate runtime error is a class cast exception, but the compiler can't really insert a cast since it doesn't know what type it's casting to. (You *could* have it insert some code to dynamically determine this but that would probably mean all type errors would be moved to runtime.) If you want a runtime error you'll have to make it explicit by casting to *something*. So if you make your method `AddItem(ICollection items)`, you can do `items.Add((T) value)` – millimoose Mar 06 '14 at 10:36