Когда требуется объединить несколько управляемых сборок в одном файле, можно воспользоваться утилитой ILMerge:
В данном примере из двух сборок будет создан объединённый исполняемый файл. Атрибут /t:winexe указывает на то, что результатом будет оконное (WinForms) приложение.
Однако, с приложениями WPF утилита ILMerge работать не умеет. Это связано с особенностями компиляции XAML-файлов, использующихся в архитектуре WPF для декларативного описания структуры, поведения и анимации пользовательского интерфейса:
При объединении подобных сборок ILMerge следовало бы исправить все URI доступа к BAML-ресурсам, однако этого не происходит.
К счастью есть другой способ: разместить файлы сборок внутри объединённой в качестве встроенных ресурсов.
Для того, чтобы размещение происходило автоматически, достаточно изменить файл проекта, добавив к нему следующую цель сразу после импорта файла Microsoft.CSharp.targets:
Выполнение данной цели происходит сразу же после завершения задачи ResolveAssemblyReference. В результате выполнения указанной цели связанные сборки упаковываются в виде встроенных ресурсов. Каждый ресурс именуется согласно относительному пути и имени файла сборки.
Использование пути до сборки в именовании встроенных ресурсов является более гибким подходом и позволяет, к примеру, встраивать сборку вместе с локализациями, которые, как правило, имеют то же имя.
После того, как сборки упакованы в едином файле, необходимо загрузить все сборки в память до того, как будут инициализированы ядро и программная инфраструктура WPF.
«Волшебной» точкой входа в приложение WPF является файл App.xaml. Однако, на самом деле App.xaml компонуется в файл App.g.cs, который можно найти внутри проекта в каталоге obj. В файле App.g.cs находится стандартная точка входа — статитческий метод Main.
Таким образом, для того, чтобы загрузить вложенные сборки до инициализации ядра WPF необходимо переопределить стандартную точку входа.
Например вот так:

Последний шаг — собственный обработчик события AppDomain.AssemblyResolve, которое срабатывает каждый раз когда стандартный загрузчик не может определить физическое расположение очередной сборки.
Представленный способ прост в реализации и предназначен для автоматической сборки управляемых модулей любого .NET приложения в одном исполняемом файле. Однако с точки зрения практической ценности, очевидно, что данное решение подходходит лишь для сборки небольших проектов. При этом неуправляемый код, очевидно, остаётся «за бортом» объединённого файла приложения.
1. Построение приложения WPF | msdn.microsoft.com/ru-ru/library/aa970678.aspx
2. MSBuild | msdn.microsoft.com/ru-ru/library/ms171452.aspx
3. Влад Чистяков: MSBuild | www.rsdn.ru/article/devtools/msbuild-05.xml
4. Задача ResolveAssemblyReference | msdn.microsoft.com/ru-ru/library/9ad3f294.aspx
5. Расширение процесса построения Visual Studio | msdn.microsoft.com/ru-ru/library/ms366724.aspx
6. Разрешение загрузки сборок | msdn.microsoft.com/ru-ru/library/ff527268.aspx2
7. C# + WPF + сторонние сборки -> один .exe-шник | habrahabr.ru/blogs/personal/67836
В июне 2012 года вышел в свет opensource-плагин Costura для Visual Studio 2010, позволяющий свести описанный выше процесс к управлению настройками проекта.
ILMerge.exe /t:winexe /out:test.exe test1.exe test2.dllВ данном примере из двух сборок будет создан объединённый исполняемый файл. Атрибут /t:winexe указывает на то, что результатом будет оконное (WinForms) приложение.
Однако, с приложениями WPF утилита ILMerge работать не умеет. Это связано с особенностями компиляции XAML-файлов, использующихся в архитектуре WPF для декларативного описания структуры, поведения и анимации пользовательского интерфейса:
- XAML-файл компилируется в BAML-код (аналог IL), который затем размещается в ресурсах сборки.
- С помощью объявлений пространства имён XML XAML-файл может ссылаться на другие сборки.
- XAML-файлы могут ссылаться друг на друга используя, к примеру, такие элементы, как объединённые словари ресурсов и фреймы.
При объединении подобных сборок ILMerge следовало бы исправить все URI доступа к BAML-ресурсам, однако этого не происходит.
К счастью есть другой способ: разместить файлы сборок внутри объединённой в качестве встроенных ресурсов.
Для того, чтобы размещение происходило автоматически, достаточно изменить файл проекта, добавив к нему следующую цель сразу после импорта файла Microsoft.CSharp.targets:
<Target Name="AfterResolveReferences"> <ItemGroup> <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'"> <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName> </EmbeddedResource> </ItemGroup> </Target>
Выполнение данной цели происходит сразу же после завершения задачи ResolveAssemblyReference. В результате выполнения указанной цели связанные сборки упаковываются в виде встроенных ресурсов. Каждый ресурс именуется согласно относительному пути и имени файла сборки.
Использование пути до сборки в именовании встроенных ресурсов является более гибким подходом и позволяет, к примеру, встраивать сборку вместе с локализациями, которые, как правило, имеют то же имя.
После того, как сборки упакованы в едином файле, необходимо загрузить все сборки в память до того, как будут инициализированы ядро и программная инфраструктура WPF.
«Волшебной» точкой входа в приложение WPF является файл App.xaml. Однако, на самом деле App.xaml компонуется в файл App.g.cs, который можно найти внутри проекта в каталоге obj. В файле App.g.cs находится стандартная точка входа — статитческий метод Main.
Таким образом, для того, чтобы загрузить вложенные сборки до инициализации ядра WPF необходимо переопределить стандартную точку входа.
Например вот так:
public class Program { [STAThread] public static void Main() { App.Main(); } }

Последний шаг — собственный обработчик события AppDomain.AssemblyResolve, которое срабатывает каждый раз когда стандартный загрузчик не может определить физическое расположение очередной сборки.
public class Program { static Dictionary<string, Assembly> assembliesDictionary = new Dictionary<string, Assembly>(); [STAThread] public static void Main() { AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly; App.Main(); } private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args) { AssemblyName assemblyName = new AssemblyName(args.Name); Assembly executingAssembly = Assembly.GetExecutingAssembly(); string path = string.Format("{0}.dll", assemblyName.Name); if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false) { path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path); } if (!assembliesDictionary.ContainsKey(path)) { using (Stream assemblyStream = executingAssembly.GetManifestResourceStream(path)) { if (assemblyStream != null) { var assemblyRawBytes = new byte[assemblyStream.Length]; assemblyStream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length); using (var pdbStream = executingAssembly.GetManifestResourceStream(Path.ChangeExtension(path, "pdb"))) { if (pdbStream != null) { var pdbData = new Byte[pdbStream.Length]; pdbStream.Read(pdbData, 0, pdbData.Length); var assembly = Assembly.Load(assemblyRawBytes, pdbData); assembliesDictionary.Add(path, assembly); return assembly; } } assembliesDictionary.Add(path, Assembly.Load(assemblyRawBytes)); } else { assembliesDictionary.Add(path, null); } } } return assembliesDictionary[path]; } }
Резюме
Представленный способ прост в реализации и предназначен для автоматической сборки управляемых модулей любого .NET приложения в одном исполняемом файле. Однако с точки зрения практической ценности, очевидно, что данное решение подходходит лишь для сборки небольших проектов. При этом неуправляемый код, очевидно, остаётся «за бортом» объединённого файла приложения.
Список литературы:
1. Построение приложения WPF | msdn.microsoft.com/ru-ru/library/aa970678.aspx
2. MSBuild | msdn.microsoft.com/ru-ru/library/ms171452.aspx
3. Влад Чистяков: MSBuild | www.rsdn.ru/article/devtools/msbuild-05.xml
4. Задача ResolveAssemblyReference | msdn.microsoft.com/ru-ru/library/9ad3f294.aspx
5. Расширение процесса построения Visual Studio | msdn.microsoft.com/ru-ru/library/ms366724.aspx
6. Разрешение загрузки сборок | msdn.microsoft.com/ru-ru/library/ff527268.aspx2
7. C# + WPF + сторонние сборки -> один .exe-шник | habrahabr.ru/blogs/personal/67836
Послесловие
В июне 2012 года вышел в свет opensource-плагин Costura для Visual Studio 2010, позволяющий свести описанный выше процесс к управлению настройками проекта.
