Detecting Generic Types with C# Reflection
c# reflection generic type detection: Learn how to detect generic types, read their arguments, and resolve generic interfaces using C# reflection with practical code e...
c# reflection generic type detection requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Detecting whether a Type represents a generic type, and then extracting its generic arguments, is a recurring task in C# reflection. Libraries that map entities, route messages, or build dependency graphs all need to answer the same question: does this type implement IEnumerable<T>, and if so, what is T? The reflection API for this is compact, but the distinction between a generic type definition, a constructed generic type, and a type that still contains open generic parameters is easy to miss. This article walks through the Type members used for C# reflection generic type detection and shows how to apply them to real scenarios.
The Type Members That Reveal Generic Structure
The System.Type class exposes four members that cover most generic detection needs:
| Member | Returns | Meaning |
|---|---|---|
IsGenericType | bool | True if the type is generic, whether open or closed |
IsGenericTypeDefinition | bool | True only for open definitions such as List<> |
IsConstructedGenericType | bool | True when type arguments have been supplied |
ContainsGenericParameters | bool | True if any generic parameters remain unresolved |
IsGenericType is the first check you will normally perform. It returns true for both typeof(List<>) and typeof(List<int>), so it tells you only that the type has a generic form. The other three properties refine that answer.
IsGenericTypeDefinition identifies the open definition, the form you use when calling MakeGenericType. IsConstructedGenericType identifies a type that has been constructed from a definition, even if some of its arguments are themselves generic parameters. ContainsGenericParameters reports whether the type is still open, meaning it cannot be instantiated yet.
Open, Closed, and Open Constructed Types
The four properties are best understood together. Consider this inspection routine:
using System; using System.Collections.Generic; void Inspect(Type type) { Console.WriteLine($"{type.FullName}:"); Console.WriteLine($" IsGenericType: {type.IsGenericType}"); Console.WriteLine($" IsGenericTypeDefinition: {type.IsGenericTypeDefinition}"); Console.WriteLine($" IsConstructedGenericType: {type.IsConstructedGenericType}"); Console.WriteLine($" ContainsGenericParameters: {type.ContainsGenericParameters}"); } Inspect(typeof(List<>)); // open generic definition Inspect(typeof(List<int>)); // closed constructed type
The output for List<> shows IsGenericType = true, IsGenericTypeDefinition = true, IsConstructedGenericType = false, and ContainsGenericParameters = true. For List<int>, the values are true, false, true, and false.
The interesting case appears inside a generic class. If T is a type parameter of the enclosing class, then typeof(List<T>) is a constructed type because List has been given an argument, but it is still open because that argument is unresolved:
class Processor<T> { public void Run() { Type t = typeof(List<T>); Console.WriteLine(t.IsConstructedGenericType); // True Console.WriteLine(t.ContainsGenericParameters); // True } }
This combination is called an open constructed type. It matters when you build generic types dynamically or when you walk a type hierarchy and encounter base classes that reference the enclosing class's parameters.
Reading Generic Arguments and Definitions
Once you know a type is generic, GetGenericArguments() returns the type arguments in declaration order:
Type[] arguments = typeof(Dictionary<string, int>).GetGenericArguments(); // arguments[0] == typeof(string) // arguments[1] == typeof(int)
For an open definition, the same method returns the generic parameters rather than concrete types:
Type[] parameters = typeof(Dictionary<,>).GetGenericArguments(); // parameters[0].IsGenericParameter == true // parameters[1].IsGenericParameter == true
The inverse operation is GetGenericTypeDefinition(), which returns the open definition for a constructed type:
Type definition = typeof(Dictionary<string, int>).GetGenericTypeDefinition(); // definition == typeof(Dictionary<,>)
GetGenericTypeDefinition() throws InvalidOperationException if the type is not generic, so guard it with IsGenericType first. The returned definition is the same cached Type instance the runtime uses for the open definition, which means reference equality with typeof(Dictionary<,>) works reliably.
MakeGenericType performs the reverse direction: it takes an open definition and an array of arguments and produces a constructed type. This is how you build a generic type at runtime when you only know the argument types after inspection.
Detecting a Generic Interface Implementation
A common requirement is checking whether a type implements a specific generic interface, such as IEnumerable<T> or IRepository<T>. GetInterfaces() returns every interface the type implements, including inherited ones, so you can scan for a matching definition:
using System; using System.Linq; public static bool ImplementsGenericInterface(Type type, Type genericInterfaceDefinition) { if (!genericInterfaceDefinition.IsGenericTypeDefinition) { throw new ArgumentException( "Pass an open generic definition such as typeof(IEnumerable<>).", nameof(genericInterfaceDefinition)); } return type.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == genericInterfaceDefinition); } bool result = ImplementsGenericInterface(typeof(List<int>), typeof(IEnumerable<>)); // result == true
The comparison uses GetGenericTypeDefinition() because the interface found on the type is constructed, such as IEnumerable<int>, while the caller supplies the open form IEnumerable<>. Reference equality works here because the runtime returns the canonical instance for each generic type definition.
This approach does not account for variance. IEnumerable<string> is assignable to IEnumerable<object>, but GetInterfaces() on a List<string> returns IEnumerable<string>, not IEnumerable<object>. If you need variance-aware matching, combine the scan with IsAssignableFrom or handle the variance rules explicitly.
Resolving a Generic Argument from a Base Class
Another frequent scenario is extracting the type argument from a generic base class. Frameworks that use base classes like Repository<TEntity> or Handler<TMessage> need to discover TEntity or TMessage from a derived type. Walking the base class chain handles this:
public static Type GetGenericBaseArgument(Type type, Type genericBaseDefinition, int argumentIndex = 0) { Type current = type; while (current != null && current != typeof(object)) { if (current.IsGenericType && current.GetGenericTypeDefinition() == genericBaseDefinition) { return current.GetGenericArguments()[argumentIndex]; } current = current.BaseType; } return null; }
For a class OrderRepository : Repository<Order>, calling GetGenericBaseArgument(typeof(OrderRepository), typeof(Repository<>)) returns typeof(Order). The loop matters because the generic base may be several levels up, and BaseType returns null only at the top of the hierarchy.
If the base type is an interface rather than a class, use the interface scan from the previous section instead, because BaseType does not traverse interfaces.
Performance: Caching Recurring Reflection Lookups
Reflection calls are not free. GetGenericArguments() allocates a new array on every call, GetInterfaces() builds a collection of all implemented interfaces, and each metadata lookup carries runtime cost. When generic type detection runs per request, per message, or inside a loop, the overhead accumulates.
Caching the results is the standard mitigation. A ConcurrentDictionary keyed by Type works well because Type instances are cached by the runtime, so the key is stable:
using System.Collections.Concurrent; private static readonly ConcurrentDictionary<Type, Type[]> GenericArgumentsCache = new(); public static Type[] GetGenericArgumentsCached(Type type) { return GenericArgumentsCache.GetOrAdd(type, static t => t.GetGenericArguments()); }
The same pattern applies to interface scans and base class resolution. The cache grows with the number of distinct types it sees, so for unbounded input sets, consider a bounded cache or periodic eviction. For a fixed set of types, such as the types registered at application startup, an unbounded cache is usually fine.
Edge Cases: Nested Generics and Open Constructed Types
Nested generic types combine the type parameters of every enclosing type. Given:
class Outer<T> { public class Inner<U> { } }
typeof(Outer<>.Inner<>) is a generic type definition whose GetGenericArguments() returns both T and U in that order. For the constructed form typeof(Outer<int>.Inner<string>), the same method returns int and string. When you resolve arguments from a nested type, remember that the argument index includes the outer parameters.
Open constructed types, described earlier, also appear in real hierarchies. A base class like Repository<T> inside a generic service may be constructed with the service's own type parameter. In that case ContainsGenericParameters returns true even though IsConstructedGenericType is also true. Code that only checks IsGenericTypeDefinition will miss this state, so decide explicitly which condition you need: an open definition, a fully closed type, or a type that is constructed but still open.
The practical rule is to check IsGenericType before calling GetGenericTypeDefinition() or GetGenericArguments(), and to treat ContainsGenericParameters as the signal for whether the type can be instantiated. With those checks in place, generic type detection becomes a reliable part of any reflection-based library.