Skip to content

Advanced Usage

Marcus Ackre Medina edited this page Sep 13, 2026 · 1 revision

Advanced Usage

Beyond the basic GetClassesByInterface/GetClassesByInheritance calls in Getting Started, a few implementation details affect how you should use this library.

The cache is keyed per EnumerateClasses<T>, per assembly, per lookup mode

EnumerateClasses<T> is a generic static class, so EnumerateClasses<ICommand> and EnumerateClasses<IPlugin> each get their own independent cache — there's no cross-contamination between different Ts. Within one T, the cache key is (assembly, isInheritanceLookup), so calling both GetClassesByInterface(useCache: true) and GetClassesByInheritance(useCache: true) on the same T caches them separately, and scanning a different assembly gets its own entry too.

// These are cached independently, even though it's the same T:
var byInterface   = EnumerateClasses<ICommand>.GetClassesByInterface(useCache: true);
var byInheritance = EnumerateClasses<ICommand>.GetClassesByInheritance(useCache: true);

ClearCache() is also per-T — call it on the specific EnumerateClasses<T> whose cache is stale, not globally:

EnumerateClasses<IPlugin>.ClearCache(); // only clears IPlugin's cache

Prefer List* over Get* when you don't need instances

GetClassesByInterface/GetClassesByInheritance construct an instance of every matching type via Activator.CreateInstance. If you only need to inspect the types themselves — for logging, filtering by attribute, or deciding what to register in a DI container — use ListClassesByInterface/ListClassesByInheritance instead, which return Type objects and skip instantiation entirely.

var pluginTypes = EnumerateClasses<IPlugin>.ListClassesByInterface();
foreach (var type in pluginTypes)
{
    Console.WriteLine($"Found plugin type: {type.FullName}");
}

A class must be non-abstract with a public parameterless constructor

Both Get* methods filter to !type.IsAbstract && type.GetConstructor(Type.EmptyTypes) != null before attempting instantiation. A matching type without a public parameterless constructor is silently excluded — it won't appear in the results and won't throw. If a type passes that filter but still fails to construct (e.g. its constructor throws), CreateInstance catches MissingMethodException, TargetInvocationException, and TypeLoadException internally and drops that instance from the results rather than propagating the exception.

Inheritance matching is direct base class only

GetClassesByInheritance/ListClassesByInheritance match on type.BaseType == typeof(T) — only classes that inherit directly from T. A grandchild class (T -> Middle -> Leaf) is not matched by searching for T; you'd need to search for Middle to find Leaf. Interface matching (GetClassesByInterface/ListClassesByInterface) doesn't have this limitation — type.GetInterfaces() includes interfaces implemented anywhere in the hierarchy.

assembly: null means the calling assembly, not the executing one

When assembly is omitted, both lookup methods use Assembly.GetCallingAssembly() — the assembly of the code that called EnumerateClasses<T>, not the assembly the library itself lives in. If you wrap a call to EnumerateClasses<T> inside your own helper method, the "calling assembly" becomes your helper's assembly, not your application's — pass assembly explicitly if that distinction matters for your use case.