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

📚 Interview question

What is a .NET assembly, and what does assembly identity (name, version, culture, public key token) mean in modern .NET?

Assemblies as the unit of deployment and versioning, their contents, identity and how modern .NET treats versions and strong names.

Mid-Level Theory Intermediate assemblies clr strong-naming versioning

🎯 Key points

  • An assembly is the unit of deployment, versioning and loading: IL, metadata, manifest and resources
  • Identity is name, version, culture and public key token
  • Modern .NET has no GAC and accepts the same or a higher assembly version
  • Strong-name signatures are not validated at load and are not a security feature
  • AssemblyVersion, FileVersion, InformationalVersion and package version have different jobs
  • Reference assemblies are for compiling; implementation assemblies are loaded at run time

⏱ 30-second 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.

🗣 Full answer (senior)

How I would answer this in an interview

A .NET assembly is the unit of deployment, versioning, loading and security in .NET — usually a single .dll or .exe file (a PE file) containing IL code, metadata describing its types, a manifest describing the assembly itself, and optionally resources. Assembly identity is the combination of name, version, culture and public key token — important in .NET Framework (GAC, binding redirects), but much less strict in modern .NET, where the host loads what .deps.json says and versions are mostly informational for the loader.

1. What an assembly contains

Ordering.Domain.dll (PE file)
├── PE/COFF headers + CLR header
├── Metadata: type definitions, members, references to other assemblies and types
├── Manifest: assembly name, version, culture, public key, referenced assemblies, files, resources
├── IL code for methods
└── Resources (embedded files, .resources)

One assembly is usually one file; multi-module assemblies exist in theory but aren't used in modern .NET. Satellite assemblies hold culture-specific resources.

2. Assembly identity

typeof(Order).Assembly.GetName().FullName
// "Ordering.Domain, Version=2.3.0.0, Culture=neutral, PublicKeyToken=null"
  • Name: simple name (Ordering.Domain), matching the file name by convention.
  • Version: AssemblyVersion (major.minor.build.revision) — part of identity.
  • Culture: neutral for code; specific cultures for satellite resource assemblies (en-AU).
  • Public key token: an 8-byte hash of the public key if the assembly is strong-named; null otherwise.

3. Versions: three different numbers

Attribute Purpose Affects loading?
AssemblyVersion Identity; recorded in references .NET Framework: yes (exact match unless redirected). Modern .NET: loader accepts equal or higher versions in the default context
FileVersion Win32 file version for support/diagnostics No
InformationalVersion Product version string (often includes git SHA, e.g. 2.3.0+a1b2c3) No

NuGet package version is separate again. Many libraries keep AssemblyVersion stable within a major version (e.g. 2.0.0.0) to avoid binding issues for Framework consumers.

4. Strong naming — then and now

  • .NET Framework: strong names gave unique identity, enabled the GAC, and the runtime enforced exact version matches — hence binding redirects in app.config (<bindingRedirect oldVersion="0.0.0.0-2.3.0.0" newVersion="2.3.0.0"/>).
  • Modern .NET: the runtime doesn't validate strong-name signatures, there's no GAC, and no binding redirects. Strong naming is still used by libraries that need to be consumable by strong-named .NET Framework assemblies. It's not a security feature (Microsoft recommends Authenticode/NuGet package signing for authenticity).

5. Identity in modern .NET loading

  • The host builds a list of assemblies from .deps.json (application + NuGet dependencies + framework). The default AssemblyLoadContext resolves references by name and accepts the available version if it's ≥ the referenced version (otherwise FileLoadException).
  • NuGet picks a single version per package for the whole app ("nearest wins" / lowest applicable), avoiding most diamond conflicts at build time.
  • Multiple versions side by side require separate AssemblyLoadContexts (plugins).
  • Unification of framework assemblies happens via the shared framework version.

6. Why it matters

  • Diagnosing FileLoadException/FileNotFoundException ("Could not load file or assembly 'X, Version=...'").
  • Designing library versioning: semantic versioning of packages, stable AssemblyVersion strategies, InternalsVisibleTo with public keys for strong-named assemblies.
  • Plugin systems and isolation.
  • Reflection (Assembly.Load, Type.GetType("Type, Assembly")).

7. A production example

A plugin host loaded customer plugins that referenced Newtonsoft.Json 12 while the host used 13. In .NET Framework this needed binding redirects; after migrating to .NET 8 the default context happily used the host's v13 (higher version) — until a plugin referenced v14, which wasn't present, causing FileLoadException. We moved plugins into their own AssemblyLoadContexts with AssemblyDependencyResolver so each could load its own dependency versions, sharing only the contract assembly with the host.

🔍 What the interviewer is checking

The interviewer checks you understand what you ship and why loading can fail.

Green flags: IL, metadata, manifest and resources; four-part identity; no GAC; same-or-higher version rule; strong naming not validated; reference versus implementation assemblies.

Red flags: suggests binding redirects or the GAC for modern .NET; confuses NuGet version with assembly version.

📚 Learn in depth

1. The manifest

The manifest (in metadata tables Assembly, AssemblyRef, File, ManifestResource, ExportedType) records:

  • this assembly's identity and flags (e.g. PublicKey, Retargetable);
  • references to other assemblies (AssemblyRef with name, version, culture, public key token);
  • resources and their locations;
  • exported/forwarded types ([TypeForwardedTo]).

2. Strong-name mechanics

Signing computes a hash of the PE file and signs it with a private key; the public key is embedded. The public key token = last 8 bytes of SHA-1 of the public key (reversed). Delay signing and public signing (<PublicSign>true</PublicSign>) allow building with only the public key — common in open-source projects.

3. Modern binding rules

In the default AssemblyLoadContext:

  • If an assembly with the same simple name is already loaded, it's returned if its version is ≥ requested.
  • Otherwise, it's looked up in the TPA list (from deps.json/framework).
  • If not found, AssemblyLoadContext.Default.Resolving events and AppDomain.AssemblyResolve handlers run.
  • Culture-specific satellite assemblies are found by culture subfolders.

4. Reference assemblies

Compilation uses reference assemblies (metadata only, no IL bodies) from targeting packs (Microsoft.NETCore.App.Ref). At run time, implementation assemblies from the shared framework are loaded. Type forwarding keeps identities stable across these layers (e.g. netstandard.dll forwards to System.Runtime).

5. Versioning guidance for libraries

  • Use SemVer for packages.
  • Consider AssemblyVersion = major.0.0.0 for Framework-consumer friendliness.
  • Put rich info in InformationalVersion (SourceLink adds the commit).
  • Avoid breaking public API within a major version (use package validation).

Try it yourself

  1. Print Assembly.GetName() for your app and a NuGet dependency.
  2. Inspect .deps.json in a published app.
  3. Reference two packages that depend on different versions of a third and inspect which version is chosen in project.assets.json.
  4. Strong-name a library and print its public key token.

💻 Code

using System.Reflection;

Assembly app = typeof(Program).Assembly;
AssemblyName id = app.GetName();

Console.WriteLine($"Full name:     {id.FullName}");
Console.WriteLine($"Version:       {id.Version}");
Console.WriteLine($"Culture:       {(string.IsNullOrEmpty(id.CultureName) ? "neutral" : id.CultureName)}");
Console.WriteLine($"Token:         {Convert.ToHexString(id.GetPublicKeyToken() ?? [])}");
Console.WriteLine($"Informational: {app.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion}");

foreach (AssemblyName reference in app.GetReferencedAssemblies())
    Console.WriteLine($"  references {reference.Name} {reference.Version}");

partial class Program;
<PropertyGroup>
  <AssemblyVersion>2.0.0.0</AssemblyVersion>
  <FileVersion>2.3.1.0</FileVersion>
  <Version>2.3.1</Version>
</PropertyGroup>

⚠ Common mistakes

1. Applying .NET Framework binding concepts to modern .NET

What happens: searching for binding redirects or GAC in .NET 8+. Why it is wrong: neither exists; loading is deps.json/ALC based. Do instead: check deps.json, package versions and ALCs.

2. Treating strong naming as security

What happens: relying on strong names to prevent tampering. Why it is wrong: modern runtime doesn't validate them. Do instead: Authenticode/package signing.

3. Confusing the three version attributes

What happens: bumping AssemblyVersion for every build. Why it is wrong: causes unnecessary binding issues for Framework consumers. Do instead: use FileVersion/InformationalVersion for build numbers.

4. Expecting side-by-side versions in one context

What happens: trying to load two versions of the same assembly in the default ALC. Why it is wrong: default context unifies by name. Do instead: separate AssemblyLoadContexts.

5. Hard-coding full assembly names in reflection

What happens: Type.GetType("X, Lib, Version=1.0.0.0, ..."). Why it is wrong: breaks on version changes. Do instead: use typeof or simple names with known assemblies.

6. Ignoring culture/satellite assemblies

What happens: missing localisation at run time. Why it is wrong: satellite assemblies not deployed. Do instead: ensure culture folders are published.

7. Copying DLLs manually into bin folders

What happens: "fixing" missing assemblies by copying files. Why it is wrong: deps.json doesn't list them; inconsistent behaviour. Do instead: fix package references.

8. Breaking identity by moving types

What happens: moving public types between assemblies in a library. Why it is wrong: consumers compiled against old locations fail. Do instead: use [TypeForwardedTo].

🏛 Senior insight: trade-offs

Senior engineers treat assembly identity as an API contract.

Decision Recommended practice
Library AssemblyVersion Major only, for example 3.0.0.0
Build traceability InformationalVersion with commit hash
Strong naming a public library Keep it if .NET Framework consumers exist
Runtime load failures Read deps.json and the requested version in the exception

The skill is reading a FileNotFoundException message and knowing which project in the graph asked for which version.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions metadata and the manifest inside an assembly Why: Checks depth on what the metadata tables contain
    How to 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.

  2. When: Candidate talks about what ships with the app Why: Probes framework-dependent versus self-contained publish
    How to answer

    By default dotnet publish gives a framework-dependent folder that needs the .NET runtime on the server. Self-contained (--self-contained -r linux-x64) bundles the runtime, so nothing needs installing, but the output is bigger and you patch the runtime by redeploying. On top of that: single-file packs everything into one executable, trimming removes unused code, ReadyToRun precompiles to speed up start-up, and Native AOT compiles to a native binary with very fast start-up and low memory but no dynamic code generation. The SDK can also build a container image directly. I choose based on where it runs, start-up and memory needs, and whether the libraries are trim- and AOT-safe, and I measure the result.

  3. When: Candidate talks about libraries shared across teams Why: Checks awareness of CLS compliance for shared APIs
    How to answer

    The CLS is the subset of the CTS that every .NET language is expected to support, so a CLS-compliant public API can be used from C#, F#, VB and others. Typical rules: no unsigned types such as uint in public signatures, no public identifiers that differ only by case, no pointer types in public members. Adding [assembly: CLSCompliant(true)] makes the C# compiler warn (CS3001, CS3002, CS3005 and others) when a public member breaks the rules. Only the public surface matters; internal code can use anything.

🛠 Real-world problems: how you handle them

Plugin failed to load because it was built against a newer shared library
Situation
A reporting host loaded customer plugins; one plugin crashed on load after its author upgraded a shared package.
Signals
`FileNotFoundException: Could not load file or assembly 'Contoso.Core, Version=3.0.0.0'` while the host shipped 2.4.
Investigation
Compared the requested version in the exception with the host's `deps.json` and the plugin's references in ILSpy.
Resolution
Upgraded the host to the newer shared package and published compatibility rules for plugin authors.
Prevention
Added a CI check that plugins reference shared contracts no newer than the host's version.

How to tell it (STAR)

In [our reporting host], a plugin built against [Contoso.Core 3.0] failed on a host shipping [2.4]. I explained that modern .NET accepts same or higher versions only, so a binding redirect would not help. We upgraded the host and added [a CI rule] that blocks plugins compiled against newer contracts.

↩ Usually asked after

More in CLR Fundamentals

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me