How to Create Single-File .NET Applications

April 05, 2017

Have you ever been working on a remote computer and suddenly found your favorite tool (notepad++, 7-zip, etc.) was not installed? It can be a real bummer to stop what you're doing in order to find and install that program, assuming that you even have the rights or permission to do so. Tools like this feel like they should be very portable, perhaps just a single executable file that you can copy from a flash drive or cloud directory and double-click to run, but this is not always the case. It is often surprising to me how many of these tools expect you to download and run an MSI or other installer, which seems overkill if you are only going to use it once on that computer.

While working on a database health-monitoring tool for internal use at Sparkhound, it occurred to me that we would often need to run it in these types of scenarios. It would be great if we could package the tool into a single executable to be copied and run directly. However, the default .NET build produces a large output directory with all of the dependent libraries as separate files, and as far as I could tell there was no automatic option for bundling theses files together. I heard about a tool called ILMerge which merges .NET assemblies together into a single file, which on the surface sounded like exactly what I needed. However, upon further research I found that this would not work for my graphical WPF app which is reliant upon hard-coded assembly references in XAML.

Luckily, I found this article by Daniel Chambers which presents a solution. The idea is to bundle all of your dependent assemblies as embedded resources. Then at run-time make sure the app can load the assemblies by providing a callback function to load the assemblies into memory manually. 

Let's go into detail on that first step. While you can embed the assemblies manually one-by-one, this can become rather tedious, and it is easy to forget to add new assemblies to the list. It is much better to have your build system do this for you. Daniel's technique was to modify the project file to include a custom task which is called after the referenced assemblies are resolved. By naming the task "AfterResolveReferences" you can hook into msbuild at just the right time to bind an EmbeddedResource entry against the assemblies which will be copied to the output directory:

<!-- http://www.digitallycreated.net/Blog/61/combining-multiple-assemblies-into-a-single-exe-for-a-wpf-application -->
  <Target Name="AfterResolveReferences">
    <ItemGroup>
      <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" 
          Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
        <LogicalName>
%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)
        </LogicalName>
      </EmbeddedResource>
    </ItemGroup>
  </Target>

Now that you have the assemblies as embedded resources, the next step is load them at run-time. Daniel's recommendation here is to tie into an event provided by the AppDomain class: AssemblyResolve. This event will be called when the AppDomain is unable to resolve an assembly reference (which will always be true if you don't have the assemblies available locally or in the GAC). Simply provide a callback function and load the assembly yourself using the Assembly.Load(Stream stream) method.

public class Program {
  private static readonly Object _lock = new object();
  private static Assembly MainAssembly { get; set; }

  [STAThread]
  public static void Main() {
    MainAssembly = Assembly.GetExecutingAssembly();
    AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
    App.Main();
  }

  private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args) {
    // Get assembly path
    AssemblyName assemblyName = new AssemblyName(args.Name);
    string path = $"{assemblyName.Name}.dll";
    if ((assemblyName.CultureInfo != null) && !assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture)) {
      path = $@"{assemblyName.CultureInfo}\{path}";
    }
    var assembly = LoadAssemblyFromEmbeddedResources(path);
    return assembly;
  }

  private static Assembly LoadAssemblyFromEmbeddedResources(string path) {
    lock (_lock) {
      using (Stream stream = MainAssembly.GetManifestResourceStream(path)) {
        if (stream == null) { return null; /* quit early */ }

        byte[] assemblyRawBytes = new byte[stream.Length];
        stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
        var assembly = Assembly.Load(assemblyRawBytes);
        return assembly;
      }
    }
  }
}

With the msbuild task and runtime callback in place, I fired up the app and had success! Well, almost. I actually noticed that some of my XAML styles were not being applied correctly. Looking at the debug messages in the Output window in Visual Studio, I noticed some lines saying that the XAML was unable to bind correctly because two variables with the same type name did not have matching types. Aha! That gave me the clue that perhaps I was loading the assemblies multiple times, which was indeed the case. As far as I can tell, the XAML engine runs in its own AppDomain or at least has a different list of loaded assemblies, so the callback was being called multiple times. The fix is to keep a reference to the loaded assemblies and reuse them if the app asks for the same assembly again:

public class Program {
  protected static ConcurrentDictionary<string, Assembly> LoadedAssemblies { get; } = new ConcurrentDictionary<string, Assembly>();

  // ...

  private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args) {
    
    // ...

    // Get cached assembly or load a new one.
    var assembly = LoadedAssemblies.GetOrAdd(path, LoadAssemblyFromEmbeddedResources);
    return assembly;
  } 
}

Note that I stored the assemblies in a ConcurrentDictionary because I wasn't sure if different threads might be attempting to resolve assemblies at the same time, but this may not be an actual problem.

Looking Towards the Future

Microsoft is currently in the process of producing a platform-independent version of .NET called .NET Core. Given the focus on portability, I was curious if it was possible to perform similar steps to produce a single executable. Combined with the ability of .NET Core to produce self-contained deployments (SCD) where the .NET Core runtime and libraries are copied alongside the application, this seems like a recipe for ultimate portability. 

However, my first attempt at simply copying the logic for the msbuild task failed to embed the resources. Creating a resources file and adding the assemblies manually also met with failure, as the ResourceManager class no longer had a GetObject method.

Error Resource Manager Get Object

After turning up the verbosity level of msbuild, I determined that the "AfterResolveReferences" target was being fired, but the resolved assemblies list was blank because the .NET Core output directory structure has changed. I did some digging into the Microsoft.NET.Publish.targets file (located at C:\Program Files\dotnet\sdk\1.0.1\Sdks\Microsoft.NET.Sdk\build\) and found the RunResolvePublishAssemblies target which lists the assemblies to be copied to the publish directory:

<Target Name="EmbedAllAssembliesAsResources" 
      AfterTargets="AfterResolveReferences" 
      DependsOnTargets="RunResolvePublishAssemblies">
    <Message Text="Gathering resolved assemblies to publish..." />
    <Message Text="@(ResolvedAssembliesToPublish)" />
    <Message Text="Embedding resolved assemblies..." />
    <ItemGroup>
      <EmbeddedResource Include="@(ResolvedAssembliesToPublish)" Condition="'%(ResolvedAssembliesToPublish.Extension)' == '.dll'">
        <LogicalName>
%(ResolvedAssembliesToPublish.DestinationSubDirectory)%(ResolvedAssembliesToPublish.Filename)%(ResolvedAssembliesToPublish.Extension)
        </LogicalName>
      </EmbeddedResource>
    </ItemGroup>
  </Target>

Using this Target, I was able to embed the assemblies correctly for both Framework-dependent Deployments and Self-contained Deployments. However, I soon found that the Assembly.Load(Stream stream) method does not yet exist in .NET Core. I looked around for an alternative and seemed to find one in the AssemblyLoadContext.Default.LoadFromStream(Stream stream) method. 

private static Assembly LoadAssemblyFromEmbeddedResources(AssemblyLoadContext assemblyLoadContext, string path) {
  lock (_lock) {
    using (Stream stream = MainAssembly.GetManifestResourceStream(path)) {
      if (stream == null) { return null; /* quit early */ }                    

      var assembly = AssemblyLoadContext.Default.LoadFromStream(stream);
      return assembly;
    }
  }
}

However, when I attempt to run this code, I get a FileLoadException:

System.IO.FileLoadException: 'Could not load file or assembly 'Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed'.'

Weird, considering that I am attempting to load from a stream and not from a file... It could be that I misunderstand the usage of the method, but it seems to be a bug. I could write the bytes out to a temporary file and load that, but I would rather keep looking for a different solution. The upcoming .NET Standard 2.0  is supposed to add support for even more missing library APIs, so it is possible that we may see the return of Assembly.Load(Stream stream). I also read that about a new version of the .NET runtime called CoreRT which may be paired with .NET Native compilers to produce a single executable, but it is still under active development and not quite ready for prime time.

I plan to update this post if I find out any new information on this matter, so be sure to check back. In the meantime, feel free to keep reading about the latest web development trends and how Sparkhound can help you achieve your goals at the Sparkhound blog.

Until next time, have fun and happy coding!

Information and material in our blog posts are provided "as is" with no warranties either expressed or implied. Each post is an individual expression of our Sparkies. Should you identify any such content that is harmful, malicious, sensitive or unnecessary, please contact marketing@sparkhound.com.

Meet Sparkhound

Review our capabilities and services, meet the leadership team, see our valued partnerships, and read about the hardware we've earned.

Learn How We Work

See how our Plan/Build/Run methodology drives real client success, and gain our team's perspectives on timely tech topics.

Engage With Us

Get in touch any of our offices, or checkout our open career positions and consider joining Sparkhound's dynamic team.