📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are metadata and the manifest in a .NET assembly, and how can you read them without loading the assembly?

Metadata tables and tokens, the assembly manifest, type forwarding, and reading them with System.Reflection.Metadata.

Senior Theory Advanced clr manifest metadata reflection-metadata

🎯 Key points

  • Metadata is ECMA-335 tables describing every defined and referenced type and member
  • IL refers to metadata with 4-byte tokens whose high byte names the table
  • The manifest is the assembly-level part: identity, references, resources, exported types
  • Type forwarding via ExportedType lets types move assemblies without breaking binaries
  • PEReader and MetadataReader read metadata without loading or running code
  • MetadataLoadContext gives a reflection-like API without execution

⏱ 30-second answer

Metadata is a set of tables inside the assembly that describe every type, method, field, property, event, attribute and every external type or member referenced. IL refers to them with 4-byte tokens (the high byte is the table, for example 0x06 for MethodDef). The manifest is the part of the metadata that describes the assembly itself: its identity, the assemblies it references, resources and exported (forwarded) types. You can read both without loading anything into the runtime using System.Reflection.Metadata's PEReader and MetadataReader.

🗣 Full answer (senior)

How I would answer this in an interview

Metadata is the structured, language-neutral description of everything an assembly defines and references: types, members, signatures, attributes, generic parameters, and references to other assemblies and types. The manifest is the part of the metadata describing the assembly itself: its identity, referenced assemblies, resources and exported types. Both live in tables inside the PE file, and you can read them without loading the assembly for execution using System.Reflection.Metadata or MetadataLoadContext.

1. What metadata contains

Metadata is a set of tables plus heaps:

Table (examples) Describes
TypeDef Types defined here (name, namespace, flags, base type)
MethodDef, Field, Property, Event, Param Members and parameters
TypeRef, MemberRef Types/members used from other assemblies
CustomAttribute Attributes on any element
GenericParam, GenericParamConstraint Generics
InterfaceImpl, NestedClass Relationships
Assembly, AssemblyRef, ManifestResource, ExportedType Manifest

Heaps store strings (#Strings), user strings (#US for string literals), GUIDs and blobs (signatures, attribute values). Tokens (e.g. 0x06000012 = MethodDef row 0x12) link IL instructions to metadata.

Metadata is what makes .NET self-describing: compilers reference types from it, the CLR builds runtime types from it, reflection reads it, and tools (IntelliSense, decompilers, analysers) rely on it.

2. The manifest

  • Identity: name, version, culture, public key.
  • References: each AssemblyRef (name, version, culture, public key token) this assembly depends on.
  • Resources: embedded resources and their names.
  • Exported/forwarded types: [TypeForwardedTo] redirects (used in reference assemblies and refactorings).
  • Assembly-level attributes: [assembly: InternalsVisibleTo], TargetFramework, version attributes, etc. (stored as custom attributes on the assembly).

3. Reading metadata without loading the assembly

Loading an assembly via reflection (Assembly.LoadFrom) executes nothing by itself but binds it into a load context, may lock the file, resolves dependencies, can't be unloaded easily, and may fail for assemblies targeting other frameworks. For tooling, inspect metadata directly:

System.Reflection.Metadata (low-level, fast, no loading):

using var stream = File.OpenRead(path);
using var pe = new PEReader(stream);
var md = pe.GetMetadataReader();

var def = md.GetAssemblyDefinition();
Console.WriteLine($"{md.GetString(def.Name)} {def.Version}");

foreach (var refHandle in md.AssemblyReferences)
{
    var r = md.GetAssemblyReference(refHandle);
    Console.WriteLine($"  -> {md.GetString(r.Name)} {r.Version}");
}

foreach (var typeHandle in md.TypeDefinitions)
{
    var t = md.GetTypeDefinition(typeHandle);
    Console.WriteLine($"{md.GetString(t.Namespace)}.{md.GetString(t.Name)}");
}

MetadataLoadContext (System.Reflection.MetadataLoadContext package) — reflection-like API (Type, MethodInfo, GetCustomAttributesData) over assemblies loaded for inspection only, with a resolver you control:

var resolver = new PathAssemblyResolver(Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll").Append(path));
using var mlc = new MetadataLoadContext(resolver);
var asm = mlc.LoadFromAssemblyPath(path);
foreach (var type in asm.GetTypes().Where(t => t.GetCustomAttributesData().Any(a => a.AttributeType.Name == "PluginAttribute")))
    Console.WriteLine(type.FullName);

You can't execute code or create instances from MetadataLoadContext types, which is exactly what makes it safe for scanning.

Other tools: AssemblyName.GetAssemblyName(path) (identity only), ILSpy/dnSpy/monodis, ildasm, Roslyn reading references, dotnet-symbol.

4. Use cases

  • Plugin discovery: find types implementing an interface or marked with an attribute without loading untrusted plugins into the host.
  • Build tools/CI checks: list dependencies and versions, detect forbidden references, verify public API.
  • License/SBOM tools: inventory assemblies and versions.
  • Analysers and generators: Roslyn reads referenced assemblies' metadata.
  • Diagnosing load errors: check the exact AssemblyRef versions an assembly expects.

5. A production example

A plugin platform scanned a folder at start-up with Assembly.LoadFrom to find plugin types. A malformed or framework-mismatched DLL crashed start-up, and files stayed locked so plugins couldn't be updated without restarting. We replaced scanning with MetadataLoadContext to read attributes and referenced versions (rejecting incompatible plugins with clear errors), and only then loaded valid plugins into collectible AssemblyLoadContexts. Start-up became robust and plugin updates no longer needed host restarts.

🔍 What the interviewer is checking

The interviewer checks depth on how .NET is self-describing and whether you can build safe tooling.

Green flags: names tables and tokens, explains manifest as assembly-level metadata, type forwarding, System.Reflection.Metadata or MetadataLoadContext for inspection.

Red flags: "metadata is just attributes"; loads untrusted DLLs with Assembly.LoadFrom to inspect them.

📚 Learn in depth

1. PE layout

A .NET PE file has a CLI header (IMAGE_COR20_HEADER) pointing to the metadata root, which contains stream headers for #~ (compressed tables), #Strings, #US, #GUID and #Blob. Method bodies (IL) are in the PE's code section, referenced by RVA from MethodDef rows.

2. Tokens

A metadata token is 4 bytes: high byte = table (0x02 TypeDef, 0x06 MethodDef, 0x0A MemberRef, 0x70 user string...), low 3 bytes = row index. IL instructions like call 0x0A00001F reference members by token; the JIT resolves tokens via metadata when compiling.

3. Signatures

Method and field signatures are encoded in the #Blob heap using compact element-type codes (ELEMENT_TYPE_I4, CLASS, GENERICINST, VAR, MVAR...). System.Reflection.Metadata provides SignatureDecoder to decode them into your own representation.

4. Portable PDBs

Debug information (sequence points, local names) is stored in Portable PDB format — itself using metadata tables — either as a separate .pdb or embedded. SourceLink metadata maps files to source control URLs.

5. Performance

System.Reflection.Metadata is designed for speed and low allocation (handles are structs, strings read on demand). Tools like Roslyn and the .NET SDK use it heavily. MetadataLoadContext is built on it, adding a reflection-compatible surface.

Try it yourself

  1. List types and references of an assembly with System.Reflection.Metadata.
  2. Use MetadataLoadContext to find types with a specific attribute.
  3. Open the same assembly in ILSpy and view metadata tables.
  4. Compare memory/time vs Assembly.LoadFrom for scanning 100 assemblies.

💻 Code

using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

string path = args.Length > 0 ? args[0] : typeof(Program).Assembly.Location;

using FileStream stream = File.OpenRead(path);
using var pe = new PEReader(stream);
MetadataReader md = pe.GetMetadataReader();

// Manifest: identity and references
AssemblyDefinition self = md.GetAssemblyDefinition();
Console.WriteLine($"{md.GetString(self.Name)} {self.Version}");

foreach (AssemblyReferenceHandle h in md.AssemblyReferences)
{
    AssemblyReference r = md.GetAssemblyReference(h);
    Console.WriteLine($"  -> {md.GetString(r.Name)} {r.Version}");
}

// Metadata: types and their method tokens
foreach (TypeDefinitionHandle th in md.TypeDefinitions)
{
    TypeDefinition t = md.GetTypeDefinition(th);
    Console.WriteLine($"{md.GetString(t.Namespace)}.{md.GetString(t.Name)}");
    foreach (MethodDefinitionHandle mh in t.GetMethods())
    {
        int token = MetadataTokens.GetToken(mh);
        Console.WriteLine($"    0x{token:X8} {md.GetString(md.GetMethodDefinition(mh).Name)}");
    }
}

partial class Program;

Nothing in the inspected assembly is executed or loaded into the runtime.

⚠ Common mistakes

1. Loading assemblies just to inspect them

What happens: Assembly.LoadFrom for scanning. Why it is wrong: locks files, triggers dependency resolution, can't unload, may fail. Do instead: System.Reflection.Metadata or MetadataLoadContext.

2. Confusing metadata and manifest

What happens: using the terms interchangeably. Why it is wrong: manifest is the assembly-level subset. Do instead: explain both.

3. Trying to execute types from MetadataLoadContext

What happens: Activator.CreateInstance on inspection-only types. Why it is wrong: not supported. Do instead: load into an execution context after validation.

4. Missing core assemblies in the resolver

What happens: MetadataLoadContext fails to resolve System.Runtime. Why it is wrong: resolver needs the target framework's assemblies. Do instead: include runtime/reference assemblies paths.

5. Instantiating attributes during scans

What happens: using GetCustomAttributes (which constructs attributes). Why it is wrong: executes code and requires loadable types. Do instead: GetCustomAttributesData.

6. Ignoring type forwarding

What happens: tools can't find types in expected assemblies. Why it is wrong: types may be forwarded. Do instead: follow ExportedType entries.

7. Treating metadata as a security boundary

What happens: trusting plugin metadata blindly. Why it is wrong: malicious assemblies can declare anything. Do instead: validate signatures and sandbox behaviour.

8. Decoding signatures manually

What happens: hand-parsing blobs. Why it is wrong: error-prone. Do instead: use SignatureDecoder and provided readers.

🏛 Senior insight: trade-offs

Metadata literacy pays off when debugging tooling, trimming and versioning problems.

Problem Metadata angle
TypeLoadException after upgrade A TypeRef points at an assembly that no longer defines or forwards the type
Trimming removed a type The trimmer only keeps what metadata references or annotations prove is used
Moving a type between packages Use TypeForwardedTo to keep old binaries working
Scanning third-party DLLs Read metadata without executing code

Knowing that IL is just tokens into tables also explains why obfuscators and IL weavers work.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says IL refers to metadata tokens Why: Checks they can read the IL that uses those tokens
    How to answer

    IL (Common Intermediate Language, CIL) is the CPU-independent, stack-based instruction set that C# compiles to. Instructions push and pop values on an evaluation stack: ldarg.0, ldarg.1, add, ret. Calls use metadata tokens (call, callvirt, newobj), and conversions such as box are explicit. You read it with ILSpy, SharpLab or ildasm-style tools. Reading IL helps you spot hidden allocations (boxing, closures), see how async, foreach or using are lowered, and settle "what does the compiler really do" arguments.

  2. When: Candidate mentions reflection reads metadata Why: Probes reflection cost and alternatives
    How to answer

    Reflection reads the metadata the compiler wrote into an assembly at run time: typeof(T) or obj.GetType() gives a Type, and from it you get PropertyInfo, MethodInfo, FieldInfo and so on, filtered by BindingFlags. It is flexible but slow on hot paths: lookups search metadata, Invoke takes an object[], boxes value types, checks arguments and wraps exceptions in TargetInvocationException. Make it fast by looking up once and caching, turning a MethodInfo into a typed delegate, using MethodInvoker (.NET 8) when you only have a MethodInfo, or [UnsafeAccessor] (.NET 8) to reach a private member with no reflection at all. Reflection also hides dependencies from the trimmer, so in trimmed or Native AOT apps you must annotate with [DynamicallyAccessedMembers] or move the work to a source generator.

  3. When: Candidate mentions the manifest's assembly references Why: Checks understanding of assembly identity and version binding
    How to answer

    An assembly is the unit of deployment, versioning and loading in .NET: a PE file (.dll or .exe) containing IL, metadata, a manifest and optional resources. Its identity is name, version, culture and public key token. In modern .NET there is no GAC, strong-name signatures are not validated at load, and the runtime accepts an assembly whose version is the same or higher than the one referenced, so binding redirects are not needed. Assembly versions still matter for compatibility and for what NuGet and the SDK place in deps.json.

🛠 Real-world problems: how you handle them

Plugin scanner locked files and ran plugin code during discovery
Situation
A host scanned a plugins folder at startup to list available plugins.
Signals
Plugin DLLs could not be replaced while the host ran, and a faulty plugin's module initialiser crashed the scan.
Investigation
Found the scanner used `Assembly.LoadFrom` and reflection over every DLL in the folder.
Resolution
Rewrote discovery with `PEReader` and `MetadataReader` to find types implementing the plugin interface by name, and loaded only chosen plugins into their own load context.
Prevention
Added a rule that discovery code must never load assemblies into the runtime.

How to tell it (STAR)

In [our plugin host], discovery loaded every DLL and a broken plugin could crash startup. I switched discovery to System.Reflection.Metadata so we read type names without running code. Startup scan time dropped from [X] to [Y] and file locks disappeared.

↩ Usually asked after

More in CLR Fundamentals

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me