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:
static partial class ReflectionUtility
{
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;
}
public static Type GetCollectionElementType(Type type)
{
if (null == type)
throw new ArgumentNullException("type");
var etype = typeof(IEnumerable<>);
foreach (var bt in type.GetInterfaces())
if (bt.IsGenericType && bt.GetGenericTypeDefinition() == etype)
return bt.GetGenericArguments()[0];
if (typeof(System.Collections.IDictionary).IsAssignableFrom(type))
return typeof(System.Collections.DictionaryEntry);
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(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!
History
- 5th May, 2020 - Initial submission
Just a shiny lil monster. Casts spells in C++. Mostly harmless.