Sunday, March 18, 2012

Creating single EXE that depends on many DLL files in C# using visual studio

Many applications consist of an EXE that depends on many DLL files. When deploying an application all files must be deployed. So carrying all dependent DLL files for an exe during deployment is a problem and you may miss one of the dependent DLL file. To overcome on this problem you can create a single deployable EXE in Visual Studio. In visual studio, we can make configuration of EXE in such a way that, we can load an assembly at runtime that is located in a folder that is not the bin folder of the application. You do not have to put an assembly that an application must use at runtime in the bin folder of the application. You can put the assembly in any folder on the system, and then you can refer to the assembly at runtime. Following procedure explain the same –
The technique uses Appdomain’s “AssemblyResolve” event to achieve it. First, identify all the DLL files that your EXE file depends on. Then add these DLL files in one of the folder in your project. For example, I am using following DLL files in my application which I have added in dll folder in my application –  

Add the reference of these DLL files in the application from the folder. In my case I added reference from “dll” folder. Then in the “dll” folder, select each DLL file and open properties; change “Build Action” to “Embedded Resource”. This causes C# compiler to embed the DLL files into your EXE file and then you can deploy this one EXE file. At runtime, the CLR won’t be able to find the dependent DLL assemblies, which is a problem. The “AssemblyResolve” event fires whenever CLR tries to bind an assembly and fails. The “AssemblyResolve” event handler must return an [Assembly] object.
Also the call to event handler method registration must be the first statement and rest of the logic should be wrapped in another method. In my case, for example, I am generating bitmap image in Main method of my console application. Therefore my code segment in Main method is as follows –
static void Main(string[] args)
        {
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
GenerateImage();//this can be any method containg other work you need to carry out
        }

The code for Assembly Resolve is as follows –
using System.Reflection;

 /// <summary>
        /// Event is raised when CLR is not able to find referenced assemblies (in this case - gheat.dll, GMap.NET.Core.dll, Microsoft.WindowsAzure.storageClient.dll)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        /// <returns> Assembly to be loaded from embeded resource.</returns>
        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {           
            //This handler is called only when the common language runtime tries to bind to the assembly and fails.
            //Retrieve the list of referenced assemblies in an array of AssemblyName.
            Assembly objExecutingAssemblies;           
            Byte[] assemblyData = null;

            objExecutingAssemblies = Assembly.GetExecutingAssembly();
            AssemblyName[] arrReferencedAssmbNames = objExecutingAssemblies.GetReferencedAssemblies();

            //Loop through the array of referenced assembly names.
            foreach (AssemblyName strAssmbName in arrReferencedAssmbNames)
            {
                //Check for the assembly names that have raised the "AssemblyResolve" event.
                if (strAssmbName.FullName.Substring(0, strAssmbName.FullName.IndexOf(",")) == args.Name.Substring(0, args.Name.IndexOf(",")))
                {
                    //Build the path of the assembly from where it has to be loaded.                                               
                   
                    //Console.WriteLine(args.Name.Substring(0, args.Name.IndexOf(",")) + ".dll");
                    //Console.WriteLine("GHeatReducer." + new AssemblyName(args.Name).Name + ".dll");
                   
                    //GHeatReducer.dll. - this name is retrievd from ILDasm tool and by opening GHeatReducer.exe in the ILDasm tool. In ILDASM tool select name rpesent against “.mresource” to write in GetManifestResourceStream method.  GHeatReducer” is the name of namespace and “dll” is the name of folder where DLL files to be referenced is present as shown in above screenshot.
                    var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("GHeatReducer.dll." + new AssemblyName(args.Name).Name + ".dll");

                    assemblyData = new Byte[stream.Length];
                    stream.Read(assemblyData, 0, assemblyData.Length);
                    break;                   
                }               
            }
            //Return the loaded assembly.
            return Assembly.Load(assemblyData);
        }

public static void GenerateImage()
        {
                //code to generate image goes here
        }

Hope this helps.
Cheers…

16 comments:

  1. Finally found what I was looking for :) Thank you very much

    I was trying to use the method here:

    http://blogs.msdn.com/b/microsoft_press/archive/2010/02/03/jeffrey-richter-excerpt-2-from-clr-via-c-third-edition.aspx?PageIndex=1#comments

    The event handler method is slightly cleaner but I was missing one important point.

    I was adding the event handler in the Main() method and executing code in the same.

    Just wrapping the rest of the code in another method solved this for console app.

    Thanks again.

    ReplyDelete
  2. Hi,

    Same thanks as David ;)

    ReplyDelete
  3. I'm having a problem with this.. this is a small console application that solves a ProjectEuler problem. It requires the FSharp.PowerPack DLL.

    I've done exactly as this article suggests. Add the fsharp.powerpack.dll file to the solution, changed the build action to "embedded resource". Added a reference to the dll file "..\ConsoleApplication1\dlls\FSharp.PowerPack.dll"

    Here's what my "Main" function looks like:
    static void Main(string[] args)
    {
    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);

    DoWork();
    }

    Yet, when I copy the ConsoleApplication1.exe from the debug folder (after doing a build), to a computer that doesn't have F# library installed, I get "Unhandled exception: system.io.fileloadexception: could not load file or assembly 'FSharp.PowerPack...." when running it...

    ReplyDelete
    Replies
    1. Hey Wes,
      Assuming, you are using F# code only to load F# dll; as I don't know much about F#, Make sure that you have given correct path from .mresource in the code. Also make sure that, when you build your EXE, does it have size including the size of your DLL?

      Delete
    2. I ended up using the code at the bottom of this forum post:

      http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral
      /thread/13868df7-897f-48f1-81cd-c41b925ac928/

      and it worked like a charm.

      I'm still not sure what I needed to do differently with the code you provided. :(

      Maybe you could d/l the dll and try it?
      http://dl.dropbox.com/u/1586533/FSharp.PowerPack.dll

      do something like this in your code:

      using Microsoft.FSharp.Math;

      BigRational someNumber;
      someNumber = 123456789123456789123456789;

      Delete
  4. How do you do this in Visual Studio 2005. There is no program.cs in the class library project. Where do I write this code.

    I have an exe that depends on DLL_1. DLL_1 depends on several other dlls. I have added all the dlls as resources (embedded resource) in DLL_1. If I am correct the above code should go into DLL_1.

    Thanks for the help,
    Rao

    ReplyDelete
    Replies
    1. Hey Rao, in class library you will not find program.cs as this is for console application. For your scenario, the above code of main method and handler method will go in EXE and not in DLL_1. hope this helps.

      Delete
  5. Hello,

    I am getting the following error when I use the code in Exe.

    Could not load file or assembly 'ServiceAgent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependenciesAn expected resource in the assembly manifest was missing. (Exception from HRESULT: 0x80131532)

    Thanks,
    Rao.

    ReplyDelete
  6. It might be the case that, Service Agent dll is also required to be added in the folder. Also make sure that, you are referring the correct path. Use ILDASM tool to reach to the manifest of serviceagent dll. When you build the EXE, does it have size including the size of serviceagent dll. If not then service agent is not getting added in the EXE. So add and then try. Hope this helps.

    ReplyDelete
  7. Yes. Altogether I added 10 other dlls in DLL_1. And it increased the size after I added each dll.

    What code should go in GenerateImage()

    public static void GenerateImage()
    {
    //code to generate image goes here
    }

    Thanks
    Rao

    ReplyDelete
  8. No code will go generate image method in your case. It is just a place holder method. This method can be any business logic of your choice.
    Not sure of problem you are facing.

    ReplyDelete
  9. Please tell me how to access the functions that are inside the emebedded dll?

    ReplyDelete
  10. Very cool tutorial,

    I have tested the code, and had the initialization inside the static ctor of the console app.
    This eliminates the need to have additional function in Main() if you don't want it.
    Here is the code:

    class Program
    {
    static Program()
    {
    ResolveAssemblies();
    }

    private static void ResolveAssemblies()
    {
    AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
    {
    string resourceName = "Fun.Resources." +
    new AssemblyName(args.Name).Name + ".dll";

    using(var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
    {
    byte[] assemblyData = new byte[stream.Length];
    stream.Read(assemblyData, 0, assemblyData.Length);
    return Assembly.Load(assemblyData);
    }
    };
    }

    static void Main(string[] args)
    {

    if (null == args || args.Length == 0)
    {
    Logger.Info("No command line arguments found!");
    return;
    }

    for (int i = 0; i < args.Length; i++)
    {
    Logger.Debug("arg at [{0}]: {1}", i, args[i]);
    }
    }

    The embeded assembly is just a simple Logger.

    ReplyDelete
  11. Great - this was just what I have been looking for! Thank you very much!

    ReplyDelete
  12. would someone please post a complete application? With an XML resource?

    ReplyDelete