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;
}