Advanced .NET Assembly Internals: Part 2

Before reading this article, I highly recommend reading the previous part:

Abstract

In the previous article of this series we got an understanding of the assembly loading process, its format and anatomy. This article provides advanced concepts of Assemblies such as its classification in the form of private and shared assemblies, strong names, security and the Global Assembly Cache. This article will also explain the publisher policy and code base attributes related to assemblies and its configuration manipulation. We briefly illustrate the delayed signing and probing process of both types of assemblies.

Assembly Classification

Assemblies are the indispensable building blocks of the functionality of any kind in the .NET jurisdiction. When the Managed code is compiled into a format, the unit of execution is called an Assembly. The assemblies are far more reliable, more secure, well-behaved and easy to manage. An assembly can take on form of an EXE or DLL for libraries on which other programs and library may depend. Regardless of whether you create an EXE or DLL, an assembly always follows the Portable Executable / Common Object File Format (PE/COFF) format, the common format for binary code in Windows.

We can broadly classify assemblies as Private and Shared assemblies on the origin of their location and accessibility to the client.

Private Assembly

Private assemblies are the simplest type and normally ships with software. They are intended to be used only with that software, in other words that you have much more control of what software uses them. A private assembly is found either in the same directory or within one of its subdirectories. Unlike COM components, the private assembly would not register themselves into the registry to create a global presence in the system. In fact, private assemblies take the feature of traditional DLLs where they are private to the client and the files could be copied to the client's application directory and no other application must know about them.

Private assemblies are a normal way to build assemblies, especially when applications and components are built within the same infrastructure. Technically speaking, the code library that we have created so far in the previous article samples have been considered as private assemblies. Private Assemblies have at least a number of management issues and can be installed and uninstalled without any fear of breaking other applications. They don't even need to be strongly named.

A private assembly is entirely self-contained; the process of deploying is simple. You simply place the appropriate files into a suitable folder in the file system. You don't need to make an entry into the system registry.

The following sample shows the creation of a simple private assembly:

  1. using System;  
  2.   
  3. namespace PrivateAssem  
  4. {  
  5.     public class Test  
  6.     {  
  7.         private int X;  
  8.         public Test(int x)  
  9.         {  
  10.             this.X = x;  
  11.         }  
  12.         public int CalSquare()  
  13.         {  
  14.             return (X*X);  
  15.         }  
  16.     }  
  17. }  
                                                                                                                  Test.cs

When we compile this code, PrivateAssem.dll file is created in the solution folder that is further referenced in other client projects.

Shared Assembly

A shared assembly is a collection of types and resources. The main difference between a private and a shared assembly is that a single copy of a shared assembly can be consumed across several applications on the same machine whereas a private assembly is used once. Furthermore, a copy of a private assembly is copied into the solution reference directory. As a rule of thumb, when you are building libraries that need to be used by a variety of applications, a shared assembly can be quite helpful in that they can be updated to a new version very easily.

It is necessary to follow some semantics when using shared assemblies as such. They must be unique and therefore must have a unique name called a strong name. A shared assembly will mostly be used when a vendor, different from that of the application, builds the component, or when a large application is split into subprojects.

Global Assembly Cache (GAC)

The Global Assembly Cache (GAC) is a repository cache for globally available assemblies. The existing Framework Class Libraries (FCL) that is shipped with .NET is usually stored in the GAC from where they are referenced by the client applications.

The GAC viewer can be used to display it using shfusion.dll. It is a Windows shell extension to view and manipulate the contents of the cache. With the GAC we can view the global assembly name, type, version, culture and the public key token.

Note: you cannot install an executable (*.exe) assembly into the GAC. Only an assembly with the *.dll extension can be deployed as a shared assembly.

The following figure shows the GAC viewer that is opened by going into the <windir>\Assembly folder:

Assembly folder

Inside the <windir>\Assembly folder, you can find multiple GACxxx directories and the NativeImages_<runtime version> directory. The GACxxx directories contain shared assembies and GAC_MSIL contains the pure CIL coded assemblies. With NativeImages_<runtime version>, you can find the assemblies compiled to native code.

A shared assembly is not deployed within the same directory such as a private assembly. Rather, they are installed into the Global Assembly Cache. But some private assemblies can also be found there. Such magic can be done using the Native Image Generator Utility. If a private assembly is compiled to native code using the native image generator, the compiled native code goes into the GAC.

Native Image Generator

We can compile the IL code to native code at installation time. This way the program can start faster because the compilation during runtime is no longer necessary. The ngen.exe utility installs the native image in the native image cache. Here, we deploy the previously created PrivateAssem.dll assembly into the GAC as in the following:

Native Image Generator

After executing this command, your private assembly is installed into the GAC. You can view this as in the following:

private access

Creating Shared Assembly


In order to fully get an understanding, we shall explore how to create and consume assemblies with strong names and how to deploy them to the GAC using the GACUTIL.exe utility. This section describes the following operations:
  • Building Shared Assembly
  • Generating Strong Name
  • Installing shared assembly into GAC
  • Delay Signing

Building Shared Assembly

Building a shared assembly is not much different from creating private assemblies. The following sample creates a simple library project that calculates a maximum number.

  1. using System;  
  2.   
  3. namespace SharedAssem  
  4. {  
  5.     public class Magic  
  6.     {  
  7.         private int x,y;  
  8.         public Magic(int x,int y)  
  9.         {  
  10.             this.x = x;  
  11.             this.y = y;  
  12.         }  
  13.   
  14.         public int cal()  
  15.         {  
  16.             return Math.Max(x, y);    
  17.         }  
  18.     }  
  19. }  
Generating Strong Name

Before you can deploy an assembly into the GAC, you must assign it a strong name that uniquely identifies the publisher of the given .NET binary. As a rule of thumb, the shared assembly must be globally unique but there is a possibility that some other person creates an assembly using the same name. However, this problem is solved by generating a strong name. The strong name is accompanied by the following items:
  • Name of the assembly itself;
  • Version Number;
  • Public Key that ensure that strong name is unique;
  • Culture

To provide a strong name for an assembly, the first step is to generate public/private key data using the .NET Framework utility sn.exe that generates a file with *.snk extension that contains data for two distinct but mathematically related keys. Here is the process of creating a strong name:

command prompt

After executing this command, the SecKey.snk file is created that is consumed by a shared assembly in two ways. First it makes the following entry in the AssemblyInfo.cs file:

[assembly: AssemblyKeyFile(@"D:\SecKey.snk")]

Secondly it opens the property of the Project and goes to the Signing tab, checks the Sign the assembly option and chooses the SecKey.snk file as in the following:

SecKey snk

At compile time, a digital signature is generated and embedded into the assembly based in part on public and private key data. You can view the public key entry in the CIL code as in the following:

CIL code

Installing Shared Assembly into GAC

The final step is to deploy SharedAssem.dll into the GAC. So first open Visual Studio command prompt with admin rights and go to the project bin folder as in the following:

bin folder

Once you have done so, you can verify whether or not the library has been deployed successfully into the GAC by using the following command:

cmd prompt

Delay Signing

The private key of the company should be safely stored and mostly not all developers shoud be provided access to it. That is why the signature of the assembly is added at a later date such as before distribution. Without using a key, you cannot test the assembly and install it in the GAC. So type the following commands.

First create a new unique private key pair for our assembly:

Sn –k test.snk

Since we don't want to provide the key pair file containing both private and public key, we will extract the public key from our key pair as in the following:

Sn test.snk pKey.snk

The public key is extracted to the file called pKey.snk. Now add the reference of the pKey.snk file to the Signing portion as earlier.

Finally, build the application and you will get the following error as;

cryptographic

This is happening because we still have not set up our assembly to skip the private key signing process. We can turn off the verification using the following command:

Sn /vr SharedAssem.dll

Consuming Shared Assembly

When you install the shared assembly into the GAC and try to browse it using Windows Explorer you will not find any GAC_MSIL there. In .NET 4.0 the deployed assembly is shifted the to  <windir>Microsoft.NET\Assembly\GAC_MSIL folder.

When you are building applications that use a shared assembly, the only difference from consuming a private assembly is in how you reference the library using the Visual Studio IDE. But there is no difference, as far as Tool is concerned; you still use Add Reference and browse to the shared assembly from the earlier specified path as in the following:

Consuming Shared Assembly

Therefore, make an import to the SharedAssem library into the source file and use the following code:

  1. using System;  
  2. using SharedAssem;   
  3.   
  4. namespace FrontEnd  
  5. {  
  6.     class Program  
  7.     {  
  8.         static void Main(string[] args)  
  9.         {  
  10.             Magic obj = new Magic(5,7);  
  11.             Console.WriteLine("Maximum Number: {0}",obj.cal());  
  12.             Console.ReadLine();   
  13.         }  
  14.     }  
  15. }  
Assembly Configuration

Version Control

Imagine that you have shipped version 1.0.0.0 of the SharedAssem.dll assembly and later discover a major bug or need to change the code due to some new specification. So one corrective solution would be to correct the problem and rebuild the assembly and finally redistribute it to the client. So in such circumstance the client has no clue of the new definitions and their application might not work properly.

Another better solution is to create a new version of the existing assembly in which all the changes would be so that the client can run two versions of the assembly in parallel.

The following image shows the version 1.0.0.0 of the old SharedAssem.dll assembly as in the following:

assembly information

Now we do some modification in the original SharedAssem.dll file as per the new specifications. The most important point is, we change the version number to 2.0.0.0 this time and rebuild this assembly and finally redistribute the assembly. The changes should be as in the following:

redistributed assembly

Finally install this newly updated assembly using GACUTIL.exe. Now if you notice in your project Bin/Debug folder, you have a new version of the assembly to consume. Here you can choose, which version you want to use during Add Reference.

Add Reference

From the specified previous two versions of the same DLLs, we can resolve the inherent “DLL Hell” problem. We can redirect our client to a new version of an assembly without having to build the client application again. It is just required to configure our client FrontEnd.exe.config file as in the following:
  1. <?xml version="1.0" encoding="utf-8" ?>  
  2. <configuration>  
  3.   <assemblyBinding xmlns="urns:schemas-microsoft-com:asm.v1">  
  4.     <dependentAssembly>  
  5.       <assemblyIdentity name="SharedAssem"  
  6.                         publicKeyToken="B77A5C561934E089"  
  7.                         culture="neutral"/>  
  8.       <bindingRedirect oldVersion="1.0.0.0"  
  9.                        newVersion="2.0.0.0" />  
  10.     </dependentAssembly>  
  11.   </assemblyBinding>   
  12. </configuration>  
Assembly Probing

The .NET runtime resolves the location of a private assembly using probing. Probing is the process of mapping an external assembly request to the location of the requested binary file. An assembly can be loaded either implicitly or explicitly. An implicit request uses the reference of the external library file into the source code file that can be found by the .assembly token in MSIL code.

In the following sample, we are using a reference of the external TestLib.dll file in a console based application to utilize its functionality. Here you can see the TestLib entry in the metadata as in the following:

Assembly Probing

An explicit load request happens programmatically using late binding and Reflection in which the LoadFrom() and Load() methods use an external assembly reference as in the following:

Assembly am= Assembly.Load(“TestLib”);

The CLR extracts the friendly name of the assembly and begins probing the client's application directory for a file name TestLib.dll. If this file cannot be located, an attempt is made to locate an executable assembly based on the same friendly name. If both attempts fail then the runtime throws FileNotFoundException error.

CodeBase

In some circumstances, you need to load an assembly that does not reside in the GAC and then probing would help. Your company has a policy that critical assemblies aren't put into the GAC. Rather they are put into the company secure repository. So you have to redirect your application to look for these assemblies in the central repository. You can do this by setting the CodeBase attribute in your application configuration file as in the following:
  1. <dependentAssembly>  
  2.       <assemblyIdentity name="SharedAssem"  
  3.                         publicKeyToken="B77A5C561934E089"  
  4.                         codeBase="" version="2.0" href="d:\secure\NewGAC" />  
  5.     </dependentAssembly>  
PublisherPolicy

Assume that you have an assembly used by some applications. What can be done if a bug is found in the shared assembly? Yes, you can use configuration files to redirect to the new version of this shared assembly. But probably some applications aren't aware of this new version. They still encounter this problem. So using publisher policy, you can fix this problem for all applications that are using that shared assembly. Publisher policy only applies to the shared assemblies that are deployed in the GAC.

In order to create a publisher policy file, first make the change in the application configuration file as in the following:
  1. <dependentAssembly>  
  2.       <assemblyIdentity name="SharedAssem"  
  3.                         publicKeyToken="B77A5C561934E089" />  
  4.       <bindingRedirect oldVersion="1.0.0.0-2.0.0.0"  
  5.                        newVersion="2.0.0.0" />  
  6. </dependentAssembly>  
Then, to associate the publisher policy file with the shared assembly, it is necessary to create a publisher policy assembly and put this into the GAC. This can be done using the al.exe utility as in the following:

al /linkresource:App.config /out:policy.1.0.SharedAssem.dll /keyfile:..\..\SecKey.snk

Finally add this publisher policy assembly into the GAC using the gacutil.exe as in the following:

Gacutil –I policy.1.0.SharedAssem.dll

Conclusion

In this article, we have learned the difference between private and shared assemblies and see how private and shared are created. With private assemblies, you don't need to care about the uniqueness and versioning issues because these assemblies are copied and are only used by a single application. Shared assemblies on the other hand, require you to use a key for uniqueness and to define the version. This article also shows the side-by-side execution of a shared assembly and discusses the assembly central repository in the GAC where we have seen the installation of a shared assembly. Finally, you learned how probing, codebase and publisher policy works with assemblies.