In .NET, the basic unit deployable is called an assembly. Assemblies play an important part of the development process where understanding how they work is useful in helping you develop scalable, efficient .NET applications. This chapter explores:
The components that make up a .NET assembly
The difference between single-file and multi-file assemblies
The relationships between namespaces and assemblies
The role played by the Global Assembly Cache (GAC)
How to develop a shared assembly, which can be shared by other applications
In .NET, an assembly takes the physical form of an EXE (known as a process assembly) or DLL (known as a library assembly) file, organized in the Portable Executable (PE) format. The PE format is a file format used by the Windows operating system for storing executables, object code, and DLLs. An assembly contains code in IL (Intermediate Language; compiled from a .NET language), which is then compiled into machine language at runtime by the Common Language Runtime (CLR) just-in-time compiler.
An assembly consists of the following four parts (see Figure 15-1).
Physically, all four parts can reside in one physical file, or some parts of an assembly can be stored other modules. A module can contain type metadata and IL code, but it does not contain assembly metadata. Hence, a module cannot be deployed by itself; it must be combined with an assembly to be used. Figure 15-2 shows part of an assembly stored in two modules.
An assembly is the basic unit of installation. In this example, the assembly is made up of three files (one assembly and two modules). The two modules by themselves cannot be installed separately; they must accompany the assembly.
As mentioned briefly in Chapter 1, you can use the MSIL Disassembler tool (ildasm.exe
) to examine the content of an assembly. Figure 15-3 shows the tool displaying an assembly's content.
Among the various components in an assembly, the most important is the manifest (shown as MANIFEST in Figure 15-3), which is part of the assembly metadata. The manifest contains information such as the following:
Name, version, public key, and culture of the assembly
Files belonging to the assembly
References assemblies (other assemblies referenced by this assembly)
Permission sets
Exported types
Figure 15-4 shows the content of the manifest of the assembly shown in Figure 15-3.
In Visual Studio, each project that you create will be compiled into an assembly (either EXE or DLL). By default, a single-file assembly is created. Imagine you are working on a large project with10 other programmers. Each one of you is tasked with developing part of the project. But how do you test the system as a whole? You could ask every programmer in the team to send you his or her code and then you could compile and test the system as a whole. However, that really isn't feasible, because you have to wait for everyone to submit his or her source code. A much better way is to get each programmer to build his or her part of the project as a standalone library (DLL). You can then get the latest version of each library and test the application as a whole. This approach has an added benefit — when a deployed application needs updating, you only need to update the particular library that needs updating. This is extremely useful if the project is large. In addition, organizing your project into multiple assemblies ensures that only the needed libraries (DLLs) are loaded during runtime.
To see the benefit of creating multi-file assemblies, let's create a new Class Library project, using Visual Studio 2008, and name it MathUtil
. In the default Class1.cs
, populate it with the following code:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace MathUtil { public class Utils { public int Fibonacci(int num) { if (num <= 1) return 2; //---should return 1; error on purpose--- return Fibonacci(num - 1) + Fibonacci(num - 2); } } }
This Utils
class contains a method called Fibonacci()
, which returns the nth number in the Fibonacci sequence (note that I have purposely injected an error into the code so that I can later show you how the application can be easily updated by replacing the DLL). Figure 15-5 shows the first 20 numbers in the correct Fibonacci sequence.
Build the Class Library project (right-click on the project's name in Solution Explorer, and select Build) so that it will compile into a DLL — MathUtil.dll
.
Add a Windows Application project to the current solution, and name it WindowsApp-Util
. This application will use the Fibonacci()
method defined in MathUtil.dll
. Because the MathUtil.dll
assembly is created in the same solution as the Windows project, you can find it in the Projects tab of the Add Reference dialog (see Figure 15-6). Select the assembly, and click OK.
The MathUtil.dll
assembly will now be added to the project. Observe that the Copy Local
property for the MathUtil.dll
assembly is set to True
(see Figure 15-7). This means that a copy of the assembly will be placed in the project's output directory (that is, the binDebug folder).
When you add a reference to one of the classes in the .NET class library, the Copy Local property for the added assembly will be set to False. That's because the .NET assembly is in the Global Assembly Cache (GAC), and all computers with the .NET Framework installed have the GAC. The GAC is discussed later in this chapter.
Switch to the code-behind of the default Form1
and code the following statements:
namespace WindowsApp_Util { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { CallUtil(); } private void CallUtil() { MathUtil.Utils util = new MathUtil.Utils(); MessageBox.Show(util.Fibonacci(7).ToString()); } } }
Set a breakpoint at the CallMathUtil()
method (see Figure 15-8).
Right-click on the WindowsApp-Util
project name in Solution Explorer, and select Start as Startup Project. Press F5 to debug the application. When the application stops at the breakpoint, view the modules loaded into memory by selecting Debug
Observe that MathUtil.dll
library has not been loaded yet. Press F11 to step into the CallMathUtil()
function (see Figure 15-10). The MathUtil.dll
library is now loaded into memory.
Press F5 to continue the execution. You should see a message box displaying the value 42. In the binDebug folder of the Windows application project, you will find the EXE assembly as well as the DLL assembly (see Figure 15-11).
The Fibonacci()
method defined in the MathUtil
project contains a bug. When num
is less than or equal to 1, the method should return 1 and not 2. In the real world, the application and the DLL may already been deployed to the end user's computer. To fix this bug, you simply need to modify the Utils
class, recompile it, and then update the user's computer with the new DLL:
namespace MathUtil { public class Utils { public int Fibonacci(int num) { if (num <= 1) return 1; //---fixed!--- return Fibonacci(num - 1) + Fibonacci(num - 2); } } }
Copy the recompiled MathUtil.dll
from the binDebug folder of the MathUtil
project, and overwrite the original MathUtil.dll
located in the binDebug folder of the Windows project. When the application runs again, it will display the correct value, 21 (previously it displayed 42).
Because the
MathUtil.dll
assembly is not digitally signed, a hacker could replace this assembly with one that contains malicious code, and the client of this assembly (which is the WindowsApp-Util application in this case) would not know that the assembly has been tampered with. Later in this chapter, you will see how to give the assembly a unique identity using a strong name.
An application using a library loads it only when necessary — the entire library is loaded into memory during runtime. If the library is large, your application uses up more memory and takes a longer time to load. To solve this problem, you can split an assembly into multiple modules and then compile each individually as a module. The modules can then be compiled into an assembly.
To see how you can use a module instead of an assembly, add a new Class Library project to the solution used in the previous section. Name the Class Library project StringUtil
. Populate the default Class1.cs
file as follows:
using System.Text.RegularExpressions; namespace StringUtil { public class Utils { public bool ValidateEmail(string email) { string strRegEx = @"^([a-zA-Z0-9_-.]+)@(([[0-9]{1,3}" + @".[0-9]{1,3}.[0-9]{1,3}.)|(([a-zA-Z0-9-]+" + @".)+))([a-zA-Z]{2,4}|[0-9]{1,3})(]?)$"; Regex regex = new Regex(strRegEx); if (regex.IsMatch(email)) return (true); else return (false); } } }
Instead of using Visual Studio 2008 to build the project into an assembly, use the C# compiler to manually compile it into a module.
To use the C# compiler, launch the Visual Studio 2008 Command Prompt (Start
Navigate to the folder containing the StringUtil
project, and type in the following command to create a new module:
csc /target:module /out:StringUtil.netmodule Class1.cs
When the compilation is done, the StringUtil.netmodule
file is created (see Figure 15-12).
Do the same for the MathUtil
class that you created earlier (see Figure 15-13):
csc /target:module /out:MathUtil.netmodule Class1.cs
Copy the two modules that you have just created — StringUtil.netmodule
and MathUtil.netmodule
— into a folder, say C:Modules. Now to combine these two modules into an assembly, type the following command:
csc /target:library /addmodule:StringUtil.netmodule /addmodule:MathUtil.netmodule /out:Utils.dll
This creates the Utils.dll
assembly (see Figure 15-14).
In the WindowsApp-Utils
project, remove the previous versions of the MathUtil.dll
assembly and add a reference to the Utils.dll
assembly that you just created (see Figure 15-15). You can do so via the Browse tab of the Add Reference dialog (navigate to the directory containing the modules and assembly, C:Modules). Click OK.
In the code-behind of Form1
, modify the following code as shown:
namespace WindowsApp_Util { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { CallMathUtil(); CallStringUtil(); } private void CallMathUtil() { MathUtil.Utils util = new MathUtil.Utils(); MessageBox.Show(util.Fibonacci(7).ToString()); } private void CallStringUtil() { StringUtil.Utils util = new StringUtil.Utils(); MessageBox.Show(util.ValidateEmail( "[email protected]").ToString()); } } }
The CallMathUtil()
function invokes the method defined in the MathUtil
module. The CallStringUtil()
function invokes the method defined in the StringUtil
module.
Set a break point in the Form1_Load
event handler, as shown in Figure 15-16, and press F5 to debug the application.
When the breakpoint is reached, view the Modules window (Debug
Press F11 to step into the CallMathUtil()
function, and observe that the Utils.dll
assembly is now loaded, together with the MathUtil.netmodule
(see Figure 15-18).
Press F11 a few times to step out of the CallMathUtil()
function until you step into CallStringUtil()
. See that the StringUtil.netmodule
is now loaded (see Figure 15-19).
This example proves that modules in an assembly are loaded only as and when needed. Also, when deploying the application, the Util.dll
assembly and the two modules must be in tandem. If any of the modules is missing during runtime, you will encounter a runtime error, as shown in Figure 15-20.
As you know from Chapter 1, the various class libraries in the .NET Framework are organized using namespaces. So how do namespaces relate to assemblies? To understand the relationship between namespaces and assemblies, it's best to take a look at an example.
Create a new Class Library project in Visual Studio 2008, and name it ClassLibrary1
. In the default Class1.cs
, populate it with the following:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Learn2develop.net { public class Class1 { public void DoSomething() { } } }
Observe that the definition of Class1
is enclosed within the Learn2develop.net
namespace. The class also contains the DoSomething()
method.
Add a new class to the project by right-clicking on the project's name in Solution Explorer and selecting Add
Use the default name of Class2.cs
. In the newly added Class2.cs
, code the following:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Learn2develop.net { public class Class2 { public void DoSomething() { } } }
Class2
is enclosed within the same namespace — Learn2develop.net
, and it also has a DoSomething()
method. Compile the ClassLibrary1
project so that an assembly is generated in the binDebug folder of the project — ClassLibrary1.dll
. Add another Class Library project to the current solution and name the project ClassLibrary2
(see Figure 15-22).
Populate the default Class1.cs
as follows:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Learn2develop.net { public class Class3 { public void DoSomething() { } } } namespace CoolLabs.net { public class Class5 { public void DoSomething() { } } }
This file contains two namespaces — Learn2develop.net
and CoolLabs.net
— each containing a class and a method.
Compile the ClassLibrary2
project so that an assembly is generated in the binDebug folder of the project — ClassLibrary2.dll
.
Now, add another Class Library project to the current solution, and this time use the Visual Basic language. Name the project ClassLibrary3
(see Figure 15-23).
In the Properties page of the ClassLibrary3
project, set its root namespace to Learn2develop.net
(see Figure 15-24).
In the default Class1.vb
, define Class4
and add a method to it:
Public Class Class4 Public Sub DoSomething() End Sub End Class
Compile the ClassLibrary3
project so that an assembly is generated in the binDebug folder of the project — ClassLibrary3.dll
.
Now add a new Windows application project (name it WindowsApp
) to the current solution so that you can use the three assemblies (ClassLibrary1.dll, ClassLibrary2.dll
, and ClassLibrary3.dll
) that you have created.
To use the three assemblies, you need to add a reference to all of them. Because the assemblies are created in the same solution as the current Windows project, you can find them in the Projects tab of the Add Reference dialog (see Figure 15-25).
In the code-behind of the default Form1
, type the Learn2develop.net
namespace, and IntelliSense will show that four classes are available (see Figure 15-26).
Even though the classes are located in different assemblies, IntelliSense still finds them because all these classes are grouped within the same namespace. You can now use the classes as follows:
Learn2develop.net.Class1 c1 = new Learn2develop.net.Class1(); c1.DoSomething(); Learn2develop.net.Class2 c2 = new Learn2develop.net.Class2(); c2.DoSomething();
Learn2develop.net.Class3 c3 = new Learn2develop.net.Class3(); c3.DoSomething(); Learn2develop.net.Class4 c4 = new Learn2develop.net.Class4(); c4.DoSomething();
For Class5
, you need to use the CoolLabs.net
namespace. If you don't, IntelliSense will check against all the referenced assemblies and suggest an appropriate namespace (see Figure 15-27).
You can use Class5
as follows:
CoolLabs.net.Class5 c5 = new CoolLabs.net.Class5(); c5.DoSomething();
To summarize, this example shows that:
Classes belonging to a specific namespace can be located in different assemblies.
An assembly can contain one or more namespaces.
Assemblies created using different languages are transparent to each other.
So far, all the assemblies you have seen and created are all private assemblies — that is, they are used specifically by your application and nothing else. As private assemblies, they are stored in the same folder as your executable and that makes deployment very easy — there is no risk that someone else has another assembly that overwrites yours particular and thus breaks your application.
But assemblies can also be shared — that is, used by more than one application running on the computer. Shared assemblies are useful if they provide generic functionalities needed by most applications. To prevent DLL Hell, Microsoft has taken special care to make sure that shared assemblies are well protected. First, all shared assemblies are stored in a special location known as the Global Assembly Cache (GAC). Second, each shared assembly must have a strong name to uniquely identify itself so that no other assemblies have the same name.
A strong name comprises the following:
Name of the assembly
Version number
Public key
Culture
To deploy an assembly as a shared assembly, you need to create a signature for your assembly by performing the following steps:
These steps guarantee that the assembly cannot be altered in any way, ensuring that the shared assembly you are using is the authentic copy provided by the vendor. The signature can be verified using the public key.
The following sections will show you how to perform each of these steps.
For the client application using the shared assembly, the compiler writes the public key of the shared assembly to the manifest of the client so that it can unique identify the shared assembly (only the last 8 bytes of a hash of a public key are stored; this is known as the public key token and is always unique). When an application loads the shared assembly, it uses the public key stored in the shared assembly to decrypt the encrypted hash and match it against the hash of the shared assembly to ensure that the shared assembly is authentic.
You'll better understand how to create a shared assembly by actually creating one. In this example, you create a library to perform Base64 encoding and decoding. Basically, Base64 encoding is a technique to encode binary data into a text-based representation so that it can be easily transported over networks and Web Services. A common usage of Base64 is in emails.
Using Visual Studio 2008, create a new Class Library project and name it Base64Codec
. In the default Class1.cs
, define the Helper
class containing two methods — Decode()
and Encode()
:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Base64Codec { public class Helper { public byte[] Decode(string base64string) { byte[] binaryData; try { binaryData = Convert.FromBase64String(base64string); return binaryData; }
catch (Exception) { return null; } } public string Encode(byte[] binaryData) { string base64String; try { base64String = Convert.ToBase64String( binaryData, 0, binaryData.Length); return base64String; } catch (Exception) { return string.Empty; } } } }
To create a strong name for the assembly, you need to sign it. The easiest way is to use the Properties page of the project in Visual Studio 2008. Right-click on the project name in Solution Explorer, and select Properties. Select the Signing tab (see Figure 15-28), and check the Sign The Assembly checkbox. Select <New> from the Choose A Strong Name Key File dropdown list to specify a name for the strong name file.
In the Create Strong Name Key dialog (see Figure 15-29), specify a name to store the pair of keys (KeyFile.snk
, for instance). You also have the option to protect the file with a password. Click OK.
A strong name file is now created in your project (see Figure 15-30).
Alternatively, you can also use the command line to generate the strong name file:
sn -k KeyFile.snk
With .NET, you can create different versions of the same assembly and share them with other applications. To specify version information, you can edit the AssemblyInfo.cs
file, located under the Properties item in Solution Explorer (see Figure 15-31).
In the AssemblyInfo.cs
file, locate the following lines:
... // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")]
The version number of an assembly is specified using the following format:
[Major Version, Minor Version, Build Number, Revision]
The AssemblyVersion
attribute is used to identify the version number of an assembly. Applications that use this particular assembly reference this version number. If this version number is changed, applications using this assembly will not be able to find it and will break.
The AssemblyFileVersion
attribute is used to specify the version number of the assembly, and it shows up in the properties page of the assembly (more on this in a later section).
Build the Class Library project so that Visual Studio 2008 will now generate the shared assembly and sign it with the strong name. To examine the shared assembly created, navigate to the binDebug folder of the project and type in the following command:
ildasm Base64Codec.dll
Figure 15-32 shows the public key stored in the manifest of the shared assembly.
You can obtain the public key token of the shared assembly by using the following command:
sn -T Base64Codec.dll
Figure 15-33 shows the public key token displayed in the console window. Note this number because you will use it for comparison later.
Now that you have created a shared assembly, the next task is to put it into the GAC. The GAC is a central repository of .NET assemblies that can be shared by all applications. There are several reasons why you should put your shared assembly into the GAC, some of which are:
Security — Assemblies stored in the GAC are required to be signed with a cryptographic key. This makes it difficult for others to tamper with your assembly, such as replacing or injecting your shared assembly with malicious code.
Version management — Multiple versions of the same assembly can reside in the GAC so that each application can find and use the version of your assembly to which it was compiled. This helps to avoid DLL Hell, where applications compiled to different versions of your assembly can potentially break because they are all forced to use a single version of your assembly.
Faster loading — Assemblies are verified when they are first installed in the GAC, eliminating the need to verify an assembly each time it is loaded from the GAC. This improves the startup speed of your application if you load many shared assemblies.
The GAC is located in <windows_directory>Assembly. In most cases, it is C:WindowsAssembly. When you navigate to this folder by using Windows Explorer, the Assembly Cache Viewer launches to display the list of assemblies stored in it (see Figure 15-34).
To put the shared assembly that you have just built into the GAC, drag and drop it onto the Assembly Cache Viewer. Alternatively, you can also use the gacutil.exe
utility to install the shared assembly into the GAC (see Figure 15-35):
gacutil /i Base64Codec.dll
If you are using Windows Vista, make sure to run the command prompt as Administrator.
If the installation is successful, you will see the shared assembly in the Assembly Cache Viewer (see Figure 15-36).
The version number displayed next to the DLL is specified by using the AssemblyVersion
attribute in the AssemblyInfo.cs
file (as discussed earlier). Select the Base64Codec DLL, and click the Properties button (the button with the tick icon) to see the Properties page as shown in Figure 15-37.
The version number displayed in this page is specified using the AssemblyFileVersion
attribute.
To install different versions of the same assembly to the GAC, simply modify the version number in
AssemblyInfo.cs
(via theAssemblyVersion
attribute), recompile the assembly, and install it into the GAC.
Physically, the shared assembly is copied to a folder located under the GAC_MSIL
subfolder of the GAC, in the following format:
<Windows_Directory>assemblyGAC_MSIL<Assembly_Name><Version>_<Public_Key_Token>
In this example, it is located in:
C:WindowsassemblyGAC_MSILBase64Codec1.0.0.0_2a7dec4fb0bb6
Figure 15-38 shows the physical location of the Base64Codec.dll
assembly.
By default, adding a shared assembly into the GAC does not make it appear automatically in Visual Studio's Add Reference dialog. You need to add a registry key for that to happen. Here's how to handle that.
First, launch the registry editor by typing regedit
in the Run command box.
If you are using Windows Vista, make sure to run regedit
as Administrator.
Navigate to the HKEY_LOCAL_MACHINESOFTWAREMicrosoft.NETFrameworkAssemblyFolders
key. Right-click on the AssemblyFolders key and select New
Name the new key Base64Codec
. Double-click on the key's (Default
) value, and enter the full path of the shared assembly (for example, C:Documents and SettingsWei-Meng LeeMy DocumentsVisual Studio 2008ProjectsBase64CodecinDebug
; see Figure 15-40).
Then restart Visual Studio 2008, and the assembly should appear in the Add Reference dialog.
Let's now create a new Windows application project to use the shared assembly stored in the GAC. Name the project WinBase64
.
To use the shared assembly, add a reference to the DLL. In the Add Reference dialog, select the Base64Codec
assembly, as shown in Figure 15-41, and click OK.
Note in the Properties window that the Copy Local property of the Base64Codec
is set to False
(see Figure 15-42), indicating that the assembly is in the GAC.
Populate the default Form1
with the controls shown in Figure 15-43 (load the pictureBox1
with a JPG image).
In the code-behind of Form1
, define the two helper functions as follows:
Remember to import the System.IO
namespace for these two helper functions.
public byte[] ImageToByteArray(Image img) { MemoryStream ms = new MemoryStream(); img.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); return ms.ToArray(); } public Image ByteArrayToImage(byte[] data) { MemoryStream ms = new MemoryStream(data); Image img = new Bitmap(ms); return img; }
Code the Test button as follows:
private void btnTest_Click(object sender, EventArgs e) { //---create an instance of the Helper class--- Base64Codec.Helper codec = new Base64Codec.Helper(); //---convert the image in pictureBox1 to base64---
string base64string = codec.Encode(ImageToByteArray(pictureBox1.Image)); //---decode the base64 to binary and display in pictureBox2--- pictureBox2.Image = ByteArrayToImage(codec.Decode(base64string)); }
Here you are creating an instance of the Helper
class defined in the shared assembly. To test that the methods defined in the Helper
class are working correctly, encode the image displayed in pictureBox1
to base64, decode it back to binary, and then display the image in pictureBox2
.
Press F5 to test the application. When you click the Test button, an identical image should appear on the right (see Figure 15-44).
Examine the manifest of the WinBase64.exe
assembly to see the reference to the Base64Codec assembly (see Figure 15-45). Observe the public key token stored in the manifest — it is the public key token of the shared assembly.
This chapter explained the parts that make up a .NET assembly. Splitting your application into multiple assemblies and modules will make your application easier to manage and update. At the same time, the CLR will only load the required assembly and modules, thereby making your application more efficient. If you have a shared assembly that can be used by other applications, consider deploying it into the Global Assembly Cache (GAC).
3.148.144.228