Как правило, при написании .NET программ используются не только классы из .NET BCL, но и сторонние библиотеки. Во время выполнения программы все используемые библиотеки должны быть найдены. Для этого зависимые dll кладут в одну папку с exe файлом.

Однако существуют программы, использующие сторонние библиотеки, но при этом состоящие из одного единственного файла. Все утилиты от SysInternals, а также любимый мной LINQPad представляют из себя один файл в котором содержится все, что требуется для работы. Пользоваться такими утилитами одно удовольствие — они сразу готовы к использованию, их удобно передавать и хранить.

В статье рассказывается, как создавать такие автономные программы из одного файла. Разобран пример как со сжатием зашить библиотеку AutoMapper в программу и как ее потом достать и использовать.


Код к статье


Исходный код к статье — скачать

Код программы использует стороннюю библиотеку AutoMapper. Чтобы убедиться в работоспособности библиотеки после ее зашития в ресурсы, в программе вызывается код из семплов к библиотеке. Этот код здесь не приведен, ибо это статья не об AutoMapper. Но сама библиотека интересная и полезная — рекомендую посмотреть, что же она делает в коде.


Подход


Чтобы .NET код мог работать, в использующий его AppDomain нужно загрузить сборку, содержащую типы, которые используются в коде. Если тип находится в сборке, которая еще не была загружена в AppDomain, CLR производит поиск этой сборки по ее полному имени. Поиск происходит в нескольких местах, где именно — зависит от настроек AppDomain. Для настольных приложений это обычно GAC и текущая папка.

Если CLR не удалось найти сборку, вызывается событие AppDomain.AssemblyResolve. Событие дает возможность загрузить требуемую сборку вручную. Поэтому для реализации автономной программы, состоящей из одного exe файл��, достаточно зашить все зависимые сборки в ресурсы и в обработчике AssemblyResolve подгружать их.

Подробности про механизм нахождения сборок можно найти в книге Essential .NET, Volume 1: The Common Language Runtime (Don Box, Chris Sells) — 8 глава, AppDomains and the Assembly Resolver секция.


Архивация сборок


Удобно класть в ресурсы сборки в архивированном виде. Архивация уменьшает размер итоговой программы приблизительно в 2 раза. Скорость запуска увеличивается, но эти доли секунды мало кто заметит. А вот уменьшение размера файла позволит его быстрее качать по сети.

В .NET библиотеке есть архивирующий поток — DeflateStream. По сути он производит zip архивацию, только с патентами не связан. Сжатие сборки зависимой библиотеки происходит следующим образом:
var fileInputName = @"OneExeProgram\Libraries\AutoMapper 1.0 RTW\AutoMapper.dll";
var assembly = File.ReadAllBytes( fileInputName );

var fileOutputName = @"OneExeProgram\Libraries\AutoMapper 1.0 RTW\AutoMapper.dll.deflated";
using( var file = File.Open( fileOutputName, FileMode.Create ) )
using( var stream = new DeflateStream( file, CompressionMode.Compress ) )
using( var writer = new BinaryWriter( stream ) )
{
    writer.Write( assembly );
}

Использование сборки из ресурсов


Итак, у нас есть работающий проект, использующий сторонние библиотеки. Хочется, чтобы exe файл проекта был автономен и не требовал наличия зависимых dll в своем каталоге.

Полученную ранее архивированную сборку добавляем в ресурсы проекта через Project Properties-Resources-Files. Студия при добавлении ресурса генерирует код, который позволяет использовать добавленный ресурс через Resources класс.

Регистрируем обработчик AssemblyResolve (до использования классов зависимой библиотеки):
AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve;
Код обработчика:
private static Assembly AppDomain_AssemblyResolve( object sender, ResolveEventArgs args )
{
    if( args.Name.Contains( "AutoMapper" ) )
    {
        Console.WriteLine( "Resolving assembly: {0}", args.Name );

        // Загрузка запакованной сборки из ресурсов, ее распаковка и подстановка
        using( var resource = new MemoryStream( Resources.AutoMapper_dll ) )
        using( var deflated = new DeflateStream( resource, CompressionMode.Decompress ) )
        using( var reader = new BinaryReader( deflated ) )
        {
            var one_megabyte = 1024 * 1024;
            var buffer = reader.ReadBytes( one_megabyte );
            return Assembly.Load( buffer );
        }
    }

    return null;
}
Редкая сборка занимает более мегабайта, поэтому чтение должно произойти за один раз. По хорошему надо читать в буфер с размером равным размеру содержимого потока, но DeflateStream не поддерживает Length свойство.

Для нескольких зависимых библиотек стоит ввести соглашение, согласно которому имя ресурса и искомой сборки совпадают. Тогда можно с помощью отражения автоматически искать требуемую библиотеку в ресурсах.

По умолчанию зависимые библиотеки, добавляемые через References, копируются в выходную директорию проекта. Чтобы AssemblyResolve сработал, нужно либо скопировать выходной exe файл в другую директорию, либо запретить копировать зависимые библиотеки в конечную директорию через References-AutoMapper-Properties-Copy Local=false.


Заключение


Включение зависимых сборок в ресурсы самой программы позволяет ей работать автономно. Для запуска требуется один лишь exe файл. Это важно для служебных утилит, сразу готовых к использованию.

Фактически такие автономные программы не требуют установки и их удобно передавать по сети или хранить на флешке. Архивирование сборок позволяет уменьшить размер программы и больше таких программ разместить на флешке / быстрее выкачивать из сети.