👋

exe に DLL もまとめたい

に公開

ILMerge を使う方法が一般的ですが、もっと簡単な方法があったので共有します。
(ニッチな情報ですが、時々必要になるはず……)

環境

Visual Studio 2019
Windows フォームアプリ(.Net Framework 4.8)

csproj にタグを追加

プロジェクトを開いている場合、一旦閉じてください。
その後、テキストエディターで csproj を直接変更します。

  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

csproj の一番下はこのようになっていますが、最後のコメントタグの後、</Project>の1行上に、次のタグを追加します。

  <Target Name="AfterResolveReferences">
    <ItemGroup>
      <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
        <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
      </EmbeddedResource>
    </ItemGroup>
  </Target>

その後、ビルドすると exe のサイズが DLL 分増えていることがわかります。

Program.cs を変更する

このままだとアプリは起動しませんので、起動してくれるように書き換えます。
元の Program.cs がこうだった場合、

/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Application.Run(new Form1());
}

以下のように書き換えます。

static readonly Dictionary<string, Assembly> Cache = new Dictionary<string, Assembly>();

[STAThread]
public static void Main()
{
    AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
    OriginalMain();
}

private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
{
    var assemblyName = new AssemblyName(args.Name);
    if (Cache.TryGetValue(assemblyName.Name, out var asm)) return asm;

    var executingAssembly = Assembly.GetExecutingAssembly();

    string path = $"{assemblyName.Name}.dll";
    if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
    {
        path = $"{assemblyName.CultureInfo}\\{path}";
    }

    using (var stream = executingAssembly.GetManifestResourceStream(path))
    {
        if (stream == null) return null;

        byte[] assemblyRawBytes = new byte[stream.Length];
        stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);

        asm = Assembly.Load(assemblyRawBytes);
        Cache[assemblyName.Name] = asm;

        return asm;
    }
}

// 元の Main()
private static void OriginalMain()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Application.Run(new Form1());
}

これでビルドされた exe は、単体で実行できるようになります。

どうしてもひとまとめに出来ない DLL の場合

この方法で、読み込めない DLL があった場合、「DLL を exe とは別のフォルダーにまとめる」という手段も記しておきます。

ツールが MyTools.exe の場合、同じ階層に MyTools.exe.config というファイルを作成し、その中にこう記述します。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <!-- exe から見た相対パスで OK -->
      <probing privatePath="dll" />
    </assemblyBinding>
  </runtime>
</configuration>

これで、dll/ の中に exe が参照している DLL をいれることができます。

Discussion