C# Reflection: Inspect Types and Assemblies at Runtime
Reflection in C# is a powerful feature provided by the
System.Reflection namespace that allows applications to
inspect, discover, and invoke metadata about assemblies, modules, and
types at runtime. Instead of relying solely on static, compile-time
declarations, reflection enables programs to interrogate their own
internal structure or external compiled binaries. This article explains
how reflection works beneath the surface, how the Common Language
Runtime (CLR) exposes metadata, and the practical mechanisms developers
use to dynamically inspect assemblies, types, and their members.
The Role of Metadata in the CLR
To understand reflection, you must understand how .NET manages compiled code. When you compile C# source code, the compiler generates Common Intermediate Language (CIL) along with detailed metadata tables. These metadata tables contain comprehensive descriptions of every component within the binary:
- Assembly and Module Manifests: Identities, versions, culture, and external dependencies.
- Type Tables: Names, namespaces, base classes, implemented interfaces, and visibility scopes.
- Member Tables: Methods, fields, properties, events, parameter lists, return types, and custom attributes.
This metadata is embedded directly into the compiled Portable Executable (PE) file. When an assembly is loaded, the CLR reads this metadata. Reflection acts as an API layer that parses these tables and translates them into managed objects that developers can query programmatically.
Inspecting Assemblies
The highest level of inspection begins with the Assembly
class. Reflection allows you to dynamically load an assembly from disk,
examine the current running assembly, or query the assemblies loaded
into the current application domain.
using System.Reflection;
// Inspect the currently executing assembly
Assembly currentAssembly = Assembly.GetExecutingAssembly();
// Dynamically load an external assembly
Assembly externalAssembly = Assembly.LoadFrom("Plugin.dll");
// Retrieve all types defined within the assembly
Type[] exportedTypes = externalAssembly.GetExportedTypes();Through the Assembly object, you can inspect:
- Full identity and version information (
FullName,GetName()). - Entry points (
EntryPoint). - Custom assembly-level attributes (such as
AssemblyVersionAttributeorTargetFrameworkAttribute). - Contained modules and resources.
Inspecting Types
The fundamental core of runtime inspection is the
System.Type class. A Type instance represents
type declarations including classes, interfaces, value types, enums, and
delegates.
You obtain a Type object in multiple ways:
- Compile-time token:
Type t = typeof(MyClass); - Runtime object instance:
Type t = instance.GetType(); - String lookup:
Type t = Type.GetType("Namespace.MyClass");
Once you possess a Type reference, you can evaluate its
structural properties:
Type type = typeof(List<string>);
Console.WriteLine($"Name: {type.Name}"); // List`1
Console.WriteLine($"Is Generic: {type.IsGenericType}"); // True
Console.WriteLine($"Base Type: {type.BaseType}"); // System.Object
Console.WriteLine($"Is Class: {type.IsClass}"); // True
// Inspect implemented interfaces
foreach (Type iface in type.GetInterfaces())
{
Console.WriteLine($"Implements: {iface.Name}");
}This structural analysis allows frameworks (like Dependency Injection containers or Object-Relational Mappers) to verify whether a given type implements an interface or inherits from a base class before attempting instantiation.
Inspecting Members
Once a Type is targeted, reflection allows granular
inspection of its internal members through classes derived from
MemberInfo, including:
MethodInfo: Describes methods, their signatures, and return types.PropertyInfo: Exposes property getters, setters, and data types.FieldInfo: Describes internal fields, including backing fields.ConstructorInfo: Details constructors and parameter requirements.EventInfo: Describes event registrations and handler delegates.
Using BindingFlags
By default, reflection methods like GetMembers() or
GetMethods() return only public, instance-level members. To
inspect internal, private, or static members, you pass
BindingFlags bitmasks:
Type userType = typeof(User);
// Retrieve all private and public instance fields
FieldInfo[] fields = userType.GetFields(
BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public
);
// Inspect specific methods and their parameters
MethodInfo method = userType.GetMethod("CalculateTax", BindingFlags.Instance | BindingFlags.Public);
if (method != null)
{
foreach (ParameterInfo param in method.GetParameters())
{
Console.WriteLine($"Param: {param.Name}, Type: {param.ParameterType}");
}
}Inspecting Attributes
Custom attributes allow metadata to be attached to declarations.
Reflection provides mechanisms such as
GetCustomAttributes() to read these attributes at runtime,
which forms the basis for validation frameworks, serialization engines
(like System.Text.Json), and routing attributes in ASP.NET
Core:
if (type.IsDefined(typeof(SerializableAttribute), inherit: true))
{
// Execute logic for serializable types
}Trade-offs: Performance and AOT Considerations
While reflection provides immense flexibility for building extensible architectures, dynamic plugins, and test runners, it introduces trade-offs:
- Performance Overhead: Navigating metadata tables and performing runtime type checks bypasses standard compiler optimizations and involves significant overhead compared to direct method calls.
- Type Safety: Member lookups rely on strings, which removes compile-time type verification and makes refactoring more prone to runtime failures.
- Trimming and Native AOT: Modern .NET features like Native AOT (Ahead-Of-Time compilation) and IL trimming strip unused metadata to reduce binary sizes. Heavy use of unannotated reflection can break applications under AOT environments unless preserved using code-generation or trimming attributes.