A single-module assembly has a single file that can be an EXE or DLL file. This single module contains all the types and implementations for the application. The assembly manifest is embedded within this module.
A multi-module assembly consists of multiple files (zero or one EXE and zero or more DLL files, though you must have at least one EXE or DLL). The assembly manifest in this case can reside in a standalone file, or it can be embedded in one of the modules. When the assembly is referenced, the runtime loads the file containing the manifest and then loads the required modules as needed.
Multi-module assemblies have advantages for real-world programs, especially if they are developed by multiple developers or are very large.
Imagine that 25 developers are working on a single project. If they were to create a single-module assembly to build and test the application, all 25 programmers would have to check in their latest code simultaneously, and the entire mammoth application would be built. That creates a logistical nightmare.
If they each build their own modules, however, the program can be built with the latest available module from each programmer. This relieves the logistics problems; each module can be checked in when it is ready.
Perhaps more importantly, multiple modules make it easier to deploy and to maintain large programs. Imagine that each of the 25 developers builds a separate module, each in its own DLL. The person responsible for building the application would then create a 26th module with the manifest for the entire assembly. These 26 files can be deployed to the end user. The end user then need only load the one module with the manifest, and he can ignore the other 25. The manifest will identify which of the 25 modules has each method, and the appropriate modules will be loaded as methods are invoked. This will be transparent to the user.
As modules are updated, the programmers only need to send the updated modules (and a module with an updated manifest). Additional modules can be added and existing modules can be deleted; the end user continues to load only the one module with the manifest.
In addition, it is entirely likely that not all 25 modules will need to be loaded into the program. By breaking the program into 25 modules, the loader can load only those parts of the program that are needed. This makes it easy to shunt aside code that is only rarely needed into its own module, which might not be loaded at all in the normal course of events. Although this was the theory behind DLLs all along, .NET accomplishes this without “DLL Hell,” a monumental achievement described later in this chapter.
To demonstrate
the use of multi-module assemblies, the following example creates a
couple of very simple modules that you can then combine into a single
assembly. The first module is a Fraction
class.
This simple class will allow you to create and manipulate common
fractions. Example 17-1 illustrates.
Example 17-1. The Fraction class
namespace ProgCS { using System; public class Fraction { public Fraction(int numerator, int denominator) { this.numerator = numerator; this.denominator = denominator; } public Fraction Add(Fraction rhs) { if (rhs.denominator != this.denominator) { throw new ArgumentException( "Denominators must match"); } return new Fraction( this.numerator + rhs.numerator, this.denominator); } public override string ToString( ) { return numerator + "/" + denominator; } private int numerator; private int denominator; } }
Notice that the Fraction
class is in the
ProgCS
namespace. The full name for the
class will be ProgCS.Fraction
.
The Fraction
class takes two values in its
constructor: a numerator
and a
denominator
. There is also an Add( )
method, which takes a second Fraction
and returns
the sum, assuming the two share a common
denominator
. This class is simplistic, but it will
demonstrate the functionality necessary for this example.
The second class is the myCalc
class, which stands
in for a robust calculator. Example 17-2 illustrates.
Example 17-2. The Calculator
namespace ProgCS { using System; public class myCalc { public int Add(int val1, int val2) { return val1 + val2; } public int Mult(int val1, int val2) { return val1 * val2; } } }
Once again, myCalc
is a very stripped-down class
to keep things simple. Notice that calc
is also in
the ProgCS
namespace.
This is sufficient to create an assembly. You’ll use an AssemblyInfo.cs file to add some metadata to the assembly. The use of metadata will be covered in Chapter 19.
You can write your own AssemblyInfo.cs file, but the simplest approach is to let Visual Studio generate one for you automatically.
Visual Studio creates single-module assemblies by default. You can
create a multi-module resource with the
/addModules
command line. The easiest way to
compile and build a multi-module assembly is with a
makefile
, which you can create with Notepad or any
text editor.
If you are unfamiliar with makefiles, don’t worry; this is the only example that needs a makefile, and that is only to get around the current limitation of Visual Studio creating only single-module assemblies. If necessary, you can just use the makefile as offered without fully understanding every line.
Example 17-3 shows the complete
makefile
(which is explained in detail immediately
afterward).
Example 17-3. The complete makefile for a multi-module assembly
ASSEMBLY= MySharedAssembly.dll BIN=.in SRC=. DEST=.in CSC=csc /nologo /debug+ /d:DEBUG /d:TRACE MODULETARGET=/t:module LIBTARGET=/t:library EXETARGET=/t:exe REFERENCES=System.dll MODULES=$(DEST)Fraction.dll $(DEST)Calc.dll METADATA=$(SRC)AssemblyInfo.cs all: $(DEST)MySharedAssembly.dll # Assembly metadata placed in same module as manifest $(DEST)$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST) $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s # Add Calc.dll module to this dependency list $(DEST)Calc.dll: Calc.cs $(DEST) $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s # Add Fraction $(DEST)Fraction.dll: Fraction.cs $(DEST) $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s $(DEST):: !if !EXISTS($(DEST)) mkdir $(DEST) !endif
The makefile
begins by defining the assembly you
want to build:
ASSEMBLY= MySharedAssembly.dll
It then defines the directories you’ll use, putting the output
in a bin
directory beneath the current directory
and retrieving the source code from the current directory:
BIN=.in SRC=. DEST=.in
You build the assembly as follows:
$(DEST)$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST) $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s
This places the assembly (MySharedAssembly.dll)
in the destination directory (bin
). It tells
nmake
(the program that executes the
makefile
) that the assembly consists of the
metadata and the modules, and it provides the command line required
to build the assembly.
The metadata is defined earlier as:
METADATA=$(SRC)AssemblyInfo.cs
The modules are defined as the two DLLs:
MODULES=$(DEST)Fraction.dll $(DEST)Calc.dll
The compile line builds the library and adds the modules, putting the output into the assembly file MySharedAssembly.dll:
$(DEST)$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST) $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s
To accomplish this, nmake
needs to know how to
make the modules. You start by telling nmake
how
to create calc.dll. For this you need the
calc.cs source file, and you tell
nmake
the command line to build that DLL:
$(DEST)Calc.dll: Calc.cs $(DEST) $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s
Then do the same thing for fraction.dll:
$(DEST)Fraction.dll: Fraction.cs $(DEST) $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s
The result of running nmake
on this
make
file is to create three DLLs:
fraction.dll
,
calc.dll, and
MySharedAssembly.dll. If you open
MySharedAssembly.dll with
ILDasm
, you’ll find that it consists of
nothing but a manifest, as shown in Figure 17-3.
If you examine the manifest, you see the metadata for the libraries you created, as shown in Figure 17-4.
You first see an external assembly for the core library
(mscorlib
), followed by the two modules,
ProgCS.Fraction
and
ProgCS.myCalc
.
You now have an assembly that consists of three DLL files: MySharedAssembly.dll with the manifest, and Calc.dll and Fraction.dll with the types and implementation needed.
To use these modules, you need to create a driver program that will load in the modules as needed. Example 17-4 illustrates. Save this program as Test.cs in the same directory as the other modules.
Example 17-4. A module test driver
namespace Programming_CSharp { using System; public class Test { // main will not load the shared assembly static void Main( ) { Test t = new Test( ); t.UseCS( ); t.UseFraction( ); } // calling this loads the myCalc assembly // and the mySharedAssembly assembly as well public void UseCS( ) { ProgCS.myCalc calc = new ProgCS.myCalc( ); Console.WriteLine("3+5 = {0} 3*5 = {1}", calc.Add(3,5), calc.Mult(3,5)); } // calling this adds the Fraction assembly public void UseFraction( ) { ProgCS.Fraction frac1 = new ProgCS.Fraction(3,5); ProgCS.Fraction frac2 = new ProgCS.Fraction(1,5); ProgCS.Fraction frac3 = frac1.Add(frac2); Console.WriteLine("{0} + {1} = {2}", frac1, frac2, frac3); } } } Output: 3+5 = 8 3*5 = 15 3/5 + 1/5 = 4/5
For the purposes of this demonstration, it is important not to put
any code in Main( )
that depends on your modules.
You do not want the modules loaded when Main( )
loads, and so no Fraction
or
Calc
objects are placed in Main( )
. When you call into UseFraction
and
UseCalc
, you’ll be able to see that the
modules are individually loaded.
An assembly is loaded into its application by the
AssemblyResolver
through
a process called
probing
. The assembly
resolver is called by the .NET Framework automatically; you do not
call it explicitly. Its job is to resolve the assembly name to an EXE
program and load your program.
With a private assembly, the AssemblyResolver
looks only in the application load directory and its
subdirectories—that is, the directory in which you invoked your
application.
The three DLLs produced earlier must be in the directory in which Example 17-4 executes or in a subdirectory of that directory.
Put a break point on the second line in Main
, as
shown in Figure 17-5.
Execute to the break point and open the Modules window. Only two modules are loaded, as shown in Figure 17-6.
Step into the first method call and watch the modules window. As soon
as you step into UseCS
, the
AssemblyLoader
recognizes that it needs an
assembly from MySharedAssembly.Dll. The DLL is
loaded, and from that assembly’s manifest the
AssemblyLoader
finds that it needs
Calc.dll, which is loaded as well, as shown in
Figure 17-7.
When you step into Fraction
, the final DLL will be
loaded. The advantage of multi-module assemblies is that a module is
loaded only
when it is needed.
18.118.138.195