Click here to Skip to main content
16,016,882 members
Articles / Containers
Tip/Trick

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

Rate me:
Please Sign up or sign in to vote.
4.36/5 (12 votes)
5 May 2020MIT3 min read 35.2K   148   8   5
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


Written By
United States United States
Just a shiny lil monster. Casts spells in C++. Mostly harmless.

Comments and Discussions

 
BugIncorrect element type for PropertyDescriptorCollection Pin
Sergei Petrik25-Sep-21 4:27
Sergei Petrik25-Sep-21 4:27 
GeneralRe: Incorrect element type for PropertyDescriptorCollection Pin
honey the codewitch25-Sep-21 8:52
mvahoney the codewitch25-Sep-21 8:52 
The key there is GetIndexParameters() and that can work, but as I recall, this does the same thing as that under the covers. PropertyDescriptors are wrappers for underlying reflection calls. Dictionaries themselves do not return Property Descriptors, but old .NET 1.x dictionaries do return DictionaryEntry - it's actually coded to IDictionary in 1.x

Edit: The above is off the cuff. I haven't visited this code in ages, so my understanding is fuzzy. I'd have to look at it again.
Real programmers use butterflies

GeneralRe: Incorrect element type for PropertyDescriptorCollection Pin
honey the codewitch26-Sep-21 21:19
mvahoney the codewitch26-Sep-21 21:19 
GeneralMy vote of 5 Pin
GerVenson6-May-20 1:36
professionalGerVenson6-May-20 1:36 
GeneralRe: My vote of 5 Pin
honey the codewitch6-May-20 4:14
mvahoney the codewitch6-May-20 4:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.