1

I have this helper method that is designed to transfer collection items from one collection object instance to another. It works, but I have recently ran into an issue where a particular collection implements at different points IEnumerable<T>;. At one level as IEnumerable<KeyValuePair<TKey, TValue>> and at another IEnumerable<TValue>. In my code below, the declaration of secondaryCollection causes it to use the IEnumerable<TValue> instance type versus the collectionType declaration finds it as the base ICollection<KeyValuePair<TKey, TValue>> type so that I can invoke the Add() and Remove(). With this type mismatch though the Add() and Remove() method invocations fail. I think if I can figure out how to declare secondaryCollection as type IEnumerable<object> where 'object' is of type KeyValuePair<TKey, TValue> and not of just the type TValue that this should work without the type mismatch exception (it's actually an argument exception for the Add(), Remove() methods). The problem is this is all done in reflection and the types are unknown. How can I do this?

Here's the current method code:

public void MergeCollection(FieldInfo primaryMember, object primaryObject, FieldInfo secondaryMember, object secondaryObject)
    {
        if (primaryMember == null)
            throw new ArgumentNullException("primaryMember");

        if (primaryObject == null)
            throw new ArgumentNullException("primaryObject");

        if (secondaryMember == null)
            throw new ArgumentNullException("secondaryMember");

        if (secondaryObject == null)
            throw new ArgumentNullException("secondaryObject");

        //Get the collection type and validate
        Type genericType = typeof(ICollection<>);

        Type collectionType = primaryMember.FieldType.GetBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericArguments().Length == 1 && t == genericType.MakeGenericType(t.GetGenericArguments()));

        if (!collectionType.IsAssignableFrom(secondaryMember.FieldType))
            throw new InvalidOperationException("Primary and secondary collection types do not match.");

        Type collectionParamType = collectionType.GetGenericArguments()[0];


        //Get the collection invocable methods
        MethodInfo add = collectionType.GetMethod("Add", new Type[] { collectionParamType });
        MethodInfo remove = collectionType.GetMethod("Remove", new Type[] { collectionParamType });

        //Declare the collections
        object primaryCollectionObject = primaryMember.GetValue(primaryObject);
        object secondaryCollectionObject = secondaryMember.GetValue(secondaryObject);

        Type genericEnumerableType = typeof(IEnumerable<>);
        Type enumerableType = primaryMember.FieldType.GetBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericArguments().Length == 1 && t == genericEnumerableType.MakeGenericType(t.GetGenericArguments()));

        IEnumerable<object> secondaryCollection = ((IEnumerable)secondaryCollectionObject).Cast<object>();

        //Transfer the items
        int noItems = secondaryCollection.Count();
        // int noItems = (int)count.GetValue(secondaryCollectionObject);
        for (int i = 0; i < noItems; i++)
        {
            try
            {
                add.Invoke(primaryCollectionObject, new object[] { secondaryCollection.ElementAt(0) });
                remove.Invoke(secondaryCollectionObject, new object[] { secondaryCollection.ElementAt(0) });
            }
            catch (ArgumentException ex)
            {
                //The argument exception can be captured here
            }
        }
    }

Edit:

Maybe just to add some clarification for what I'm needing help with... I have a custom collection that's used in a class being evaluated by a method using reflection. This collection implements IEnumerable twice... IEnumerable<TValue> and IEnumerable<KeyValuePair<TKey, TValue>>. Instead of this...

IEnumerable<object> secondaryCollection = ((IEnumerable)secondaryCollectionObject).Cast<object>();

which ends up using the IEnumerable<TValue> because of the Cast<T>() operation, I need something where the secondaryCollection uses the IEnumerable<KeyValuePair<TKey, TValue>>. And it can't know that the collection originally used two implementations. Since this line:

Type collectionType = primaryMember.FieldType.GetBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericArguments().Length == 1 && t == genericType.MakeGenericType(t.GetGenericArguments()));

does identify the correct type, I originally thought it could be used but I'm not sure how.

ckruczek
  • 2,361
  • 2
  • 20
  • 23
bjhuffine
  • 924
  • 1
  • 11
  • 23
  • 1
    why don't you just do `public void MoveItems(ICollection source, ICollection target) { /* code to remove all items from source and add to target without reflection */ }` ? – herzmeister Jul 28 '15 at 14:24
  • I can't because this is just a small part of a process designed to split a larger object (Message) into multiple smaller ones sent across process boundaries. This entire process is handled via reflection so that it doesn't have to know the specifics (or even if the Message object has a collection let alone its collection type). This allows me to have Message objects of different types for different purposes. – bjhuffine Jul 28 '15 at 14:41
  • The collections probaly imlement multiple IEnumerable, but only one ICollection. – Zotta Aug 12 '15 at 07:41

3 Answers3

1

Just saw that Zotta brought a similar idea while I was preparing my answer. Here is mine anyway. The easiest way is to have the main implementation in "pure" C# generic method and just call it from reflection. Now the question is how to call a generic method via reflection. There are several SO items regarding that, and so far the easiest way is to use dynamic feature. Here is a sample solution including both dynamic or pure reflection methods (validations skipped) and a simple test:

static class Helper
{
    public static void Merge<T>(ICollection<T> source, ICollection<T> target)
    {
        foreach (var item in source) target.Add(item);
        source.Clear();
    }

    #region Using dynamic

    public static void MergeCollection(FieldInfo sourceMember, object sourceObject, FieldInfo targetMember, object targetObject)
    {
        var sourceCollection = sourceMember.GetValue(sourceObject);
        var targetCollection = targetMember.GetValue(targetObject);
        Merge((dynamic)sourceCollection, (dynamic)targetCollection);
    }

    #endregion

    #region Using reflection only

    public static void MergeCollection2(FieldInfo sourceMember, object sourceObject, FieldInfo targetMember, object targetObject)
    {
        var collectionType = targetMember.FieldType.GetInterfaces().Single(
            t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ICollection<>)
        );
        var itemType = collectionType.GetGenericArguments()[0];
        var mergeMethod = MergeMethodInfo.MakeGenericMethod(itemType);
        var sourceCollection = sourceMember.GetValue(sourceObject);
        var targetCollection = targetMember.GetValue(targetObject);
        mergeMethod.Invoke(null, new[] { sourceCollection, targetCollection });
    }

    private static readonly MethodInfo MergeMethodInfo = GetGenericMethodDefinition(
        (ICollection<object> source, ICollection<object> target) => Merge(source, target)
    );

    private static MethodInfo GetGenericMethodDefinition<T1, T2>(Expression<Action<T1, T2>> e)
    {
        return ((MethodCallExpression)e.Body).Method.GetGenericMethodDefinition();
    }

    #endregion

    #region Test

    class MyCollection1<TKey, TValue> : Dictionary<TKey, TValue>, IEnumerable<TValue>
    {
        IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() { return Values.GetEnumerator(); }
    }

    class MyCollection2<TKey, TValue> : List<KeyValuePair<TKey, TValue>>, IEnumerable<TValue>
    {
        IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator()
        {
            IEnumerable<KeyValuePair<TKey, TValue>> e = this;
            return e.Select(item => item.Value).GetEnumerator();
        }
    }

    class MyClass1
    {
        public MyCollection1<int, string> Items1 = new MyCollection1<int, string>();
    }

    class MyClass2
    {
        public MyCollection2<int, string> Items2 = new MyCollection2<int, string>();
    }

    private static FieldInfo GetField<T, V>(Expression<Func<T, V>> e)
    {
        return (FieldInfo)((MemberExpression)e.Body).Member;
    }

    public static void Test()
    {
        var source = new MyClass1();
        for (int i = 0; i < 10; i++) source.Items1.Add(i + 1, new string((char)('A' + i), 1));
        var target = new MyClass2();
        var sourceField = GetField((MyClass1 c) => c.Items1);
        var targetField = GetField((MyClass2 c) => c.Items2);
        // Merge source into target using dynamic approach
        MergeCollection(sourceField, source, targetField, target);
        // Merge target back into source using reflection approach
        MergeCollection2(targetField, target, sourceField, source);
    }

    #endregion
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Okay, so I've tried these and ended up with a RuntimeBinderException on the dynamic approach when calling the Merge(). I'm not sure why. If I make both arguments dynamic rather than ICollection and ignore the generic type, it works. With that in mind, here's the performance difference for 10,000 items: My original code: 63 ms, Dynamic Method: 35 ms, and your version of Reflection: 15 ms. – bjhuffine Aug 13 '15 at 17:10
  • What's the best approach for awarding points? You've helped me improve the process, but Deepak Bhatia answered the question directly. – bjhuffine Aug 13 '15 at 17:13
  • Interesting, you really found another way to accomplish the same thing - a dynamic **implementation**. But as you noticed, it would have a similar performance to pure reflection. My goal was to have as much as possible complied code, and just to call it somehow. Since dynamic **binding** to a generic method looks like is not working always, now I think the "reflection only" approach is preferable. Regarding your question, it's up to you. I'm interested in solving issues that I find challenging. I'm a "performance" guy and always look for the best solution. Glad if that helped you. Cheers. – Ivan Stoev Aug 14 '15 at 12:05
0

As you correctly said the issue is with the Cast method. Because you are not passing the proper type in Cast method you are getting wrong IEnumrable. The solution is not to call the Cast method with the type "object", instead use proper type. As you don't have proper type information at compile time you may need to call Cast using reflection, something like this:

var castMethod = typeof(Enumerable).GetMethod("Cast", BindingFlags.Static | BindingFlags.Public);
var castGenericMethod = castMethod.MakeGenericMethod(new Type[] {  collectionParamType});
secondaryCollection = castGenericMethod.Invoke(null, new object[] {secondaryCollectionObject})

Note: I have just typed the above code in my iPad so there may be some syntax issues.

Deepak Bhatia
  • 1,090
  • 1
  • 8
  • 13
  • Sorry for the late reply. We deployed an application update company-wide and my day has been crazy. I tried this and it does get the collection as the KeyValuePair<,> type. I can even cast secondaryCollection as an IEnumerable, but as you can see in my example, there's an add.Invoke() and remove.Invoke() designed to add the first item from the secondaryCollection to the primary and then remove that first item. The ElementAt() is available to IEnumerable, but not IEnumerable. I can't cast to IEnumerable or I'm back to where I was. How do I work around this? – bjhuffine Aug 12 '15 at 20:52
  • You don't need to cast to IEnumerable, instead use reflection to get reference to ElementAt method in the same way as we have done for Cast method. – Deepak Bhatia Aug 12 '15 at 23:20
  • See this on how to call ElementAt using reflection: http://stackoverflow.com/questions/1948506/how-to-call-extension-method-elementatof-listt-with-reflection – Deepak Bhatia Aug 12 '15 at 23:31
  • Thank you Deepak, you answered my question directly. If you see my response to Ivan below, what's the best way to award points? – bjhuffine Aug 13 '15 at 17:32
  • I don,t think u can distribute points among us. It purely your decision to whome you award, but I am glad my solution helped. See this on how points can be awarded:http://stackoverflow.com/help/bounty – Deepak Bhatia Aug 14 '15 at 01:32
0

This might work (just pass the collections as source and target):

public static void MoveItems(dynamic source, dynamic target) {
    MoveItemsImpl(source, target);
}

public static void MoveItemsImpl<T>(ICollection<T> source, dynamic target) {
    foreach(T item in source)
        target.Add(item);
    source.Clear();//optional
}

A bit hacky, but worth a try :)

Zotta
  • 2,513
  • 1
  • 21
  • 27