Merging a WPF application into a single EXE

I always dislike handing off little applications to people. Not because I can’t, but because of the steps involved to make sure it all just works. Small apps are the most problematic because I never want to take the time to create a whole installer project for just a few assemblies, and packaging up a zip file must be accompanied by “Unzip this into a folder in your programs directory and create a shortcut…” which brings us back to the whole installer business we started with.

There are a few tools already out there such as ILMerge by Microsoft Research (Which works great for most .NET-y things, but chokes on WPF applications) and a few paid tools by third party vendors that you could fork over a few hundred for to get. But, I’m a developer, which means I want to do it the Hard Way™. I did a little research and found the following blog posts on setting up and merging in DLL’s as resources into the main assembly and then extracting and loading them into memory when you run your application.

Links:

There were a few things I didn’t like about each solution. The first one (richarddingwall.name) ends up having you directly adding the .dll’s as resources directly. I hate maintaining things manually, especially when it will run fine on my machine but break when when I move it somewhere else because I forgot to update the resources when I added a new project. The one from blog.mahop.net builds on the previous one and changes the resolve location to a custom class with its own startup method. Better, because it resolves the resources earlier. Finally, the one from Daniel Chambers (digitallycreated.net) added in the final piece that automatically including the assemblies as resources. Unfortunately, the way he looks for culture specific assemblies didn’t work and I had to remove / change it to be closer to the one on mahop.net.

Final solution I’m currently using is as follows:

To the main executable project, unload and edit the .csproj file, and below the following line:

  1. <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

Add this XML to the project file, save, and load it back up.

  1. <Target Name="AfterResolveReferences">
  2. <ItemGroup>
  3. <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
  4. <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
  5. </EmbeddedResource>
  6. </ItemGroup>
  7. </Target>

It should look something like this when your done:

You’ll then add a new code file to the main project and add the following code to it (modified to fit how your application is named / structured, in a WPF application, a good place to put it would be App.xaml.cs):

  1. [STAThread]
  2. public static void Main()
  3. {
  4. AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
  5.  
  6. App.Main(); // Run WPF startup code.
  7. }
  8.  
  9. private static Assembly OnResolveAssembly(object sender, ResolveEventArgs e)
  10. {
  11. var thisAssembly = Assembly.GetExecutingAssembly();
  12.  
  13. // Get the Name of the AssemblyFile
  14. var assemblyName = new AssemblyName(e.Name);
  15. var dllName = assemblyName.Name + ".dll";
  16.  
  17. // Load from Embedded Resources - This function is not called if the Assembly is already
  18. // in the same folder as the app.
  19. var resources = thisAssembly.GetManifestResourceNames().Where(s => s.EndsWith(dllName));
  20. if (resources.Any())
  21. {
  22.  
  23. // 99% of cases will only have one matching item, but if you don't,
  24. // you will have to change the logic to handle those cases.
  25. var resourceName = resources.First();
  26. using (var stream = thisAssembly.GetManifestResourceStream(resourceName))
  27. {
  28. if (stream == null) return null;
  29. var block = new byte[stream.Length];
  30.  
  31. // Safely try to load the assembly.
  32. try
  33. {
  34. stream.Read(block, 0, block.Length);
  35. return Assembly.Load(block);
  36. }
  37. catch (IOException)
  38. {
  39. return null;
  40. }
  41. catch(BadImageFormatException)
  42. {
  43. return null;
  44. }
  45. }
  46. }
  47.  
  48. // in the case the resource doesn't exist, return null.
  49. return null;
  50. }

Finally, make sure you update the target method for your main application to be the main method for the project you just added:

And, that’s it!

When you build your application you’ll still see all the assemblies in the output directory, but you should be able to take just the executable, move it somewhere else, and run it just as it is.