👋
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