简体   繁体   中英

Add items to any object of ICollection<T> using reflection

I'm trying to support mapping to/from any kind of collection that implements ICollection<T> via reflection, because ICollection<T> requires implementation of the Add method.

This works fine for most common collection types, but fails for edge cases like LinkedList<T> where the Add method is hidden and can only be called by casting the LinkedList<T> to ICollection<T> .

However it's not possible to convert to ICollection<> because it is not covariant.

The other option I was considering was searching for both implicit and explicit implementations of Add, but I don't see any information on how to do this when the interface is generic?

What would be the correct approach to take?

Updated to show code snippet where I'm reflecting from xml to object mapping.

    private object CollectionXmlNodeListToObject(
         XmlNodeList nodeList, System.Type collectionType)
    {
        // this is not possible because ICollection<> is not covariant
        object collection = Convert.ChangeType(
              CreateInstanceOfType(collectionType), ICollection<>);
        Type containedType = collectionType.GetTypeInfo().GenericTypeArguments[0];

        foreach (XmlNode node in nodeList)
        {
            object value = CreateInstanceOfType(containedType);

            if (containedType.IsClass && MetaDataCache.Contains(containedType))
                value = ToObject(value, node, node.Name);
            else
                value = node.InnerText;

            // this throws NullReferenceException when the type is LinkedList,
            // because this is explicitly implemented in LinkedList
            collectionType.GetMethod("Add")
                 .Invoke(collection, new[] { value });
        }

        return collection;
    }

I am writing a small framework to map from object to xml using class and property attributes. So I cannot use generics because all of this is being done at runtime.

I initially was checking for IEnumerable before, but ran into other oddities with it (strings implement IEnumerable and are immutable) that I decided it was safest to stick to ICollection<>

With explicit interface implementation, the object has all the interface methods, but the object's Type does not.

So here's how to add an item to a LinkedList<T> , or any ICollection<T> through reflection:

        var ll = new LinkedList<int>();

        var t = typeof(int);

        var colType = typeof(ICollection<>).MakeGenericType(t);

        var addMethod = colType.GetMethod("Add");

        addMethod.Invoke(ll, new object[] { 1 });

This functionality is met at compile-time using the Cast<T>() method. You just need a run-time version, which is pretty straightforward:

static public object LateCast(this ICollection items, Type itemType)
{
    var methodDefintionForCast = typeof(System.Linq.Enumerable)
        .GetMethods(BindingFlags.Static | BindingFlags.Public)
        .Where(mi => mi.Name == "Cast")
        .Select(mi => mi.GetGenericMethodDefinition())
        .Single(gmd => gmd != null && gmd.GetGenericArguments().Length == 1);

    var method = methodDefintionForCast.MakeGenericMethod(new Type[] { itemType });
    return method.Invoke(null, new[] { items });
}

Now you can take any non-generic collection and make it generic at run-time. For example, these two are equivalent:

var list = nodeList.Cast<XmlNode>();
object list = nodeList.LateCast(typeof(XmlNode));

And you can convert a whole collection with this:

static public IEnumerable ConvertToGeneric(this ICollection source, Type collectionType)
{
    return source.LateCast(collectionType.GetGenericArguments()[0]) as IEnumerable;
}

object list = nodeList.ConvertToGeneric(nodeList, typeof(ICollection<XmlNode>));

This solution works with linked lists as well as all the other collection types.

See my working example on DotNetFiddle

Pretty much all .NET collections take an IEnumerable<T> as the constructor, so you could make use of that:

private static object CollectionXmlNodeListToObject(System.Type collectionType)
{
    // T
    Type containedType = collectionType.GetTypeInfo().GenericTypeArguments[0];
    // List<T>
    Type interimListType = typeof(List<>).MakeGenericType(containedType);
    // IEnumerable<T>
    Type ienumerableType = typeof(IEnumerable<>).MakeGenericType(containedType);

    IList interimList = Activator.CreateInstance(interimListType) as IList;

    interimList.Add(null);
    interimList.Add(null);
    interimList.Add(null);
    interimList.Add(null);

    // If we can directly assign the interim list, do so
    if (collectionType == interimListType || collectionType.IsAssignableFrom(interimListType))
    {
        return interimList;
    }

    // Try to get the IEnumerable<T> constructor and use that to construct the collection object
    var constructor = collectionType.GetConstructor(new Type[] { ienumerableType });
    if (constructor != null)
    {
        return constructor.Invoke(new object[] { interimList });
    }
    else
    {
        throw new NotImplementedException();
    }
}   

Try it online

Obviously you could optimise this by moving the list population to another method, and then maybe use your existing method as far as you can, and then use this where you can't.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM