Assembly Loading: Combine Assemblies & Executables Using ilMerge [2 of 3]

Combine all libraries into a single executable or library to simplify deployment (Merging):

Assembly Loading (Example Application)

If you're like me, I get a tickle when I download an application only to realize that what I just downloaded isn't an installer, but a fully portable executable wrapped up in a single file (ProcessExplorer comes to mind). If your goal is to simply consolidate all your application resources into a single library or executable, then dynamic runtime loading isn't required. The first tool I generally reach for is ilMerge (http://research.microsoft.com/en-us/people/mbarnett/ilmerge.aspx).

A bit about ilMerge: it's lightweight, fast, can quickly be incorporated into build scripts/msbuild, its output is reliable. It takes multiple assemblies, with a single primary assembly/executable (the primary is always the first in the argument list), and outputs a single, merged assembly or executable. Optionally, you can provide a strong name (my example below is signed). Its appropriately named, as it merges assemblies and resources into a single file.

From the command line ('\bin\Release\' folder, from part 1):

mkdir Merged
"..\..\ILMerge\ILMerge.exe" /keyfile:"..\..\Public.Process.snk" "Public.Process.exe" "Public.Dependency.dll" "Public.SubDependency.dll" /out:"Merged\Public.Merged.exe"
Output Folder in 'bin\Release\Merged':

merged

Public.Process.exe, Public.Dependency.dll,  Public.SubDependency.dll assemblies are merged into a single exe (with symbols being output [*.pdb]); From ilSpy:

merged_ilspy

Alternatively, you can selectively include only the files you want merged by excluding them from the argument list (the next example deliberately excludes Public.SubDependency.dll). In some cases, dependencies might exist outside the same path you're executing ilMerge from. If our library Public.SubDependency.dll was in another directory (outside the build output folder) when executing the ilMerge command, you can use the /lib:[path to Public.SubDependency.dll] switch to tell the ilMerge dependency checker to reference that file location when merging (you can also point it to the GAC).

From the command line ('\bin\Release\' folder):

mkdir Missing
ilMerge command, excluding Public.SubDependency.dll:
"..\..\ILMerge\ILMerge.exe" /keyfile:"..\..\Public.Process.snk" "Public.Process.exe" "Public.Dependency.dll" /out:"Missing\Public.Merged.exe"
Only Public.Process.exe, Public.Dependency.dll were merged; Public.SubDependency.dll was excluded. From ilSpy:

Public.Merged missing dependency

Since we didn't supply Public.SubDependency.dll (and since its being called in our entry point [Main]), the application fails to load since the assembly doesn't exist in the probing path ('bin\Release\Missing' subdirectory).

Output in 'bin\Release\Missing' directory:

missing_folder

error_missing_assembly

While the error is expected since we deliberately left out Public.SubDependency.dll assembly — assembly loading errors can commonly be the cause of long evenings!

Fuslogvw.exe (Assembly Binding Log Viewer) allows you to watch the binding as it happens. From the visual studio command line (run as administrator), run 'fuslogvw.exe'… (Set the HKLM\Software\Microsoft\Fusion\ForceLog registry value to 1 to enable logging).

Fusion (fuslogvw.exe) output with a failed bind for Public.SubDependency.dll:

fuslogvw_missing_subdependency

Fusion's log offers some insight, you can see the order in which it was firing off dependency binds. First, the executable (file://…Public.Process.exe), mscorlib (the .NET runtime — this is especially handy to know what runtime is executing your assembly, in this case it's v4), and lastly the Public.SubDependency.dll bind. Double clicking any one of the entries gives you the actual binding steps.

The output from a missing Public.SubDependency.dll, from Fuslogvw.exe:

fuslog_failed_output

As expected, it could not load Public.SubDependency.dll. If you offer a .config file with your application, the fusion log will include the probing paths that that are loaded in the AppDomain. If the probing path doesn't have the assembly its looking for, the log will make a mention of all the paths it attempted to load from (and missed). Fuslog can help resolve a lot of headaches quickly, as it tells you with specifics, where things are being loaded at bind time.

In general, the probe path starts with the GAC first, then any private paths added to the AppDomain (by default, the current *.exe path is included; additional probe paths can be supplied in the app config file if needed).  To get the executable to run without failing, we need to put Public.SubDependency.dll in a valid probing path (next to the exe, or in the GAC), easiest way to do that is to copy it to the output directory (from '\bin\Release').

Copy Public.SubDependency.* to the executable folder '\bin\Release\Missing\':
robocopy "." "./Missing" Public.SubDependency.*
The '\bin\Release\Missing' output folder with Public.SubDependency.dll in the probe location:

merged_folder_with_subdependency

Once we have Public.SubDependency.dll in a probing location that is included in the AppDomain, the Fusion log output now looks like:

Output with Public.SubDependency.dll in a valid probe location, from Fuslogvw.exe:

fuslog_success_output

If we wanted to verify the assembly bind with ProcessExplorer, we can check what it thinks is loaded post-binding just by selecting the process:

Output from ProcessExplorer.exe, with Public.Process.exe selected:

process_explorer_with_path

Same as Fusion's output, it shows the 'bin\Release\Missing\' folder as the location of Public.SubDependency.dll. The key difference between the two applications though, is fuslogvw.exe logs what happens during the bind, and ProcessExplorer shows the state of the process post-bind. Without the assembly being loaded, ProcessExplorer isn't much help, but at a glance, it can help validate what resources are loaded and from what location(s).

The example project includes an msbuild script to merge all output assemblies and drop them into the output folder for the different use cases demonstrated here. The msbuild steps can be found in the project folder under the 'Build' directory.

MSBuild Target To Output 'Merged' and 'Missing' folders:
  <Target Name="ILMergeTarget">

    <CreateItem Include="$(OutputPath)*.dll;$(OutputPath)*.exe" Exclude="$(OutputPath)*.vshost.exe">
      <Output TaskParameter="Include" ItemName="MergeAssemblies" />
    </CreateItem>
    <CreateItem Include="$(OutputPath)*.dll;$(OutputPath)*.exe" Exclude="$(OutputPath)*.vshost.exe;$(OutputPath)*.SubDependency.dll;">
      <Output TaskParameter="Include" ItemName="MissingAssemblies" />
    </CreateItem>

    <Exec Command="mkdir &quot;$(OutputPath)Merged&quot;" IgnoreExitCode="true" />
    <Exec Command="mkdir &quot;$(OutputPath)Missing&quot;" IgnoreExitCode="true"/>

    <Message Text="Merging: %0D%0A @(MergeAssemblies->'%(Filename)%0D%0A')" Importance="High" />
    <Exec Command="&quot;$(ProjectDir)ILMerge\ILMerge.exe&quot; /allowDup /keyfile:$(ProjectDir)Public.Process.snk /lib:&quot;&quot; /targetplatform:v4 /out:$(OutputPath)Merged\Public.Merged.dll @(MergeAssemblies->'&quot;%(FullPath)&quot;', ' ')" />

    <Message Text="Merging: %0D%0A @(MissingAssemblies->'%(Filename)%0D%0A') (without Public.SubDependency.dll)" Importance="High" />
    <Exec Command="&quot;$(ProjectDir)ILMerge\ILMerge.exe&quot; /allowDup /keyfile:$(ProjectDir)Public.Process.snk /lib:&quot;&quot; /targetplatform:v4 /out:$(OutputPath)Missing\Public.Merged.dll @(MissingAssemblies->'&quot;%(FullPath)&quot;', ' ')" />
    <Message Text="Robocopy Public.SubDependency.* to the 'bin\Release\Missing\' folder..." Importance="High" />
    <Exec Command="robocopy &quot;$(ProjectDir)\bin\$(Configuration)&quot; &quot;$(ProjectDir)\bin\$(Configuration)\Missing&quot; *.SubDependency.* /XO" IgnoreExitCode="true" />

  </Target>

ilMerge is great for the very specific purpose of merging libraries into a single assembly; it offers a quick way to clean up your output folder and simplify deployment. It's still important to know where you're assemblies are being loaded during this step, because the next use case for combining assemblies (dynamic .net assembly loading) is more manual, however has some real advantages (and disadvantages) to using ilMerge.

Next…  Assembly Loading: Dynamic Assembly Loading & Compression

Leave a comment

Your email address will not be published. Required fields are marked *