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:

  1. Generic Type Definition: An unbound generic template containing type parameters rather than concrete types (for example, List<> or Dictionary<,>).
  2. Constructed Generic Type: A generic type where specific types have been assigned to its type parameters (for example, List<int> or Dictionary<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: Returns true if the type represents any generic structure (either a definition or a constructed type).
  • IsGenericTypeDefinition: Returns true only if the instance is the open generic template (e.g., typeof(List<>)).
  • IsConstructedGenericType: Returns true if type arguments have been applied to the definition (e.g., typeof(List<int>)).
  • ContainsGenericParameters: Returns true if 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); // True

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 Type objects 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