All applications written with Visual C# are written using the .NET Framework. The .NET Framework provides the class libraries that simplify programming in Visual C#, and it also provides the runtime support you need to execute and manage your code. In this section, we’ll look at the .NET Framework and assemblies, which are the unit of reuse, versioning, and deployment in the .NET platform.
A framework is commonly thought of as a set of class libraries that aid in the development of applications. The .NET Framework is more than just a set of classes. The .NET Framework is targeted by compilers using a wide variety of programming languages (over twenty at the time of this writing). These languages are used to create a wide range of applications, including everything from small components that run on handheld devices to large Microsoft ASP.NET applications that span Web farms, where multiple Web servers act together to improve the performance and fault tolerance of a Web site. The .NET Framework is responsible for providing a basic platform that these applications can share. This basic platform includes a runtime set of services that oversee the execution of applications. A key responsibility of the runtime is to manage execution so that software written by different programming languages uses classes and other types safely.
The common language runtime is responsible for managing and executing code written for the .NET Framework. Code that’s compiled with the Visual C# .NET compiler always runs with the help of the runtime and is known as managed code. The runtime is responsible for overseeing all aspects of code execution, including the following tasks:
Determining how and when code should be loaded and managing the layout of objects in memory. As your code executes, the runtime determines which classes and methods are required for execution and compiles them as needed. This process is discussed later in this chapter, in the section “Compiling Assemblies for Execution.”
Handling the memory needs of managed code. The garbage collection mechanism used by the .NET Framework is discussed in more detail in Chapter 3.
Ensuring type safety for the code the runtime executes. The runtime is responsible for guaranteeing that classes and other types are used in a safe manner.
Handling and propagating errors in managed code using a common error-handling framework based on exceptions.
Maintaining security for the runtime and your applications. Security in the .NET Framework takes two forms: code access security, which is used to ensure that code is executed in a safe context, and role-based security, which controls access to system resources.
The runtime ensures type safety for applications and components. The runtime notion of type safety extends beyond the traditional notion, which simply guarantees that types interact with each other in a predictably safe way. For example, a traditional view of type safety is that a variable that refers to a string shouldn’t occasionally hold an integer value. This sort of type safety is enforced by the Visual C# .NET compiler.
The runtime enforces a broader view of type safety that guarantees that types and variables are never used in a way that’s dangerous to the runtime or to neighboring applications or components. Types such as classes or structures can be accessed only in specific ways. For example, access to memory locations is allowed only at offsets that correspond to actual fields in a class or a structure. This broader view of type safety helps ensure that multiple applications and components can be executed without interfering with each other.
Type safety is an important part of the .NET security architecture, and the runtime uses type safety to keep applications working robustly. The runtime can offer greater assurances that code will execute properly when code can be verified as type-safe, a process that occurs when your code is compiled into its final machine code format. Many languages that target the .NET platform, including Visual C#, generate code that can be verified to ensure type safety. Code that can be verified as type-safe is allowed to run with a lower level of trust than code that isn’t verifiable.
Applications built for Windows typically have dependencies on one or more dynamic-link libraries (DLLs). Often these DLLs are components that are shared with other applications, and sometimes these DLLs contain Component Object Model (COM) classes that are registered in the system registry. Unfortunately, when these components are updated or improperly installed, existing applications can be broken—a situation commonly known as “DLL hell”. The .NET Framework seeks to avoid DLL hell through the use of assemblies—self-describing modules that replace the notion of DLLs and executable files (EXEs).
The Visual C# .NET compiler doesn’t generate machine code that can be directly executed on your computer. Instead, your project’s source code is compiled into an assembly, as shown in Figure 1-1.
As you can see in Figure 1-1, an assembly has two parts: intermediate language (IL) and metadata.
Intermediate language (IL) Contains the executable portion of the program. IL is similar to the output from the first pass of a compiler. It can’t be executed directly on your computer because it hasn’t been translated into the binary format that your computer’s processor recognizes. Instead, it must undergo a final compilation pass by a compiler that’s part of the .NET Framework, as described later in this chapter, in the section “Compiling Assemblies for Execution.”
Metadata Describes the assembly contents. Embedding metadata into each assembly enables any assembly to be completely self-describing, simplifying many of the tasks that need to be performed when you’re distributing components with older technologies. The .NET Framework uses metadata to eliminate the need for component registration. Each assembly includes information about references to other assemblies in metadata, enabling the runtime to bind assemblies together using a flexible approach that’s discussed in the next section. Tools such as Visual C# .NET use metadata to simplify development; tools can simply inspect the assembly’s metadata and determine the types and operations that are exported by the assembly.
When the common language runtime loads your application, it examines your program’s metadata to determine which external assemblies are required for execution. There are two types of assemblies.
Private assemblies Used by a single application; typically located in the same directory as the application that uses them. This is the preferred method of using assemblies. Because a private assembly isn’t shared with other applications, private assemblies can be easily updated or replaced, with no impact on any other applications.
Shared assemblies Intended for use by multiple applications. A shared assembly has restrictions placed on it by the runtime and must adhere to naming and versioning rules. More information about shared assemblies is provided later in this chapter, in the section “Using the Global Assembly Cache.”
An application that depends on private assemblies is easily installed or moved. Just copy or move the application from its current location to a new location, and it will work perfectly. Applications that target the .NET platform require no registration, and an application that uses only .NET Framework assemblies and private assemblies can be run as soon as it’s copied into its directory. There are a couple of caveats, however.
If you’re copying a .NET application to a new computer, that machine must have at least the redistributable version of the .NET runtime installed.
If your application requires access to the user’s desktop or Start menu, you’ll need to make some entries in the system registry, just as with previous Windows development tools.
When the code in an assembly must be executed, the runtime compiles the assembly into machine code. However, the entire assembly isn’t compiled in one step. Instead, each method in the assembly is compiled as it’s needed, in a process known as just-in-time compilation, or jitting. Because each method is compiled as it’s needed, this compilation process is much faster than simply interpreting the IL code.
As an option, you can elect to compile your assembly into processor-specific code that can be directly executed by the processor. This work isn’t performed by the Visual C# .NET compiler; the tool used for this type of compilation is the Native Image Generator, or ngen.exe, which is included as part of the .NET Framework. The following command creates a native image from an assembly named hello.exe:
The compiled native image is automatically installed into the native image assembly cache. Creating a native image is known as prejitting your assembly. Even if you prejit your assembly, you must deploy your nonjitted assembly; this deployment is necessary because the nonjitted assembly contains the assembly metadata and might be required if recompilation is needed due to an update in dependent assemblies.
An assembly in its simplest form looks just like any EXE or DLL module that you might have developed using earlier development tools. It’s possible for an assembly to consist of multiple modules, although the assembly is always the versioning unit for .NET programs. You can’t upgrade a single module in a multi-module assembly; the entire assembly must be deployed or versioned as a single unit. Visual C# .NET supports building single-module assemblies using Visual Studio .NET. To build a multi-module assembly, you can use the Assembly Linker tool, al.exe, included as part of the .NET Framework.
Assemblies that are employed by multiple projects can be placed in the global assembly cache, which serves as both a storage location and a registry for shared .NET components. The global assembly cache offers much more flexibility than the registration used by COM, enabling multiple versions and cultures (previously known as locales) of an assembly to be stored.
Although a private assembly has few restrictions on its naming, versioning, and deployment, the global assembly cache places stringent requirements on assemblies to properly identify each assembly image it stores. An assembly that meets these requirements is known as a strong-named assembly. The identity of a strong-named assembly consists of the following four components:
The name of the assembly
The assembly’s version number
A public/private key pair that uniquely identifies the assembly’s creator
An optional cultural designation for localization purposes
Assemblies are installed in the global assembly cache using Microsoft Windows Installer 2 or later. On a development machine, the .NET Framework includes the Global Assembly Cache Utility tool, gacutil.exe, which is used to add, remove, and view assembly components. Only assemblies with a strong name can be installed into the global assembly cache. Creating assemblies with strong names and installing them in the global assembly cache is discussed in Chapter 6.