Thursday, December 23, 2010

C# and Static Linking

When I was writing application that required to parse HTML pages, I selected HtmlAgilityPack to do this. It is excellent library but it's sometimes inconvenient to distribute program with multiple DLLs. So, I decided to link required DLL statically.

The first solving that I found was to merge required assemblies into my own using ILMerge. Spent a little time to find required options I merged DLL into the program:

ILMerge.exe /targetplatform:v4,"C:\Windows\Microsoft.NET\Framework4.0.30319" /out:appout.exe app.exe HtmlAgilityPack.dll

It works ok, but while searching command line options I found another decision by Jeffrey Richter how to get one file for application. Read the comments I took a notice on this one by Jeff:

ILMerge produces a new assembly file from a set of existing assemblies. This means that the original assemblies lose their identity (name, version, culture, and public key).
What I am showing here is creating an assembly that embeds the EXISTING assemblies into it so that they do not lose their identity at all.

So, I decided to use his method. But there was one problem - it didn't work for me :). His code couldn't find the resource. So, I started to search further for its another variations. But I couldn't find what's was wrong.

Nevertheless, I found two ways how to implement Jeff's idea. The first resolution is direct:

AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
    if (new AssemblyName(args.Name).Name == "HtmlAgilityPack")
        return Assembly.Load(Properties.Resources.HtmlAgilityPack);
    return null;
}

And the second is more universal and doesn't require to write "if" for every embedded assembly:

AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
    var requestedAssemblyName = new AssemblyName(args.Name).Name;
    var manifestResourceName = new AssemblyName(Assembly.GetExecutingAssembly().FullName).Name + ".Properties.Resources.resources";
    using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(manifestResourceName))
    {
        using (var reader = new ResourceReader(stream))
        {
            var resource = reader.GetEnumerator();
            while (resource.MoveNext())
            {
                if ((string)resource.Key == requestedAssemblyName)
                    return Assembly.Load((byte[])resource.Value);
            }
        }
    }
    return null;
}

4 comments:

  1. Thanks a lot, it works like a charm!

    ReplyDelete
  2. Thanks for posting this solution. However, this code works if we include our DLL as a resource file, not if we follow the procedure described by Jeffrey Richter. Quoting him:
    "First, identify all the DLL files that your EXE file depends on that do not ship as part of the Microsoft .NET Framework itself. Then add these DLLs to your Visual Studio project. For each DLL file you add, display its properties and change its “Build Action” to “Embedded Resource”"

    I'm working with .Net Framework 4.5. I haven't tried a different target. Neither piece of code nor Jeffrey Richter's work for me. I've managed to adapt Jeffrey's code to my own need, which works and is as follows:

    AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => {
    string resourceName = string.Format("{0}.{1}.dll", Assembly.GetExecutingAssembly().GetName().Name,
    new AssemblyName(args.Name).Name);
    using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) {
    Byte[] assemblyData = new Byte[stream.Length];
    stream.Read(assemblyData, 0, assemblyData.Length);
    return Assembly.Load(assemblyData);
    }
    };

    MS

    ReplyDelete