Updated on 2020-05-05
This tip shows you a robust way to get the type of items a collection can hold. It works with non-generic collections too.
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
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
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.
Like I usually do, I'll post the code nearly in its entirety and then we'll address it top to bottom:
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:
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!