Reflection
C#
Inheritance
Derived Classes
Programming Techniques

Get all derived types of a type

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

Finding all types derived from a base class or implementing an interface is a common reflection task in C#. It shows up in plugin systems, dependency injection registration, serializers, and factories. The core operation is simple, but production-safe scanning needs to account for interfaces, abstract types, assembly boundaries, and partially loadable assemblies.

The Basic Reflection Pattern

The usual approach is:

  1. choose the assemblies to scan
  2. read their types
  3. filter with IsAssignableFrom
  4. exclude the base type itself and any abstract types if needed
csharp
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Reflection;
5
6public static class TypeScanner
7{
8    public static IEnumerable<Type> GetDerivedTypes(Type baseType, IEnumerable<Assembly> assemblies)
9    {
10        return assemblies
11            .SelectMany(GetLoadableTypes)
12            .Where(t => baseType.IsAssignableFrom(t))
13            .Where(t => t != baseType)
14            .Where(t => !t.IsAbstract);
15    }
16
17    private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
18    {
19        try
20        {
21            return assembly.GetTypes();
22        }
23        catch (ReflectionTypeLoadException ex)
24        {
25            return ex.Types.Where(t => t != null)!;
26        }
27    }
28}

This pattern works for both subclasses and interface implementations.

Why IsAssignableFrom Is Usually Right

For interface scanning and inheritance scanning in one method, IsAssignableFrom is the practical choice.

csharp
bool ok = typeof(IPlugin).IsAssignableFrom(typeof(MyPlugin));

If you use IsSubclassOf, interfaces are excluded and some valid matches are missed.

So the common rule is:

  • use IsSubclassOf only for strict class inheritance checks
  • use IsAssignableFrom when you want subclasses or interface implementations

Scanning the Current AppDomain

If you truly want every currently loaded assembly, scan the current AppDomain.

csharp
1var pluginTypes = TypeScanner.GetDerivedTypes(
2    typeof(IPlugin),
3    AppDomain.CurrentDomain.GetAssemblies());
4
5foreach (var type in pluginTypes)
6{
7    Console.WriteLine(type.FullName);
8}

This is convenient, but it only finds assemblies that are already loaded. If a plugin assembly exists on disk and was never loaded, it will not appear here automatically.

Scanning Specific Assemblies Is Often Better

In real applications, scanning every loaded assembly can be slow and noisy. It is usually better to target the assemblies you own or the plugin assemblies you loaded intentionally.

csharp
1var assemblies = new[]
2{
3    typeof(MyAppMarker).Assembly,
4    typeof(MyPluginMarker).Assembly
5};
6
7var handlers = TypeScanner.GetDerivedTypes(typeof(ICommandHandler), assemblies);

This reduces surprises and improves startup time.

Direct Children Versus Any Descendant

Sometimes you want only immediate subclasses, not every descendant.

csharp
var directChildren = assemblies
    .SelectMany(a => a.GetTypes())
    .Where(t => t.BaseType == typeof(BaseHandler));

That is different from IsAssignableFrom, which includes grandchildren and deeper inheritance chains.

Generic Types Need Extra Care

Open generic definitions are a special case. Suppose you want all implementations of IHandler<T>. The type check may need to inspect each implemented interface and compare generic definitions.

csharp
1var matches = AppDomain.CurrentDomain.GetAssemblies()
2    .SelectMany(a => a.GetTypes())
3    .Where(t => t.IsClass && !t.IsAbstract)
4    .Where(t => t.GetInterfaces().Any(i =>
5        i.IsGenericType &&
6        i.GetGenericTypeDefinition() == typeof(IHandler<>)));

This is a common pattern in DI auto-registration.

Cache Results When the Set Is Stable

Reflection scans are not free. If the application loads the same assemblies and asks the same question repeatedly, cache the result.

csharp
private static readonly Lazy<IReadOnlyList<Type>> _cachedPlugins = new(() =>
    TypeScanner.GetDerivedTypes(typeof(IPlugin), AppDomain.CurrentDomain.GetAssemblies())
        .ToList());

Caching is especially useful during startup or command dispatch registration.

Common Pitfalls

A common mistake is assuming AppDomain.CurrentDomain.GetAssemblies() includes assemblies that exist on disk but were never loaded. It does not.

Another mistake is calling Assembly.GetTypes() without handling ReflectionTypeLoadException. One problematic type can otherwise break the whole scan.

Developers also often forget to filter out abstract classes, which leads to runtime failures when they try to instantiate the results.

Summary

  • Use reflection to scan selected assemblies and filter types.
  • 'IsAssignableFrom is usually the right predicate for subclasses and interface implementations.'
  • Handle ReflectionTypeLoadException so partial load failures do not abort the scan.
  • Filter abstract types if you need instantiable implementations only.
  • Cache the results when the type set is stable and reused.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD