Assemblies as the unit of deployment and versioning, their contents, identity and how modern .NET treats versions and strong names.
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.
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.
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.
typeof(Order).Assembly.GetName().FullName
// "Ordering.Domain, Version=2.3.0.0, Culture=neutral, PublicKeyToken=null"
Ordering.Domain), matching the file name by convention.AssemblyVersion (major.minor.build.revision) — part of identity.neutral for code; specific cultures for satellite resource assemblies (en-AU).null otherwise.| 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.
app.config (<bindingRedirect oldVersion="0.0.0.0-2.3.0.0" newVersion="2.3.0.0"/>)..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).AssemblyLoadContexts (plugins).FileLoadException/FileNotFoundException ("Could not load file or assembly 'X, Version=...'").AssemblyVersion strategies, InternalsVisibleTo with public keys for strong-named assemblies.Assembly.Load, Type.GetType("Type, Assembly")).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.
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.
The manifest (in metadata tables Assembly, AssemblyRef, File, ManifestResource, ExportedType) records:
PublicKey, Retargetable);AssemblyRef with name, version, culture, public key token);[TypeForwardedTo]).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.
In the default AssemblyLoadContext:
AssemblyLoadContext.Default.Resolving events and AppDomain.AssemblyResolve handlers run.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).
AssemblyVersion = major.0.0.0 for Framework-consumer friendliness.InformationalVersion (SourceLink adds the commit).Assembly.GetName() for your app and a NuGet dependency..deps.json in a published app.project.assets.json.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>
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.
What happens: relying on strong names to prevent tampering. Why it is wrong: modern runtime doesn't validate them. Do instead: Authenticode/package signing.
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.
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.
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.
What happens: missing localisation at run time. Why it is wrong: satellite assemblies not deployed. Do instead: ensure culture folders are published.
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.
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 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.
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.
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.
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.
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.
Last updated 30 Oct 2026.