Когда требуется объединить несколько управляемых сборок в одном файле, можно воспользоваться утилитой 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, позволяющий свести описанный выше процесс к управлению настройками проекта.