Assemblies embebidos

Una cosa que echo de menos (de las pocas) de C/C++ cuando programo en .NET es la posibilidad de linkar estáticamente las librerías que utilizo. Es verdad que eso genera ejecutables de mayor tamaño, pero también hace que sean más fáciles de copiar de un sitio a otro.

Cuando tienes una aplicación que depende varias librerías, lo más normal es desplegarla copiando esas librerías junto a tu aplicación (o instalarlas en el GAC si te sientes con fuerzas para ello), pero también hay herramientas como ILMerge que te permiten fusionar varios assemblies en uno solo.

ILMerge es muy potente, pero si quieres evitar tener que lidiar con una herramienta más durante el proceso de compilación, existe otra alternativa (un tanto chapucera, eso sí): usar recursos embebidos para almacenar tus dependencias.

Para usar esta técnica necesitas añadir los assemblies como recursos embebidos del proyecto, más o menos como se muestra en la siguiente imagen:

Embedded Assemblies

En este ejemplo, el assembly AutoMapper.dll lo estamos referenciando normalmente en el proyecto, pero además lo estamos añadiendo como recurso embebido.

El «truco» se basa en aprovechar el evento AppDomain.AssemblyResolve para interceptar la carga de assemblies y, en lugar de cargarlos desde disco, cargarlos desde el recurso embebido. ¿Cómo hacemos eso? Fácil:

public static class Program
{
	static Program()
	{
		AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
		{
			var resourceName = "Sample.libs." + new AssemblyName(args.Name).Name + ".dll";

			using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
			{
				var assemblyData = new Byte[stream.Length];
				stream.Read(assemblyData, 0, assemblyData.Length);
				return Assembly.Load(assemblyData);
			}
		};
	}
	
	public static void Main(string[] args)
	{
		// ...
	}
}

No tiene mucha complicación. Lo único que hay que hacer es engancharse al evento, leer el assembly a partir del recurso embebido y devolverlo.

Es importante tener en cuenta que el manejador de eventos debe asignarse nada más arrancar la aplicación para asegurarnos de que antes de que se intente cargar ningún assembly ya esté configurado nuestro cargador y no se intente leer desde disco. El mejor punto para hacerlo es el constructor estático de la clase que contenga el método Main, como en el ejemplo anterior, porque eso nos asegura que antes incluso de invocar al Main ya se ha configurado nuestro cargador.

No sé si es una técnica muy útil, pero desde luego es una cosa curiosa y no muy conocida. Yo la he usado un par de veces para simplificar el despliegue de aplicaciones de escritorio y me ha funcionado bien, aunque si necesitas un mayor control, siempre puedes recurrir a algo más ortodoxo como ILMerge.

6 comentarios en “Assemblies embebidos

  1. Esto genera un ejecutable que contiene todas las DLL? Y en el programa como especifico las librerías o continuo utilizando using System.Data como siempre? Gracias por tu aporte.

  2. En el ejecutable irán las dlls que pongas como recurso embebido. Para usarlas, lo haces como siempre, con su using.

    De todas formas precisamente System.Data está en el GAC y no te hace falta desplegarla.

  3. Hola Juan!

    Quería hacerte una pregunta sobre tu código ya que lo necesito para una de mis aplicaciones.

    Por lo que vi en lo que estas programando es una Aplicación de Consola ya que posee el void Main. Mi pregunta es como hacer para utilizarla en una Aplicacion de Windows Forms.

    Lo que intente fue lo siguiente:

    Primero coloque una referencia a mi DLL. Despues cree una carpeta en el proyecto llamada Resource que contiene mi DLL (Ionic.Zip.dll), y luego en el evento Form_Load coloque tu codigo realizando los cambios correspondientes.

    Lo que cambie fue:
    var resourceName = «Sample.libs.» + new AssemblyName(args.Name).Name + «.dll»;
    Por:
    var resourceName = «USB KeepOut.Resources.Ionic.Zip.dll»;

    Pero al ejecutar el programa (ya compilado y separado de la DLL) obtengo este error:

    System.IO.FileLoadException: No se puede cargar el archivo o ensamblado ‘Ionic.Zip, Version=1.9.1.8, Culture=neutral, PublicKeyToken=edbe51ad942a3f5c’ ni una de sus dependencias. Puntero no válido (Excepción de HRESULT: 0x80004003 (E_POINTER))

    No sé que podrá ser pero te agradecería mucho si puedes ayudarme.

    Gracias de antemano, Agustín.

  4. Hola Agustín,

    Aunque sea una aplicación WinForms, también tiene que tener su método Main, seguramente en el fichero Program.cs que te genera el Visual Studio. Ten en cuenta que si no hubiese un método Main y la aplicación tuviera varios Forms, ¿cuál se usaría para arrancar? En el Main tendrás una linea del tipo Application.Run(new MyForm()) que indica eso.

    Pero yendo al grano, el método Form_Load se ejecuta muy tarde, cuando ya se ha intentado cargar la DLL. Necesitas añadir el código antes, lo ideal es en el constructor estático de la clase que contiene el método Main o, en su defecto, puedes probar a añadirlo al constructor estático de tu Form, pero no es lo más recomendable.

    Un saludo,

    Juanma.

  5. Hola Juanma,

    Llegue a tu post buscando información de firmado de librerías, y en la lectura que realice veo que al final comentas «La he usado para simplificar el despliegue de aplicaciones de escritorio»?… Actualmente estoy trabajando en algunas aplicaciones de escritorio, y no he visto la necesidad, genero el instalador desde Visual Studio y ya.

    O que otras aplicaciones de usos le puedo dar el tener Assemblies embebidos? si puedes compartirnos tus experiencias.

    Saludos.

  6. Hola Luis,

    Es sobre todo una curiosidad. Es verdad que a la hora del despliegue de aplicaciones sencillas te ahorras tener que crear un instalador y puedes usar xcopy deployment, pero más allá de eso, poco más aporta.

    Incluso eso, de unir assemblies en uno solo, puedes hacerlo con herramientas tipo ILMerge o ilpack.

Comentarios cerrados.