Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / containers

How to Get a Collection Element Type Using Reflection in C#

4.36/5 (12 votes)
5 May 2020MIT3 min read 35.4K   148  
This tip shows you a robust way to get the type of items a collection can hold. It works with non-generic collections too.
This tip provides a way to query a collection type to determine the types of items it can hold. It support both generic collections and non-generic collections, using a variety of queries to ensure it's relatively robust.

Introduction

With this tip, I will endeavor to show you, gentle reader, how to use reflection to query a collection class for its element type. What may seem relatively easy at first quickly becomes complicated where typed collections are involved that do not implement IEnumerable<T>. This will be the case with typed collections that were created before .NET 2.0. These collections are surprisingly common. For example, much of Windows Forms as well as the CodeDOM have typed collections like this. Getting the element type is far more involved for these collections.

Conceptualizing this Mess

We may sometimes need to get the element type of a collection through reflection. I typically run into this while writing code generators. This is trivial for .NET post 1.1, but prior to that, there was no standard interface for typed collections, due to the impossibility of creating a generic interface that could handle that.

For getting a generic collection type, all we do is query for the IEnumerable<T> interface and then return whatever T is, in this case. Easy peasy, and it works on dictionaries, too.

For getting a non-generic collection element type, we must use some heuristics, unfortunately.

The first thing we do is query for the IDictionary interface. If we find it, we return DictionaryEntry.

If that doesn't bear fruit, next we query for IList and if we find it, we look for a public indexer property that takes a single integer parameter, and returns some type other than object.

Finally, if we can't find that, we look for ICollection, and look for an Add() method with a single parameter that is not of type object. I've found this to be the most reliable way to determine the element type of a collection.

Finally if that doesn't work, we look for IEnumerable and if we find it, we return the object type. Otherwise, we return null indicating that it's not a collection type. We could query the Current property on the enumerator's IEnumerator interface but there's no reliable way to get the type of the enumerator without calling GetEnumerator() on an instance. In practice, I don't think I've seen too many typed enumerator implementations anyway that aren't generic.

Coding this Mess

Like I usually do, I'll post the code nearly in its entirety and then we'll address it top to bottom:

C#
static partial class ReflectionUtility
{
    /// <summary>
    /// Indicates whether or not the specified type is a list.
    /// </summary>
    /// <param name="type">The type to query</param>
    /// <returns>True if the type is a list, otherwise false</returns>
    public static bool IsList(Type type)
    {
        if (null == type)
            throw new ArgumentNullException("type");

        if (typeof(System.Collections.IList).IsAssignableFrom(type))
            return true;
        foreach (var it in type.GetInterfaces())
            if (it.IsGenericType && typeof(IList<>) == it.GetGenericTypeDefinition())
                return true;
        return false;
    }
    /// <summary>
    /// Retrieves the collection element type from this type
    /// </summary>
    /// <param name="type">The type to query</param>
    /// <returns>The element type of the collection or null if the type was not a collection
    /// </returns>
    public static Type GetCollectionElementType(Type type)
    {
        if (null == type)
            throw new ArgumentNullException("type");

        // first try the generic way
        // this is easy, just query the IEnumerable<T> interface for its generic parameter
        var etype = typeof(IEnumerable<>);
        foreach (var bt in type.GetInterfaces())
            if (bt.IsGenericType && bt.GetGenericTypeDefinition() == etype)
                return bt.GetGenericArguments()[0];
            
        // now try the non-generic way

        // if it's a dictionary we always return DictionaryEntry
        if (typeof(System.Collections.IDictionary).IsAssignableFrom(type))
            return typeof(System.Collections.DictionaryEntry);
            
        // if it's a list we look for an Item property with an int index parameter
        // where the property type is anything but object
        if (typeof(System.Collections.IList).IsAssignableFrom(type))
        {
            foreach (var prop in type.GetProperties())
            {
                if ("Item" == prop.Name && typeof(object)!=prop.PropertyType)
                {
                    var ipa = prop.GetIndexParameters();
                    if (1 == ipa.Length && typeof(int) == ipa[0].ParameterType)
                    {
                        return prop.PropertyType;
                    }
                }
            }
        }

        // if it's a collection, we look for an Add() method whose parameter is 
        // anything but object
        if(typeof(System.Collections.ICollection).IsAssignableFrom(type))
        {
            foreach(var meth in type.GetMethods())
            {
                if("Add"==meth.Name)
                {
                    var pa = meth.GetParameters();
                    if (1 == pa.Length && typeof(object) != pa[0].ParameterType)
                        return pa[0].ParameterType;
                }
            }
        }
        if (typeof(System.Collections.IEnumerable).IsAssignableFrom(type))
            return typeof(object);
        return null;
    }
}

First, we have an IsList() method which I didn't cover above. It's a utility method I find myself needing quite a bit when I'm reflecting on collections, so I've provided it here. All it does is determine if the passed in type is a list.

Now, in GetCollectionElementType(), we're going through the steps I outlined in the concepts portion of the article. First, we try the generic way to determine an element type, and if we're unsuccessful, we head to the non-generic testing portion where we look first for the this[] indexer property and then if that fails, the Add() method.

Using the code is dead simple:

C#
Console.WriteLine(ReflectionUtility.GetCollectionElementType(typeof(List<string>)));
Console.WriteLine(ReflectionUtility.GetCollectionElementType(typeof(List<int>)));
Console.WriteLine(ReflectionUtility.GetCollectionElementType(typeof(CodeNamespaceCollection)));
Console.WriteLine(ReflectionUtility.GetCollectionElementType(typeof(CodeStatementCollection)));

That should be enough to get you going with this code. Enjoy!

History

  • 5th May, 2020 - Initial submission

License

This article, along with any associated source code and files, is licensed under The MIT License