Click to Rate and Give Feedback
Related Articles

Cobra, a descendant of Python, offers a combined dynamic and statically-typed programming model, built-in unit test facilities, scripting capabilities, and much more. Feel the power here.

Ted Neward

MSDN Magazine June 2009

...

Read more!

We show you how .NET Services within the Azure Services Platform makes it easy to bring workflow apps to the cloud.

Aaron Skonnard

MSDN Magazine April 2009

...

Read more!

Memory usage can have a direct impact on how fast an application executes and thus is important to optimize. In this article we discuss the basics of memory optimization for .NET programs.

Subramanian Ramaswamy and Vance Morrison

MSDN Magazine June 2009

...

Read more!

Here we examine data persistence patterns to help you determine which best suits your needs. We look at a number of patterns, including the Active Record, the Data Mapper, the Repository, the Identity Map, the Lazy Loading, and the Virtual Proxy.

Jeremy Miller

MSDN Magazine April 2009

...

Read more!

.NET RIA Services provides a set of server components and ASP.NET extensions such as authentication, roles, and profile management. We’ll show you how they work.

Jonathan Carter

MSDN Magazine May 2009

...

Read more!

Also by this Author

Jeffrey Richter

MSDN Magazine June 2001

...

Read more!

Jeffrey Richter introduces his AsyncEnumerator class and explains how it harnesses some recent additions to the C# programming language that make working with the asynchronous programming model significantly easier.

Jeffrey Richter

MSDN Magazine November 2007

...

Read more!

Jeffrey Richter

MSDN Magazine November 2006

...

Read more!

Jeffrey Richter

MSDN Magazine April 2002

...

Read more!

In my last column, I showed the various thread synchronization mechanisms employed by the Microsoft® . NET Framework (see Concurrent Affairs: Performance-Conscious Thread Synchronization). I then examined the performance characteristics of all these mechanisms and determined that the Interlocked methods performed the best because the calling thread never has to transition to kernel mode.

Jeffrey Richter

MSDN Magazine March 2006

...

Read more!

Popular Articles

Jeff Prosise explains when it's better to use UpdatePanel and when it's better to use asynchronous calls to WebMethods or page methods instead.

Jeff Prosise

MSDN Magazine June 2007

...

Read more!

James Avery does it again with his popular list of developer tools. This time he covers the best Visual Studio add-ins available today that you can download for free.

James Avery

MSDN Magazine December 2005

...

Read more!

Kenny Kerr sings the praises of the new Visual C++ 2008 Feature Pack, which brings modern conveniences to Visual C++.

Kenny Kerr

MSDN Magazine May 2008

...

Read more!

Now you can perform efficient, sophisticated text analysis using regular expressions in SQL Server 2005.

David Banister

MSDN Magazine February 2007

...

Read more!

A Sidebar gadget is a powerful little too that's surprisingly easy to create. Get in on the fun with Donavon West.

Donavon West

MSDN Magazine August 2007

...

Read more!

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MSDN Magazine

Part 2: Microsoft .NET Framework Delivers the Platform for an Integrated, Service-Oriented Web

Jeffrey Richter

This article assumes you�re familiar with COM and C++
Level of Difficulty    1   2   3 
SUMMARY This article completes the .NET Framework overview begun in the September issue. The common language specification and ILDasm, the MSIL disassembler, are discussed¯as well as how metadata, manifests, and assemblies simplify deployment and versioning.
      Security, which is integral to the design of .NET, is examined extensively, followed by a walk through the development of a single assembly containing multiple files. Finally, the way managed and unmanaged code work together, and the benefits of using each, including interoperablity with unmanaged code, are described.
L
ast month, I introduced the goals of the Microsoft® .NET Framework. In particular, I discussed language interoperability, assemblies, the base framework services, how code is compiled into Microsoft intermediate language (MSIL), and how MSIL is then processed by the common language runtime engine and compiled into native code, which can then execute on the target system. I also discussed how the common type system (CTS) governs the behavior of these types. This month I'll conclude the .NET development platform overview. I'll drill down into the topics mentioned last month, such as metadata, assemblies, and security. Then I'll show you how to deploy an application and discuss the benefits of how you can use both managed and unmanaged code. But first, let's examine how you can now integrate objects created in different languages.

The Common Language Specification

      COM allows objects created in different languages to communicate with one another. In contrast, the .NET common language runtime integrates all languages and allows objects created in one language to be treated as equal citizens by code written in a completely different language. The common language runtime makes this possible due to its standard set of types, self-describing type information (metadata), and common execution environment.
      While language integration is a fantastic goal, the truth of the matter is that programming languages are very different from one another. For example, not all languages let you treat symbols with case-sensitivity, provide unsigned integers, offer operator overloading, unions, or even have methods that support a variable number of parameters, to name a few differences.
      If you intend to create .NET types that are easily accessible from other programming languages, it is important to use features of your programming language that are guaranteed to be available in all other languages. To help you with this, Microsoft has defined a Common Language Specification (CLS) that informs compiler vendors of the minimum set of features that their compilers must support to target the common language runtime. Several vendors are already planning .NET-compatible versions of their compilers.
      Note that the CTS supports many more features than the subset defined by the common language specification, so if you don't care about interlanguage operability you can develop very rich class types that are limited only by the language's feature set.

Metadata

      Last month I mentioned that the compiler's job is to process your source code and produce the corresponding MSIL. In addition, the compiler is responsible for embedding metadata into every .NET-compliant EXE and DLL. In brief, .NET metadata is simply a collection of information persisted in binary form in a Portable Executable (PE) file that is a specification of the types declared and methods implemented in the file or assembly. Metadata is a superset of older technologies such as type libraries and IDL files. The important thing to note is that metadata is far more complete than its predecessors and is always associated with the file that contains the code. In fact, the metadata is always embedded in the same EXE and DLL as the code, making it impossible to separate the two.
      All .NET-compliant compilers are required to emit full metadata information about every class type in the compiled source code module. This metadata contains a declaration for every type and a declaration, including names and types, for all of its members (methods, fields, properties, and events). For every implemented method, the metadata contains information that the loader uses to locate the method body.
      Now I'll run through a few examples demonstrating the power of metadata. Say you're using Visual Studio® to edit your source code. In your code, you want to call a method on some class object. Because it can parse a type's metadata, Visual Studio can easily show you all the members of the type. In addition, if you call a method Visual Studio can show you the exact types required for the method's parameters and verify that your code uses the method's return type properly. It is also possible for the creator of a class type to associate help text and comments with a method or parameter in the metadata. As you can see, metadata helps you develop code faster and more accurately.
      Tools like Visual Studio .NET retrieve a file's metadata information using a technology called reflection. There is a set of classes for enumerating the types in a file as well as the type's members. See the System.Reflection namespace for more information.
      One of my favorite .NET tools is the MSIL disassembler, ILDasm.exe. Figure 1 shows what happens when you run ILDasm on a portion of the base framework contained in MSCorLib.dll. In the window, ILDasm parses the file's metadata and displays the namespaces, types, interfaces, and members in a tree-like hierarchy. Double-clicking on any method causes another window to pop up; this shows the actual MSIL code that is generated by the compiler. Figure 2 illustrates the MSIL code for the System.Object's GetType method.
      I am constantly using this tool in order to fully understand how .NET works. Note that the compiler emits types, fields, methods, properties, and event names into the metadata. However, the compiler only emits names given to values, local variables, and parameters into a module's program database (PDB) file for use when debugging. Since this debug information is not emitted into a module's metadata, it is much harder for someone to reverse-engineer MSIL code into source code.
      Finally, metadata is the technology that enables remote method calls in the .NET Framework. To make a remote method call, the common language runtime must allocate a block of memory and lay the method's parameter data into it using a process called serialization. The common language runtime uses the metadata to determine how large a memory block to allocate and how to marshal the parameter data into the block. The block of data is then sent over a stream to the remote machine, where it is deserialized. Again, the metadata provides the template for the memory block. Then the method is called and the method's return value is remoted back to the originating machine.

Assemblies and Application Deployment

      Over the years, the packaging and deployment of Windows®-based applications has gotten quite complex. Applications typically require several types of files (EXEs, DLLs, and data files) to run, contributing to the familiar problem in which a new installation causes an existing application to break because of some incompatibility between shared files. In addition, many applications¯especially COM apps¯require several registry settings to help the system locate these files. These application deployment and administration hassles are one of the pet peeves of customers that Microsoft is addressing very seriously.
      Key to application deployment and versioning in .NET is the introduction of assemblies. As I described in Part 1 last month, assemblies are collections of behaviors designed to work together, built into single or multiple files. Depending on the application model you use (for example, Web server application or desktop application), the specifics of deployment may vary somewhat. But in general, deploying an application doesn't necessarily involve any more than copying a collection of one or more assemblies (one containing your application entry point) to a specific directory that becomes your application directory.
      There are three reasons you don't need to register components. First, every assembly is self-describing through metadata. Second, every reference to a type is scoped by an assembly reference (unlike COM, where all references to types are effectively machine-wide). Third, .NET uses a set of well-defined heuristics to locate referenced assemblies. Let's take a more detailed look at assembly identity and references and how they contribute to versioning.
      An assembly's identity attributes consist of:
A textual simple name For persisted assemblies, the resolution heuristics used by the common language runtime expect that this name is the same as the name of the file in which the assembly manifest is deployed (excluding file extension). It is carried in metadata so that caching mechanisms may rename the file and still carry the original assembly identity.
A compatibility version number Each assembly contains a version number in the form of incompat.compat.hotfix. A change in the incompat portion of the version number means that the new version is not compatible with prior versions. A change in the compat portion of the version number means that this is intended to be compatible with prior versions that share the same incompat version number. A change in the hotfix portion of the version number means that, yes, you've made a change, but it should be considered essentially the same bits as the prior version. The .NET common language runtime then uses this information in making binding decisions.
A cultural locale Every assembly is either the default locale or is localized for a single locale. A development tool may be able to generate reasonable defaults for all of these elements, but most expose a way for the author to supply this information explicitly.
      The simple name (like My401kApp) is not guaranteed to be unique¯there is nothing in the tools or in the common language runtime to prevent name collisions. There is also nothing to prevent a simple-named assembly from being modified in unintended or uncontrolled ways. This approach works well for private assemblies (intended only for use by the application they are deployed with), but for published assemblies (intended to be shared by multiple applications) a stronger notion of identity is required.
      The common language runtime addresses the problem of name uniqueness and code integrity using standard public-key cryptographic techniques. The approach is to use the public-key value as a unique prefix to the simple, developer-generated name. To ensure that only the holder of the public-private key pair can publish something with this name, the private key is used to generate a digital signature for the file. This signature can later be verified using the public-key value and has the desirable side effect of sealing the executable file. Any modifications to the executable file after signing will prevent the signature from being verified. An assembly that is digitally signed in this manner is said to have a shared name that is suitable for publishing.
      Shared names satisfy the following requirements:
  • Guarantee name uniqueness by relying on unique key-pairs.
  • Prevent others from taking over your namespace. Because you are the sole owner of your private key, no one can generate the same identity that you can. The 401k assembly that is generated with my private key has a different identity than the 401k assembly that is generated with your private key. This is particularly important when releasing subsequent versions of your assembly.
  • Provide a secure notion of identity. If the common language runtime verification checks pass, you can be guaranteed that the assembly comes from the person you thought it did (assuming his private key wasn't compromised, of course). Note, however, that shared names in and of themselves only tell you that the publisher had control of the private-key, not the identity of the publisher. Microsoft Authenticode® technology provides a compatible way to establish the publisher's name if you want to use this to make trust decisions.
      Shared (or published) names work by giving the assembly developer a public-private key pair. The private key is closely guarded, while the public key is published in the manifest. .NET provides a set of APIs that compilers and "post-link" tools can use to generate shared names and manage the key pairs associated with the names.
      When the assembly's manifest is built, it includes references to all the files that make up the assembly. Each file listed in the manifest is hashed, and this hash value is stored along with the file's name. Once the file containing the manifest is built, that file's entire contents are hashed. This hash value is signed with the publisher's private key. The resulting RSA digital signature is stored in a reserved section of the file (that was not included in the hash). The file is now ready to be distributed.
      When the file is installed on a user's machine, the system verifies the shared-name digital signature using the public key. If the signature fails to verify, then the file's contents have been tampered with and cannot be trusted. Note that the system only detects this if the file containing the manifest has been altered at install time. The detection of another file being altered occurs when the assembly is accessed at runtime.
      At runtime, when a reference to an assembly is resolved, the file containing the manifest is located. At this time, the public key of the referenced assembly is compared with the public key that the referencing assembly has on record. If these keys don't match, then a TypeLoadException is raised. This also ensures that the referencing assembly was actually authored by the holder of the private key.
      Finally, when other files from this assembly are loaded, the system hashes the file and compares the resulting hash value with the hash value on record for the file (saved in the referenced assembly's manifest). If the values do not match, the file's contents have been tampered with and cannot be trusted. The one drawback to this approach is that the assembly file is hashed at runtime, causing a runtime performance hit.
      Earlier, I mentioned that every reference to a type is scoped by an assembly reference and that this information is carried in the metadata with each file. Another way to look at this is that every file carries information (through these references) about the components (assemblies) and their versions that it was built against. An assembly reference contains the simple name, the compatibility version number, and the locale/culture of the referenced assembly. If the referenced assembly has a shared name, the reference also carries the public key of the referenced assembly. (Actually, to save space, the referencing assembly may hash the public key and persist the hashed form, but that's purely an optimization.)
      Note that this is just a record of the assembly reference at the time the code was compiled. By default, .NET will allow a reference to succeed as long as a compatible version is located at runtime¯it doesn't have to be the exact version requested. Using a configuration file, an administrator can tailor the exact rules governing the version bindings.
      Because the identity of a type includes the identity of the assembly in whose context it was loaded, multiple versions of a single type may be loaded at one time. In fact, the common language runtime delivers a range of technologies that enable it to provide application isolation. Among other things, this means that a version of a type used by one application does not affect the version of a type used by another. If you are careful in how you access resources, components can live side-by-side in .NET. As a matter of fact, so is the common language runtime itself, but I'll leave that for another discussion.
      Now that you've seen the difference between private assemblies (those without shared names) and published assemblies (those with shared names), I'd like to say a little more about deployment. Your application directory may contain all of the assemblies that the application needs to run. In that case, the common language runtime will use exactly what you deployed there because it honors the application directory first. Although this is the only place that the runtime will look for application private assemblies, you don't have to redeploy published assemblies if you don't want to. You may rely on them being already installed on the machine or you can supply a configuration file that tells the common language runtime where they might be found¯locally, on your intranet, or across the Internet. Aha, you say, what about the download scenario? What about security?

Verification and Security

      Security is integral to the design of .NET. It starts as soon as a class is loaded in the form of verification checks. Then it extends to the controlling code's access to resources via code access security. It provides mechanisms for determining roles and identity information and reaches across context, process, and machine boundaries so that you can be certain that your data are not compromised in remoting scenarios. It also digs deep into the common language run-time architecture to ensure that application isolation boundaries are honored. These mechanisms work with and extend the security mechanisms commonly found in today's operating systems. Overall, .NET Framework security spans the following areas:
Type Safety Type-safe programs reference only memory that has been allocated for their use and access objects only through their exposed interfaces. From a security standpoint, referencing only designated memory allows multiple objects to safely share a single address space. Accessing objects only through their exposed interfaces guarantees that security checks associated with specific interfaces are not circumvented.
      The common language runtime ensures type safety by combining strong typing in the metadata (parameters, members and array elements, return values of methods, and static values) with strong typing in the MSIL (local variables and stack slots). This provides a basis for automatically verifying the type safety of programs written in MSIL, independent of the language that produced them. By default, all code loaded by the .NET common language runtime requires verification before it is allowed to run. It is recognized that not all safe code may be verifiable with existing technology. You can bypass verification by explicitly trusting an application using administratively controlled policies.
Code Identity There are only two ways for code to become executable: through the class loader or through interoperability services (more about this later). Both of these are services provided by the common language runtime and are part of the security perimeter. For example, the class loader maintains information about the source of every implementation that it loads. Therefore, the class loader is able to reliably provide some of the evidence upon which you can base code identity. Evidence can include information such as which Internet zone and site the code originated from, its shared name, and its publisher's identity. Using this information, security policy can control the privileges granted to a specific application or collection of related applications.
Code Access Security This mechanism provides policy-based enforcement over privileges granted to executing assemblies. These privileges can control access to system resources and other code based on the notion of permissions. .NET provides a fairly broad set of permissions related to common resources and code evidence-based identity. Tools or application developers can extend these by adding new permission types to meet specific needs.
      Permissions are logically grouped into permission sets that are granted to assemblies by the .NET security system. These granted permissions define what the code is allowed to do. They are determined based on code evidence. The evidence is used to determine a set of policy code-groups for a given assembly, which is used by the security system to determine the permission set to grant.
      Permissions are typically demanded by trusted code such as a class that encapsulates access to the local file system. A demand causes the security system to check that the calling assemblies have the required permission. Most applications will simply make calls to such trusted resource-managing code and will not need to incorporate any security-specific code.
Resource Permissions These permissions are associated with information or communications resources within the system. Examples include files, display windows, clipboard, network channels, application private storage, and the ability to make calls to unmanaged APIs. (Application private storage is provided by IsolatedStorage, a new set of types and methods supported by the .NET platform.) These types of permissions can be used to determine if code has rights to access specific resources at either load time or runtime (see Declarative Security, discussed shortly).
      Consider the case of an application that wants to read and write a file in C:\Temp. To open a file in this directory, a managed application would use the base framework's System.IO.File class. This class contains a static method called Open. Internally, the Open method takes the path name of the file that the caller wants to open, and asks the common language runtime engine if access to the specified path name is allowed. This is called demanding access. In this case, the Open method demands that the calling code be granted a FileIO permission with read and write access to C:\Temp\*.* before it will attempt to open the file.
      When a method demands access, the common language runtime walks up the call stack and checks to see if all assemblies in the call chain have the required permission. If any code in the call chain lacks the required permission, then the demand fails and a security exception is thrown; otherwise, the demand succeeds. To complete the File.Open operation, the File class needs to call the OS file system APIs. This requires the new UnmanagedCode permission, which is typically granted to highly trusted resource-managing classes. The system will check that the File class has this permission before allowing the call to unmanaged code. You should note that the other semi-trusted assemblies in the call chain aren't expected to have been granted this permission. To allow the UnmanagedCode permission demand to succeed, highly trusted code is allowed to assert its right to use a permission. This causes the permission-check stack walk to terminate at the assembly that asserted the permission.
Identity Permissions These permissions are based on evidence associated with a given assembly (Site, Zone, Shared Name, and Publisher). They allow you to control access to class methods based on this information. Identity permissions operate in the same manner as resource permissions. Code demands that calling code have a given identity attribute. If the callers have the demanded identity attribute, the call succeeds; otherwise a security exception is thrown.
Declarative Security This powerful mechanism allows you to insert code access security checks into your code by annotating classes, fields, or methods. The declarations are encoded in the assembly metadata and are enforced by the .NET security system. Using these declarations, you can request checks to be performed at either load time or runtime.
      Though they are more limited, load-time checks are more efficient in that they are only checked once during execution, as opposed to checks on each call to a method. They only check the permissions (resource or identity) of the immediate caller, so you need to carefully assess whether this is adequate for a given situation. Typically, load-time checks are used to limit access to specific classes. For example, you can use a load-time identity check to ensure that only code from a specific publisher or with a given shared name public-key can be the immediate caller to a class. You can also use these checks to limit the ability to subclass or override virtual methods.
      Declarative checks at runtime are a simple means to indicate permission demands and asserts for a given type. As mentioned earlier, demands cause a full stack walk to check the permissions of all code in the call chain. They are appropriate when the requested action and permission attributes are static; if you always demand read permission to C:\Temp, it is easy to express this declaratively. But if you need to dynamically create the check to account for different access modes and directory/file names, then imperative checks (described next) are needed.
Imperative Security These checks are implemented as code within a method by the application developer. They are enforced in exactly the same manner as declarative runtime checks. The benefit of using them is that you can support dynamic determination of specific permissions that are required in a given execution context. This is typically important for permissions such as FileIO, IsolatedStorage, UI, and so forth, that support multiple access modes and subsetting of the resources being accessed.
Policy-based Security Enabling effective use of code access security requires a policy-based system. Policy allows you to make specific statements about what the code should be allowed to do based on its point of origin and other identifying information. Using this policy, appropriate privileges are automatically granted to the code without any required user interaction. .NET is slated to ship with a default policy that reflects the different levels of trust that users typically place on code on their local system, code from known trusted servers, and code from untrusted Internet sites. The user may refine this policy as they see fit.
Role-based Security It is fairly common for developers to make authorization decisions based on the identity or roles associated with the execution context. You often see this in financial or business systems as a means of policy enforcement. For example, you may impose limits on the value of a transaction, depending on the role of the user making the request. Clerks may be able to process transactions up to some set value, supervisors to some higher limit, and VPs to an even higher limit. .NET provides services to allow applications to easily incorporate such logic in a way that is platform-independent and scalable to the Internet.
      The .NET mechanisms are built around the notion of identities and principals. Identities encapsulate entity naming that the application understands. This can map to underlying OS notions of accounts, but can also be application-defined. The corresponding principal encapsulates the identity, along with corresponding role information. Again, this may or may not map to an operating system notion such as groups.
      In operation, a trusted authentication provider will typically compute the principal information based on credentials associated with a given request (HTTP Put, remote procedure call, and so on). The principal is then attached to the execution context, making it available to application code to check the role or identity information prior to taking a given action.
Remoting Security .NET provides support for remote object invocation that can span AppDomains, processes, or machines. (An AppDomain is a logical application; multiple AppDomains may run in a single Win32® process.) When crossing machine boundaries, security issues such as authentication, authorization, confidentiality, and integrity become critical. .NET will provide support for these mechanisms in a way that's compatible with existing network protocols and will integrate tightly into the remoting infrastructure. This will make it simple for applications to incorporate these security features.
      Authentication mechanisms will be suitable for identification of users as well as applications or business entities. A key-based identity infrastructure will be provided for managing such identities and integrating with key-based authentication protocols. Confidentiality and integrity services will take advantage of cryptographic techniques. It will be possible to use point-to-point mechanisms such as Secure Sockets Layer (SSL) and IP Security Protocol (IPSec). Application layer encryption, integrity, and digital signature technologies at the messaging level will also be supported in the .NET Framework.
Cryptography .NET will provide a set of cryptographic objects supporting well-known algorithms and common uses (hashing, encryption, and digital signatures). These objects are designed in a manner that facilitates the incorporation of these basic capabilities into more complex operations, such as signing and encrypting a document. Cryptographic objects are used by .NET to support internal services, but are also available to developers who need cryptographic support.

Putting it all Together

      Now that you've been introduced to the key concepts of the .NET common language runtime, let's actually deploy and run the code. Say I'm building an application that's a single assembly containing multiple files, as shown in Figure 3. The assembly manifest is embedded in MyApp.exe. It shows the three files that make up the assembly, which types are defined in the assembly's files (MyApp.exe and MyApp.dll), and what other assemblies the MyApp assembly requires to execute. In this example, MyApp requires the .NET base framework assembly (version 1.0, English).
      Let's say that my assembly has been copied to a user's machine and resides on their local hard disk. One of the assemblies that my application references has been installed into the global assembly cache on that user's machine (described in a little more detail later), so that any application can just find it there. The other assembly is not deployed with my application, but on a server on my company's intranet.
      The user double-clicks on my EXE. Since the PE file is a .NET file, the common language runtime initializes and establishes a "domain" for the application. The assembly is then loaded into the domain, essentially just reading in the assembly manifest. (Note that the common language runtime does not verify that all of its files are present or that referenced assemblies are available. Downloading an assembly really just downloads the file containing the manifest.) Next, the entry point for the application is loaded, verified, JITted (compiled with the JITCompiler), and the code begins to execute.
      As the code executes, it may make references to other types. These references will be in one of three forms: a reference to a type in the same file, a reference to a type in another file in the same assembly, or a reference to a type in a dependent assembly. If it is a reference to a type in the same file, the reference is early bound and the type is loaded out of the file directly. If it's a reference to a type in another file in the same assembly, the common language runtime makes sure that the file being referenced is, in fact, in the file list in the current assembly's manifest and then looks in the application's directory for the file to load. Finally, if it's a reference to a type in a dependent assembly, the common language runtime resolves the reference using the following heuristics:
  1. The common language runtime passes the assembly reference to the assembly resolver. The assembly resolver will pick up from the application's directory any configuration information that might affect the binding rules, such as version binding, and the result may be a modified form of the persisted assembly reference.
  2. If the reference does not have a shared name (that is, does not include a public key and is thus considered a private assembly):
    1. The assembly resolver looks in the specified application directory for a file with the same name as the assembly. If found, the assembly resolver makes sure that all of the attributes passed with the reference match those in the located manifest. (Actually, the heuristics are a little more complex than this, involving looking for naming patterns in the subdirectories as well.)
    2. If successful, the assembly resolver returns a pointer to the assembly manifest at its persisted file location.
    3. If not successful, a TypeLoadException is raised.
  3. If the reference has a shared name (that is, has been published with a public key, as in my example):
    1. The assembly resolver looks in the specified application directory for a file with the same name as the assembly. If found, the assembly resolver makes sure that all of the attributes passed with the reference match those in the located manifest, including using the public key to check the manifest file signature. The assembly resolver first looks in the application directory to give the application an opportunity to deploy a preferred version of the assembly, even if there might be another version already loaded in the global assembly cache.
    2. If successful, the assembly resolver returns a pointer to the assembly manifest at its persisted file location.
    3. If the file is not found in the application directory, the assembly resolver looks in its global cache to see if an appropriate version of the assembly has already been installed. If that is the case, the assembly resolver simply returns a pointer to the assembly manifest in the global assembly cache.
    4. If the file is not found in the specified application directory or in the global assembly cache, the assembly resolver uses a search path provided in the configuration information for the application. If the assembly was located off the machine, it is downloaded into a transient assembly cache.
    5. If the file is not found in any of these locations, a TypeLoadException is raised.
      The assembly cache is a directory normally residing in the \WinNT\Assembly directory. When assemblies are installed on the machine, they can get merged into the assembly cache. The assembly cache consists of two logically separate caches: the global assembly cache and the transient assembly cache.
      When using MSI files to distribute an assembly, the Windows installer package (the MSI file) can be authored so that assemblies are automatically added to the global assembly cache. When assemblies are downloaded using Microsoft Internet Explorer, the assembly is installed in the transient assembly cache. Keeping these assemblies logically separate prevents a downloaded module from adversely affecting an installed application.
      Note that the assembly cache can hold multiple versions of an assembly. For example, the assembly cache can contain Versions 1 and 2 of MyAssem.dll. If an application was built and tested using Version 1 of MyAssem.dll, then the common language runtime will load Version 1 of MyAssem.dll for that application even though a later version of the assembly exists in the cache. An administrator can change this version policy, but doing so is not recommended because the strict version policy resolves the classic DLL Hell problems. The application should behave as it always has because the code that it is executing is the same code that it was built and tested with. This also means that Version 2 of MyAssem.dll doesn't have to maintain backward compatibility with Version 1.

Interoperability with Unmanaged Code

      The managed code environment provided by the common language runtime hosts a ton of features that make software development quite a pleasure. However, the operating system on which the common language runtime executes also offers a number of features, and it would be a terrible misfortune if managed applications were prevented from using the native operating system's services or other unmanaged code.
      To this end, the common language runtime offers a System.Runtime.InteropServices namespace. This namespace contains a set of types that allow managed applications to access unmanaged code. Specifically, there are three supported interop scenarios: managed code calling unmanaged DLL functions, managed code instantiating and calling interface methods of COM servers, and unmanaged code instantiating and calling methods on .NET servers. To call an unmanaged DLL function, you just need to tell the common language runtime the name of the function you want to call (such as MessageBoxA), the name of the DLL containing the function (such as User32.dll), and how to marshal the function's parameters (for example, which parameters are input, output or input/output parameters).
      To work with a COM server, a .NET wrapper must be created so that .NET clients think that they are calling .NET code; the underlying details are handled seamlessly by the common language runtime. The TlbImp.exe utility parses a COM type library and produces a managed DLL whose metadata describes the methods that wrap the COM server's interface methods. A managed application can now create instances of the object and use it as though it were a native managed type. For each COM server, the runtime generates a runtime-callable wrapper. This wrapper acts as a proxy between the managed and unmanaged code, handling all administrative tasks such as marshaling, AddRef, Release, and so on.
      For an unmanaged application to instantiate and use a managed .NET server, the .NET object must be added to the system's registry (since that's how COM expects to locate COM servers). The register assembly utility, RegAsm.exe, generates a GUID for the .NET server and updates the registry. If the unmanaged code wants a type library, one can be generated using the TlbExp.exe utility. An unmanaged application can now create instances of the object and use it as though it were a COM server. For each type of managed .NET server, the common language runtime generates a COM-callable wrapper. This wrapper acts as a proxy between the unmanaged and managed code, handling all administrative tasks such as marshaling, AddRef, Release, and so on.
      Due to the common language runtime's interoperability features, developers do not have to give up anything in order to gain the benefits offered by the common language runtime. In addition, the common language runtime makes working with COM servers even easier than it would be from unmanaged C/C++ code. Of course, transitioning between managed and unmanaged code has its performance issues, so you should try to reduce these transitions when possible. This may mean porting your COM server to .NET. At least the interoperability features allow you to make this decision on your own time when you're ready.

Just the Beginning

      Parts 1 and 2 of this article introduced the .NET common language runtime and briefly mentioned some of the technologies that it offers. However, I haven't described the services Microsoft is delivering via its class libraries. In particular, Microsoft is creating a new database access model, called ADO+, that exposes database functionality via .NET types contained in the System.Data namespace. Microsoft is also creating a class library, called WinForms, for developing standalone apps with rich user interfaces. You can find these types in the System.WinForms namespace.
      Finally, Microsoft is providing a new architecture called Active Server Pages+ (ASP+) for producing Web applications. This architecture includes a class library of types that render complex visuals as HTML. The architecture also manages all client/server state information, making rapid Web application development a breeze. You can learn more about these types by examining the System.Web namespace.
      Users might not appreciate the common language runtime and its capabilities, but they will certainly notice the quality and features of the applications that utilize it. In addition, developers will certainly appreciate how the common language runtime allows applications to be built and deployed more rapidly and with less administration than Windows has ever allowed in the past.

For related articles see:
Microsoft .NET Framework Delivers the Platform for an Integrated, Service-Oriented Web
For background information see:
http://msdn.microsoft.com/NET/default.asp
http://www.microsoft.net/net/whitepaper.asp
Jeffrey Richter is the author of Programming Applications for Microsoft Windows (Microsoft Press, 1999). Jeff specializes in programming/design for .NET and Win32. He is also a cofounder of Wintellect, a software consulting and education firm, and can be reached at http://www.JeffreyRichter.com.

From the October 2000 issue of MSDN Magazine.

Page view tracker