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 AssemblyVersionAttribute or TargetFrameworkAttribute).
  • 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:

  1. Compile-time token: Type t = typeof(MyClass);
  2. Runtime object instance: Type t = instance.GetType();
  3. 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:

  1. Performance Overhead: Navigating metadata tables and performing runtime type checks bypasses standard compiler optimizations and involves significant overhead compared to direct method calls.
  2. Type Safety: Member lookups rely on strings, which removes compile-time type verification and makes refactoring more prone to runtime failures.
  3. 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.