Nota del autor

Si la entrada que estás leyendo carece de imágenes, no se ve el vídeo que teóricamente lleva incrustado o el código fuente mostrado aparece sin formato, podéis conocer los motivos aquí. Poco a poco iré restableciendo la normalidad en el blog.
Este blog es un archivo de los artículos situados previamente en Lobosoft.es y ha dejado de ser actualizado. Las nuevas entradas pueden encontrarse en www.lobosoft.es. Un saludo,
Lobosoft.

jueves, 27 de agosto de 2009

Evitando fallos de carga dinámica de ensamblados

En ocasiones necesitamos cargar un tipo contenido en una DLL de forma dinámica, sin haber establecido una referencia cuando generamos nuestra aplicación. Es el caso de un sistema de plugins en el que una aplicación debe cargar en tiempo de ejecución una serie de DLLs que le ofrecen una funcionalidad adicional. En .NET disponemos de la poderosa herramienta de la reflexión (reflection) para cargar una DLL dinámicamente e instanciar un objeto de una determinada clase para hacer uso del mismo. En principio su uso es bastante sencillo y no suele darnos demasiados problemas pero ¿qué ocurre cuando una DLL que cargamos dinámicamente hace referencias a otras? Si esas librerías están ubicadas en la ruta de nuestro ejecutable no tendremos mayor problema. Recordemos que el comportamiento establecido por diseño en .NET para localizar una DLL es el siguiente:




  1. Busca en el GAC.

  2. Intenta localizarla en el directorio que contiene el ejecutable.

  3. La busca en un subdirectorio dentro de la ruta del ejecutable, cuando se ha establecido a través de un archivo de configuración.


En el caso en que nuestras DLLs no estén en ninguno de estos lugares, se producirá una excepción al intentar cargar el ensamblado correspondiente.


Para la primera situación, la solución es fácil: basta con instalar en el Global Assembly Cache la DLL en cuestión. Sin embargo, no es algo que convenga hacer con todas nuestras DLLs: el GAC debe mantenerse tan limpio como sea posible.


En el segundo caso, por supuesto, bastaría con tener todas las DLLs en la ruta del ejecutable en cuestión, pero no es algo recomendable ni siempre debe ser así. Comenzaríamos a tener librerías duplicadas en el sistema y las aplicaciones terminarían por ser difíciles de mantener.


Para el tercer caso, la solución pasa por incluir unas etiquetas que especifiquen dónde buscará el CLR nuestro ensamblado. Hay que llevar a cabo una firma del ensamblado para obtener su strong name, usar los atributos publicKeyToken y la versión del ensamblado para terminar incluyendo en nuestro archivo de configuración algo como lo siguiente (se trata de un XML, claro está, aunque sustituyo un carácter para que no se interprete como tal y sea visible):



[configuration>
[runtime>
[assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
[dependentAssembly>
[assemblyIdentity name="Dummy" culture="neutral"
publicKeyToken="589048994a485948"/>
[codeBase version="1.0.1423.13588" 
href="FILE://C:/Lobosoft/Dummy.dll"/>
[/dependentAssembly>
[/assemblyBinding>
[/runtime>
[/configuration>

Pero, ¿qué ocurre si las DLLs pueden estar en distintas ubicaciones, no tenemos posibilidad de firmar digitalmente las mismas, o se da algún otro impedimento para optar por esta solución? En ese caso, tal vez debamos optar por controlar el evento AssembyResolve, asociado al dominio de aplicación actual. Su uso es bastante simple:



AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(AssemblyResolveEventHandler);

Al incluir este par de líneas, en el momento en que deba resolverse si un determinado ensamblado ha sido cargado. Esto ocurre, por ejemplo, al hacer una llamada al método CreateInstance() de un determinado Assembly, para instanciar un objeto de un tipo determinado. En el caso de que alguna de las DLLs referenciadas por el ensamblado no esté disponible, nuestro manejador de evento procederá a llevar a cabo la carga correspondiente:


[Csharp]
Assembly AssemblyResolveEventHandler(object sender, ResolveEventArgs args)
{
//Este manejador es llamado únicamente cuando el CLR intenta cargar un ensamblado y se produce un error.
//Recupera la lista de ensamblados referenciados
Assembly needAssembly, objExecutingAssemblies;
string assemblyTemporalPath = "";
/* myAssembly, en este caso, es el objeto que intentó realizar la carga fallida. Es el Assembly que hemos cargado
* mediante reflection, y que ha lanzado el evento al hacer un CreateInstance() sobre él. Si quisiéramos llevar a
* cabo el control sobre nuestra aplicación podríamos sustituir myAssembly por Assembly.GetExecutingAssembly();
*/
objExecutingAssemblies = myAssembly;
AssemblyName[] arrReferencedAssmbNames = objExecutingAssemblies.GetReferencedAssemblies();
foreach (AssemblyName strAssmbName in arrReferencedAssmbNames)
{
if (strAssmbName.FullName.Substring(0, strAssmbName.FullName.IndexOf(",")) == args.Name.Substring(0, args.Name.IndexOf(",")))
{
assemblyTemporalPath = DllBasePath + @"\" + args.Name.Substring(0, args.Name.IndexOf(",")) + ".dll";
break;
}
}
needAssembly = Assembly.LoadFrom(assemblyTemporalPath);
return needAssembly;
}
[/Csharp]

Es el caso para un Assembly (myAssembly) que pudimos cargar con un Assembly.LoadFile(DllBasePath + “\DllName.dll”), y que al recuperar sus tipos e intentar instanciar uno de ellos con Assembly.CreateInstance() ha intentado acceder a otra DLL de la que haga uso o herede. Si la ubicación de esa DLL no coincide con los tres casos que especificábamos antes, la carga dará un error. Será entonces cuando entre en acción el manejador del evento AssemblyResolve y llevará a cabo la carga de las DLLs necesarias para continuar con la creación de la instancia de nuestro objeto.

No hay comentarios:

Publicar un comentario