Cargar información de reflection sin cargar el assembly

Bueno… veamos un post rapidito. En un proyecto en el que he participado hemos estado personalizando Visual Studio a través de varios custom editors, plugins, packages y demás fauna que pulula por la selva de extensibilidad de Visual Studio.

Estos editores, addines y demás necesitaban acceder a información de Reflection de la propia DLL que se estaba compilando. Teóricamente obtener la información es muy sencillo. Basta con obtener la ruta a la DLL que se está compilando:

private static EnvDTE.DTE DTE

{

    get { return (EnvDTE.DTE)Package.GetGlobalService(typeof(EnvDTE.DTE)); }

}

 

public static string ObtenerRutaEnsamblado()

{

    var project = DTE.ActiveDocument.ProjectItem.ContainingProject;

    return project.Properties.Item("LocalPath").Value.ToString() +

        project.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value.ToString();

}

 

public static string ObtenerNombreEnsamblado()

{

    var project = DTE.ActiveDocument.ProjectItem.ContainingProject;

    return string.Concat(ObtenerRutaEnsamblado(), project.Properties.Item("OutputFileName").Value.ToString());

}

El método ObtenerNombreEnsamblado da la ruta física de la DLL que se está compilando. A partir de aquí, debería bastar con usar LoadAssembly, para cargar la DLL y listos. Pero por supuesto, si esto fuese así, esta entrada del blog no existiría 🙂

El tema está en que cuando accedemos a un Assembly via Reflection, este assembly se carga en el CLR.  Y una vez un Assembly está cargado no puede ni cargarse de nuevo (para ver las modificaciones, por ejemplo, recordad que estamos cargando la propia DLL que el usuario está creando en VS) ni tampoco descargarse. Además el archivo físico se puede crear bloqueado (lo que en nuestro caso impedía que pudieses compilar el proyecto, ya que estaba bloqueado por el addin). Si alguno de vosotros está pensando en cargar el proyecto “solo para Reflection”, que se olvide. Cargar un assembly “solo para Reflection” lo carga igual y tampoco se puede ni cargar de nuevo ni descargar.

¿La solución? Bueno, pues utilizar un AppDomain nuevo. Para los que no lo sepáis los AppDomains son como “procesos” dentro del CLR. Un programa se ejecuta dentro de un AppDomain pero puede crear más AppDomains, del mismo modo que un proceso puede crear procesos hijos. Por supuesto la comunicación entre dos AppDomains se trata como comunicación interproceso: o a través de proxies (objetos MarshalByRef) o pasando objetos serializables. ¡Viva la vida!

Al final, terminé con una clase AppDomainUtils, con métodos estáticos parecidos a los siguientes:

    /// <summary>

    /// Carga el tipo TObj en un AppDomain nuevo.

    /// TObj DEBE ser MarshalByRef

    /// </summary>

    private static TObj LoadFromType<TObj>(AppDomain appDomain)

    {

        var tokens = typeof(TObj).AssemblyQualifiedName.Split(‘,’);

        var assName = tokens[1];

        var typeName = tokens[0];

        var obj = appDomain.CreateInstanceAndUnwrap(assName, typeName);

        return (TObj)obj;

    }

 

    /// <summary>

    /// Obtiene información (de tipo TR) de un System.Type.

    /// </summary>

    /// <typeparam name="TR">Tipo de información que se devuelve. Debe ser Serializable</typeparam>

    /// <typeparam name="TU">Tipo de la clase que extrae la información a partir del System.Type</typeparam>

    /// <param name="fullName">Nombre del System.Type a cargar (con assembly incorporado)</param>

    /// <param name="locationPath">Ruta fisica real del assembly</param>

    /// <returns>La información extraída del System.Type</returns>

    public static TR GetTypeInfo<TR, TU>(string fullName, string locationPath)

        where TU : TypeLoader

    {

        var appDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString());

        var tloader = LoadFromType<TU>(appDomain);

        var result = tloader.LoadTypeInfo<TR>(fullName, locationPath);

        AppDomain.Unload(appDomain);

        return result;

    }

}

La clase TypeLoader es como sigue:

/// <summary>

/// Carga información de un tipo.

/// </summary>

public class TypeLoader : MarshalByRefObject

{

    /// <summary>

    /// Carga el tipo y extrae la información

    /// </summary>

    public TR LoadTypeInfo<TR>(string fullName, string locationPath)

    {

        var type = Type.GetType(fullName);

        if (type == null)

        {

            var tokens = fullName.Split(‘,’).Select(x => x.Trim()).ToArray();

            var assFileName = tokens[1];

            var assFileNameWithExtension = string.Concat(assFileName.Trim(), ".dll");

            var assembly = AssemblyLoader.CargarAssemblyDesdeByteArray(Path.Combine(locationPath, assFileNameWithExtension));

            var typeName = tokens[0];

            type = assembly.GetTypes().FirstOrDefault(x => x.FullName == typeName);

        }

        return type != null ? (TR)Select(type) : default(TR);

    }

 

    /// <summary>

    /// Este método recibe un Type y debe devolver la info que se necesita de dicho Type.

    /// Este objeto DEBE ser serializable y debe ser una instancia (o casteable) de TR

    /// </summary>

    protected virtual object Select(Type type) { return null; }

La idea es cargar un System.Type, extraer información de él y devolverla. Evidentemente esto debe hacerse en un AppDomain nuevo. El método GetTypeInfo lo que hace es crear este AppDomain nuevo y luego, dentro de este AppDomain crear una instancia de un objeto propio, de un tipo cualquiera TU, pero que TU derive de TypeLoader. Y llama al método LoadTypeInfo de este objeto propio. El método LoadTypeInfo (definido en la clase TypeLoader) es el método que:

  1. Carga el assembly (usando un método propio que lo carga desde un array de bytes para asegurar que el fichero no se queda bloqueado. Simplemente lee todo el contenido del fichero en un byte[] y luego usa Assembly.Load pasándole este byte[]).
  2. Obtiene el tipo (System.Type) especificado.
  3. Llama al método Select que recibe un System.Type y debe devolver un objeto serializable con la información. Este objeto es el que se transmitirá al AppDomain principal (de ahí que deba ser serializable). Y no, System.Type no lo es.

El uso al final es bastante sencillo:

var data = AppDomainUtils.GetTypeInfo<TypeIdInfo, TypeIdInfoLoader>(tag.TypeName, OperativaReader.ObtenerRutaEnsamblado());

En la variable tag.TypeName está el nombre del tipo (full-qualified) a cargar. Al ejecutar esta línea en data tenemos un objeto de tipo TypeIdInfo que contiene la información que nos interesaba extraer del objeto System.Type. La clase TypeIdInfoLoader es la que transforma un System.Type en un TypeIdInfo:

class TypeIdInfoLoader : TypeLoader

{

    protected override object Select(Type type)

    {       

        var data = new TypeIdInfo() { FullName = type.FullName };

        return data;

    }

}

El código del méotdo Select de la clase TypeIdInfoLoader se ejecuta en el otro AppDomain, de ahí que deba devolver un objeto serializable (la clase TypeIdInfo debe estar marcada como tal).

En fin… comentar tan solo que todo este peñazo de usar AppDomains es porque los señores de Microsoft no han tenido a bien proporcionar una API que permite usar Reflection sin cargar la DLL dentro del CLR. Y no, lo siento, pero esta API no me sirve. Quiero algo que para usarlo no deba morir mil veces.

Saludos! 😉

3 comentarios sobre “Cargar información de reflection sin cargar el assembly”

  1. @Juanma
    Pues no conocía Mono.Cecil! Si lo hubiese conocido hubiese investigado su uso seguro. No sé si se podrá usar o no desde un Addin de VS, pero bueno… ¡Gracias por la información!

  2. +1 @Juanma. Mono.Cecil es para mi la mejor alternativa ya que no solo no cargas el assembly sino que el modelo que tiene te permite acceder a toda la metadata super facil y rápido, y se puede consultar con linq.

    Es uno de los proyectos más interesantes de Mono y a mi personalmente me parece absolutamente genial.

    Saludos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *