To distribute a C# application as a single executable without external DLL dependencies, you can embed required DLLs as embedded resources and dynamically load them at runtime using the AppDomain.AssemblyResolve event. This approach eliminates the need to ship separate .dll files alongside the .exe, simplifying deployment and reducing file management overhead.
Step-by-Step Implementation
- Embed DLLs as Resources Begin by adding the required DLLs to your project’s resources:
Right-click the project in Solution Explorer → Properties → Resources tab. Select Add Resource → Add Existing File and choose the target DLL(s). In Solution Explorer, locate the added DLL under the Resources folder. Set the DLL’s Build Action to Embedded Resource in the Properties window.
- Prevent Copying DLLs to Output Directory If the DLLs were previously referenced directly:
Remove the direct reference to the DLL from the project’s References. Ensure no copy-local copy exists in the output folder by setting Copy Local to False on any prior references.
- Create the Resource Loader Class Add a new class named EmbeddedDllLoader.cs with the following implementation:
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
namespace MyApp
{
public static class EmbeddedDllLoader
{
private static readonly Dictionary<string, Assembly> LoadedAssemblies = new Dictionary<string, Assembly>();
private static readonly HashSet<string> ProcessedAssemblies = new HashSet<string>();
public static void RegisterEmbeddedDlls(string pattern = "*.dll")
{
var callingAssembly = Assembly.GetCallingAssembly();
if (!ProcessedAssemblies.Add(callingAssembly.FullName))
return;
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
var resourceNames = callingAssembly.GetManifestResourceNames();
var regex = new Regex("^" + Regex.Escape(pattern)
.Replace("\\*", ".*")
.Replace("\\?", ".") + "$", RegexOptions.IgnoreCase);
foreach (var resourceName in resourceNames)
{
if (!regex.IsMatch(resourceName)) continue;
try
{
using (var stream = callingAssembly.GetManifestResourceStream(resourceName))
{
if (stream == null) continue;
var buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
var loadedAssembly = Assembly.Load(buffer);
if (!LoadedAssemblies.ContainsKey(loadedAssembly.FullName))
LoadedAssemblies[loadedAssembly.FullName] = loadedAssembly;
}
}
catch (Exception ex)
{
// Log or handle as needed; avoid UI in non-GUI contexts
System.Diagnostics.Debug.WriteLine($"Failed to load embedded assembly: {resourceName} - {ex.Message}");
}
}
}
private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
{
var requestedName = new AssemblyName(args.Name).FullName;
return LoadedAssemblies.TryGetValue(requestedName, out var assembly) ? assembly : null;
}
}
}
- Initialize Loader in Program Entry Point Call the loader before any application logic begins — typically in Program.Main():
using System;
using System.Windows.Forms;
namespace MyApp
{
static class Program
{
[STAThread]
static void Main()
{
EmbeddedDllLoader.RegisterEmbeddedDlls();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
How It Works When the .NET runtime fails to locate a referenced assembly, it triggers the AssemblyResolve event. The custom handler checks a preloaded dictionary of embedded assemblies and returns the matching one if found. This intercepts the resolution process before the system throws a FileNotFoundException, allowing the application to load DLLs directly from the executable’s embedded resources.
By embedding the DLLs and using this mechanism, the final executable becomes self-contained — no external dependencies are required for deployment.