Inspect Generic Types Using Reflection in C
In .NET, inspecting generics through reflection requires
understanding the distinction between open generic type definitions and
closed constructed types. This guide explains how to use the
System.Type API to determine whether a type is generic,
retrieve its generic type definition, extract underlying type arguments
or parameters, inspect parameter constraints, and construct new generic
types at runtime.
Understanding Generic Type States
Reflection categorizes generic types into two primary states:
- Generic Type Definition: An unbound generic
template containing type parameters rather than concrete types (for
example,
List<>orDictionary<,>). - Constructed Generic Type: A generic type where
specific types have been assigned to its type parameters (for example,
List<int>orDictionary<string, int>). A constructed type is closed if all type arguments are concrete, or open if it still contains unbound generic parameters (often encountered inside generic classes or methods).
Key Reflection Properties
You can inspect the generic state of any Type instance
using these boolean properties:
IsGenericType: Returnstrueif the type represents any generic structure (either a definition or a constructed type).IsGenericTypeDefinition: Returnstrueonly if the instance is the open generic template (e.g.,typeof(List<>)).IsConstructedGenericType: Returnstrueif type arguments have been applied to the definition (e.g.,typeof(List<int>)).ContainsGenericParameters: Returnstrueif any type arguments are open generic parameters that have not yet been assigned concrete types.
Type openType = typeof(List<>);
Type closedType = typeof(List<int>);
Console.WriteLine(openType.IsGenericTypeDefinition); // True
Console.WriteLine(closedType.IsGenericTypeDefinition); // False
Console.WriteLine(closedType.IsConstructedGenericType); // TrueNavigating Between Definitions and Constructed Types
To retrieve the original generic blueprint from a constructed type,
call GetGenericTypeDefinition():
Type constructed = typeof(Dictionary<string, int>);
if (constructed.IsGenericType && !constructed.IsGenericTypeDefinition)
{
Type definition = constructed.GetGenericTypeDefinition();
Console.WriteLine(definition.FullName); // System.Collections.Generic.Dictionary`2
}Conversely, you can turn a generic type definition into a constructed
type dynamically by calling MakeGenericType():
Type definition = typeof(Dictionary<,>);
Type[] typeArgs = { typeof(string), typeof(int) };
Type constructed = definition.MakeGenericType(typeArgs);
Console.WriteLine(constructed.FullName);
// System.Collections.Generic.Dictionary`2[[System.String...],[System.Int32...]]Inspecting Generic Arguments and Parameters
The GetGenericArguments() method behaves differently
depending on whether you invoke it on a generic type definition or a
constructed type:
- On a Generic Type Definition, it returns an array
of
Typeobjects representing the generic type parameters (e.g.,TKey,TValue). - On a Constructed Type, it returns an array
representing the substituted type arguments (e.g.,
string,int).
Type dictionaryType = typeof(Dictionary<string, int>);
Type[] typeArguments = dictionaryType.GetGenericArguments();
foreach (Type t in typeArguments)
{
Console.WriteLine($"Argument: {t.Name}"); // Argument: String, Argument: Int32
}Inspecting Constraints on Type Parameters
When working with a generic type parameter (obtained from a generic
definition), you can inspect its constraints using
GetGenericParameterConstraints() and
GenericParameterAttributes:
public class Repository<T> where T : class, IComparable, new() { }
Type repoType = typeof(Repository<>);
Type tParam = repoType.GetGenericArguments()[0];
// Check base class or interface constraints
Type[] constraints = tParam.GetGenericParameterConstraints();
foreach (Type constraint in constraints)
{
Console.WriteLine($"Constraint: {constraint.Name}"); // IComparable
}
// Check special constraints (class, struct, new())
GenericParameterAttributes attributes = tParam.GenericParameterAttributes;
bool hasReferenceTypeConstraint = (attributes & GenericParameterAttributes.ReferenceTypeConstraint) != 0;
bool hasDefaultConstructorConstraint = (attributes & GenericParameterAttributes.DefaultConstructorConstraint) != 0;
bool hasNotNullableValueTypeConstraint = (attributes & GenericParameterAttributes.NotNullableValueTypeConstraint) != 0;
Console.WriteLine($"where T : class -> {hasReferenceTypeConstraint}");
Console.WriteLine($"where T : new() -> {hasDefaultConstructorConstraint}");Inspecting Generic Methods
Generic methods follow the same pattern using methods available on
MethodInfo:
MethodInfo openMethod = typeof(Enumerable)
.GetMethods()
.First(m => m.Name == "ToArray" && m.IsGenericMethodDefinition);
// Check method generic state
Console.WriteLine(openMethod.IsGenericMethodDefinition); // True
// Make a concrete generic method
MethodInfo closedMethod = openMethod.MakeGenericMethod(typeof(int));
Console.WriteLine(closedMethod.IsGenericMethod); // True
Console.WriteLine(closedMethod.IsGenericMethodDefinition); // False