C#
generics
Type parameter
programming
.NET

Pass An Instantiated System.Type as a Type Parameter for a Generic Class

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

C# generics require type parameters to be known at compile time (List<int>, Dictionary<string, object>), but sometimes you have a System.Type variable at runtime and need to use it as a generic parameter. This is not directly supported by the language — you cannot write List<myType> when myType is a variable. The solution is to use reflection via Type.MakeGenericType() and Activator.CreateInstance(), or to redesign with non-generic interfaces and the strategy pattern.

The Problem

csharp
1Type myType = typeof(string);  // Known only at runtime
2
3// This does NOT compile — T must be a compile-time type
4// var list = new List<myType>();
5
6// What we want: create List<string> from the Type variable

Generics in C# are resolved at compile time. The compiler generates specialized code for each T, so it cannot accept a runtime Type variable.

Solution 1: MakeGenericType + Reflection

csharp
1using System;
2using System.Collections;
3
4Type elementType = typeof(int);  // Could come from runtime logic
5
6// Create the generic type List<int> at runtime
7Type listType = typeof(List<>).MakeGenericType(elementType);
8
9// Create an instance
10object list = Activator.CreateInstance(listType);
11
12// Call methods via reflection
13var addMethod = listType.GetMethod("Add");
14addMethod.Invoke(list, new object[] { 42 });
15addMethod.Invoke(list, new object[] { 100 });
16
17// Access via non-generic interface
18var enumerable = (IEnumerable)list;
19foreach (var item in enumerable)
20{
21    Console.WriteLine(item);  // 42, 100
22}
23
24// Get count
25var countProp = listType.GetProperty("Count");
26Console.WriteLine($"Count: {countProp.GetValue(list)}");  // 2

typeof(List<>) is the open generic type. MakeGenericType(elementType) closes it with the runtime type.

Solution 2: Generic Method via Reflection

csharp
1public class Factory
2{
3    public static T CreateDefault<T>() where T : new()
4    {
5        return new T();
6    }
7}
8
9// Call CreateDefault<SomeType>() where SomeType is a runtime Type
10Type targetType = typeof(MyClass);
11
12var method = typeof(Factory).GetMethod("CreateDefault");
13var genericMethod = method.MakeGenericMethod(targetType);
14var instance = genericMethod.Invoke(null, null);
15
16Console.WriteLine(instance.GetType());  // MyClass

MakeGenericMethod is the method equivalent of MakeGenericType — it creates a closed generic method from an open one.

Solution 3: Dynamic Dispatch with Interfaces

Design your code with a non-generic interface so callers do not need generics:

csharp
1// Non-generic interface
2public interface IRepository
3{
4    object GetById(int id);
5    void Save(object entity);
6}
7
8// Generic implementation
9public class Repository<T> : IRepository where T : class
10{
11    public object GetById(int id) => /* ... */ default(T);
12    public void Save(object entity) => Save((T)entity);
13    private void Save(T entity) { /* typed save logic */ }
14}
15
16// Factory that creates the right repository at runtime
17public static IRepository CreateRepository(Type entityType)
18{
19    Type repoType = typeof(Repository<>).MakeGenericType(entityType);
20    return (IRepository)Activator.CreateInstance(repoType);
21}
22
23// Usage
24Type modelType = typeof(Customer);
25IRepository repo = CreateRepository(modelType);
26repo.Save(new Customer { Name = "Alice" });

Solution 4: Dictionary of Type-Specific Actions

csharp
1public class TypeDispatcher
2{
3    private readonly Dictionary<Type, Action<object>> _handlers = new();
4
5    public void Register<T>(Action<T> handler)
6    {
7        _handlers[typeof(T)] = obj => handler((T)obj);
8    }
9
10    public void Dispatch(object value)
11    {
12        Type type = value.GetType();
13        if (_handlers.TryGetValue(type, out var handler))
14        {
15            handler(value);
16        }
17        else
18        {
19            throw new InvalidOperationException($"No handler for {type}");
20        }
21    }
22}
23
24// Usage
25var dispatcher = new TypeDispatcher();
26dispatcher.Register<string>(s => Console.WriteLine($"String: {s}"));
27dispatcher.Register<int>(n => Console.WriteLine($"Int: {n}"));
28
29dispatcher.Dispatch("hello");  // String: hello
30dispatcher.Dispatch(42);       // Int: 42

Solution 5: Using dynamic

csharp
1public class Processor
2{
3    public void Process<T>(T item)
4    {
5        Console.WriteLine($"Processing {typeof(T).Name}: {item}");
6    }
7}
8
9// Use dynamic to bypass compile-time generic resolution
10Type runtimeType = typeof(string);
11var processor = new Processor();
12
13dynamic value = Convert.ChangeType("hello", runtimeType);
14processor.Process(value);  // Compiler resolves at runtime via DLR
15// Output: Processing String: hello

dynamic defers type resolution to runtime, letting the DLR dispatch to the correct generic overload.

Solution 6: Expression Trees (Performance)

For hot paths where reflection overhead matters:

csharp
1using System.Linq.Expressions;
2
3public static class GenericFactory
4{
5    private static readonly Dictionary<Type, Func<object>> _cache = new();
6
7    public static object Create(Type type)
8    {
9        if (!_cache.TryGetValue(type, out var factory))
10        {
11            var expr = Expression.Lambda<Func<object>>(
12                Expression.Convert(
13                    Expression.New(type),
14                    typeof(object)
15                )
16            );
17            factory = expr.Compile();
18            _cache[type] = factory;
19        }
20        return factory();
21    }
22}
23
24// 10x faster than Activator.CreateInstance for repeated calls
25var obj = GenericFactory.Create(typeof(MyClass));

Common Pitfalls

  • Performance overhead: MakeGenericType and Activator.CreateInstance use reflection, which is 10-100x slower than direct construction. Cache the constructed Type and compiled delegates for hot paths.
  • Generic constraints not checked at compile time: If the generic class has where T : IComparable, MakeGenericType still creates the type at runtime but will throw ArgumentException if the constraint is violated.
  • Value types and boxing: Using object as the bridge between generic and non-generic code boxes value types. For performance-sensitive code with value types, consider source generators.
  • Assembly loading issues: MakeGenericType can fail if the type is in an assembly that has not been loaded yet. Ensure all referenced assemblies are loaded.
  • Thread safety: Shared caches of generic types or compiled expressions need ConcurrentDictionary in multi-threaded scenarios.

Summary

  • C# generics require compile-time type parameters — you cannot use a Type variable directly
  • Use typeof(Generic<>).MakeGenericType(runtimeType) to construct generic types at runtime
  • Use method.MakeGenericMethod(runtimeType) for generic method invocation
  • Design non-generic interfaces (IRepository) to avoid runtime generics in calling code
  • Use dynamic for simple cases where DLR dispatch is acceptable
  • Cache constructed types and compiled expressions to avoid repeated reflection overhead

Course illustration
Course illustration

All Rights Reserved.