Understanding .NET Assemblies
Last updated: October 26, 2023
Introduction to .NET Assemblies
Assemblies are the fundamental building blocks of the .NET Framework. They are the unit of deployment, versioning, security, and activation. Every .NET application, from the smallest utility to the largest enterprise system, is composed of one or more assemblies.
This documentation provides a comprehensive overview of .NET assemblies, their structure, purpose, and how they contribute to the .NET runtime environment.
What are Assemblies?
An assembly is a collection of types and resources that are built to work together and form a logical unit of functionality. Assemblies are deployed as one or more files, typically with the .dll (Dynamic Link Library) or .exe (Executable) file extensions.
Key characteristics of assemblies include:
- Unit of Deployment: Assemblies are the smallest unit that can be deployed.
- Unit of Versioning: Assemblies are versioned independently.
- Unit of Security: Permissions can be granted or denied at the assembly level.
- Unit of Activation: The .NET runtime activates assemblies.
- Self-Describing: Assemblies contain metadata that describes their contents and dependencies.
Assembly Manifest
Each assembly contains an assembly manifest. This manifest is a special section within an assembly that describes the assembly's identity and its contents. It's like a table of contents and a directory for the assembly.
The manifest includes information such as:
- Identity: Name, version, culture, and public key token.
- File Table: A list of all files that make up the assembly.
- Exported Types: A list of all types that are exported by the assembly (visible to other assemblies).
- Referenced Assemblies: A list of all other assemblies that this assembly depends on.
- Security Permissions: Information about the permissions requested by the assembly.
The manifest can be embedded within a module file or in a separate manifest file.
Assembly Metadata
Metadata is information that describes the code and data within an assembly. This metadata is crucial for the Common Language Runtime (CLR) to understand how to load, link, and execute the code.
Key metadata components include:
- Type Definitions: Descriptions of classes, structs, interfaces, enums, and delegates.
- Member Definitions: Information about fields, properties, methods, and events within types.
- Attributes: Declarative tags that provide additional information about code elements.
- Referential Integrity: Information about dependencies on other assemblies.
Metadata is stored in a format called the Common Intermediate Language (CIL) metadata, which is part of the Portable Executable (PE) file format.
Types in Assemblies
Assemblies contain type definitions. A type is a blueprint for creating objects and defines their behavior and data. Examples of types include:
- Classes
- Structs
- Interfaces
- Enumerations (Enums)
- Delegates
Each type is defined once and can be referenced by multiple assemblies. The CLR uses metadata to locate and load type definitions when they are needed.
References
Assemblies often depend on other assemblies to provide functionality. These dependencies are known as references. When an assembly references another assembly, it indicates a compile-time and run-time dependency.
The assembly manifest lists all referenced assemblies. The CLR uses this information to locate and load dependent assemblies when the application is executed.
<assemblyIdentity name="MyApplication" version="1.0.0.0" />
<!-- ... other manifest elements ... -->
<dependency>
<dependentAssembly>
<assemblyIdentity name="System.Core" publicKeyToken="b77a5c561934e089" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
</dependentAssembly>
</dependency>
Versioning
Assemblies are versioned to manage changes and ensure compatibility. Each assembly has a version number composed of four parts: Major, Minor, Build, and Revision (e.g., 1.2.3.4).
Versioning is critical for:
- Allowing multiple versions of the same assembly to coexist (side-by-side execution).
- Controlling which version of a dependent assembly is loaded.
- Ensuring that applications continue to function correctly after updates.
The CLR uses the version information in the assembly manifest and the application configuration to resolve dependencies.
Strong Naming
Strong naming provides a unique identity to an assembly and helps prevent versioning conflicts and malicious assemblies. A strongly named assembly is signed with a cryptographic key pair.
The public key from the key pair is embedded in the assembly's manifest, and the private key is used to sign the assembly during the build process.
Strongly named assemblies provide:
- Unique Identity: Guarantees that an assembly with the same name, version, and culture originates from the same publisher.
- Security: Prevents spoofing by ensuring the assembly hasn't been tampered with.
- Version Control: Facilitates precise control over which versions of assemblies an application uses.
Side-by-Side Execution
Side-by-side (SxS) execution allows multiple versions of the same assembly to exist and be used concurrently by different applications on the same computer. This is a significant advantage over earlier component models.
The CLR's assembly loader is responsible for managing SxS execution, ensuring that each application receives the correct version of a dependent assembly based on its configuration and the assembly's identity.
Resource Files
Assemblies can also contain resource files, such as images, strings, or configuration data. These resources can be embedded directly within the assembly or specified as separate files that the assembly references.
Embedding resources simplifies deployment as all necessary components are contained within a single assembly file.
Examples
Let's consider a simple C# application that references the System.Core assembly.
Example 1: A Simple Console Application
Consider the following C# code:
using System;
using System.Linq;
public class Program
{
public static void Main(string[] args)
{
var numbers = new[] { 1, 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(n => n * n);
foreach (var num in squaredNumbers)
{
Console.WriteLine(num);
}
}
}
This code uses LINQ (Language Integrated Query), which is part of the System.Core.dll assembly. When you compile this code, the compiler will ensure that the System.Core assembly is referenced. At runtime, the CLR will load System.Core.dll to execute the LINQ operations.
Example 2: Referenced Assembly Metadata Snippet
When an assembly references another, the manifest will contain information like this:
<dependentAssembly>
<assemblyIdentity name="System.Core"
publicKeyToken="b77a5c561934e089"
culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0"
newVersion="4.0.0.0" />
</dependentAssembly>
This snippet indicates that the assembly depends on System.Core with a specific public key token and specifies a binding redirect for version compatibility.