¿Cómo cargar un ensamblado en AppDomain con todas las referencias de forma recursiva?

113

Quiero cargar en un nuevo AppDomainensamblado que tiene un árbol de referencias complejo (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

Por lo que tengo entendido, cuando se carga un ensamblaje AppDomain, sus referencias no se cargan automáticamente y tengo que cargarlas manualmente. Entonces, cuando lo hago:

string dir = @"SomePath"; // different from AppDomain.CurrentDomain.BaseDirectory
string path = System.IO.Path.Combine(dir, "MyDll.dll");

AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
setup.ApplicationBase = dir;
AppDomain domain = AppDomain.CreateDomain("SomeAppDomain", null, setup);

domain.Load(AssemblyName.GetAssemblyName(path));

y obtuve FileNotFoundException:

No se pudo cargar el archivo o ensamblado 'MyDll, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' o una de sus dependencias. El sistema no puede encontrar el archivo especificado.

Creo que la parte clave es una de sus dependencias. .

Ok, hago lo siguiente antes domain.Load(AssemblyName.GetAssemblyName(path));

foreach (AssemblyName refAsmName in Assembly.ReflectionOnlyLoadFrom(path).GetReferencedAssemblies())
{
    domain.Load(refAsmName);
}

Pero tengo FileNotFoundException nuevo, en otro ensamblado (referenciado).

¿Cómo cargar todas las referencias de forma recursiva?

¿Tengo que crear un árbol de referencias antes de cargar el ensamblaje raíz? ¿Cómo obtener las referencias de un ensamblado sin cargarlo?

abatishchev
fuente
1
He cargado ensamblajes como este muchas veces antes, nunca tuve que cargar manualmente todas sus referencias. No estoy seguro de que la premisa de esta pregunta sea correcta.
Mick

Respuestas:

68

Debe invocar CreateInstanceAndUnwrapantes de que su objeto proxy se ejecute en el dominio de la aplicación externa.

 class Program
{
    static void Main(string[] args)
    {
        AppDomainSetup domaininfo = new AppDomainSetup();
        domaininfo.ApplicationBase = System.Environment.CurrentDirectory;
        Evidence adevidence = AppDomain.CurrentDomain.Evidence;
        AppDomain domain = AppDomain.CreateDomain("MyDomain", adevidence, domaininfo);

        Type type = typeof(Proxy);
        var value = (Proxy)domain.CreateInstanceAndUnwrap(
            type.Assembly.FullName,
            type.FullName);

        var assembly = value.GetAssembly(args[0]);
        // AppDomain.Unload(domain);
    }
}

public class Proxy : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFile(assemblyPath);
        }
        catch (Exception)
        {
            return null;
            // throw new InvalidOperationException(ex);
        }
    }
}

Además, tenga en cuenta que si lo usa LoadFrom, probablemente obtendrá una FileNotFoundexcepción porque el solucionador de ensamblados intentará encontrar el ensamblado que está cargando en el GAC o en la carpeta bin de la aplicación actual. Úselo LoadFilepara cargar un archivo de ensamblaje arbitrario en su lugar, pero tenga en cuenta que si hace esto, deberá cargar las dependencias usted mismo.

Jduv
fuente
20
Consulte el código que escribí para resolver este problema: github.com/jduv/AppDomainToolkit . Específicamente, mire el método LoadAssemblyWithReferences en esta clase: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv
3
Descubrí que, aunque esto funciona la mayor parte del tiempo, en algunos casos aún es necesario adjuntar un controlador al AppDomain.CurrentDomain.AssemblyResolveevento como se describe en esta respuesta de MSDN . En mi caso, estaba tratando de conectarme a la implementación de SpecRun que se ejecuta bajo MSTest, pero creo que se aplica a muchas situaciones en las que su código podría no ejecutarse desde el AppDomain "primario" - Extensiones VS, MSTest, etc.
Aaronaught
Ah interesante. Lo investigaré y veré si puedo hacer que sea un poco más fácil trabajar con él a través de ADT. Lo siento, ese código ha estado un poco muerto por un tiempo, todos tenemos trabajos diurnos :).
Jduv
@Jduv Votaría su comentario unas 100 veces si pudiera. Su biblioteca me ayudó a resolver un problema aparentemente irresoluble que estaba teniendo con la carga dinámica de ensamblajes en MSBuild. ¡Deberías promoverlo como una respuesta!
Philip Daniels
2
@Jduv, ¿está seguro de que la assemblyvariable hará referencia al ensamblado de "MyDomain"? Creo var assembly = value.GetAssembly(args[0]);que cargará su args[0]en ambos dominios y la assemblyvariable hará referencia a la copia del dominio de la aplicación principal
Igor Bendrup
14

http://support.microsoft.com/kb/837908/en-us

Versión C #:

Cree una clase de moderador y heredela de MarshalByRefObject:

class ProxyDomain : MarshalByRefObject
{
    public Assembly GetAssembly(string assemblyPath)
    {
        try
        {
            return Assembly.LoadFrom(assemblyPath);
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message);
        }
    }
}

llamar desde el sitio del cliente

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);
rockvista
fuente
6
¿Cómo se pone esta solución en el contexto de la creación de un nuevo AppDomain? ¿Alguien puede explicarlo?
Tri Q Tran
2
Se MarshalByRefObjectpuede pasar por los dominios de aplicación. Entonces, supongo que Assembly.LoadFromintenta cargar el ensamblado en un nuevo dominio de aplicación, lo que solo es posible, si el objeto de llamada pudiera pasarse entre esos dominios de aplicación. Esto también se denomina comunicación remota como se describe aquí: msdn.microsoft.com/en-us/library/…
Christoph Meißner
32
Esto no funciona. Si ejecuta el código y verifica AppDomain.CurrentDomain.GetAssemblies () verá que el ensamblado de destino que está intentando cargar está cargado en el dominio de la aplicación actual y no en el proxy.
Jduv
41
Esto es una completa tontería. Heredar de MarshalByRefObjectno hace que se cargue mágicamente en todos los demás AppDomain, solo le dice al marco .NET que cree un proxy remoto transparente en lugar de usar la serialización cuando desenvuelve la referencia de uno AppDomainen otro AppDomain(la forma típica es el CreateInstanceAndUnwrapmétodo). No puedo creer que esta respuesta tenga más de 30 votos a favor; el código aquí es solo una forma indirecta de llamar sin sentido Assembly.LoadFrom.
Aaronaught
1
Sí, parece una completa tontería, pero tiene 28 votos a favor y está marcado como la respuesta. El enlace proporcionado ni siquiera menciona MarshalByRefObject. Bastante extraño. Si esto realmente hace algo, me encantaría que alguien me explicara cómo
Mick
12

Una vez que vuelva a pasar la instancia de ensamblado al dominio de la persona que llama, el dominio de la persona que llama intentará cargarla. Es por eso que obtiene la excepción. Esto sucede en su última línea de código:

domain.Load(AssemblyName.GetAssemblyName(path));

Por lo tanto, lo que sea que desee hacer con el ensamblado, debe hacerlo en una clase proxy, una clase que hereda MarshalByRefObject .

Tenga en cuenta que el dominio de la persona que llama y el nuevo dominio creado deben tener acceso al ensamblaje de la clase de proxy. Si su problema no es demasiado complicado, considere dejar la carpeta ApplicationBase sin cambios, por lo que será la misma que la carpeta del dominio de la persona que llama (el nuevo dominio solo cargará los ensamblados que necesita).

En código simple:

public void DoStuffInOtherDomain()
{
    const string assemblyPath = @"[AsmPath]";
    var newDomain = AppDomain.CreateDomain("newDomain");
    var asmLoaderProxy = (ProxyDomain)newDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(ProxyDomain).FullName);

    asmLoaderProxy.GetAssembly(assemblyPath);
}

class ProxyDomain : MarshalByRefObject
{
    public void GetAssembly(string AssemblyPath)
    {
        try
        {
            Assembly.LoadFrom(AssemblyPath);
            //If you want to do anything further to that assembly, you need to do it here.
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }
}

Si necesita cargar los ensamblados desde una carpeta que es diferente a la carpeta del dominio de la aplicación actual, cree el nuevo dominio de la aplicación con la carpeta de ruta de búsqueda de dlls específica.

Por ejemplo, la línea de creación del dominio de la aplicación del código anterior debe reemplazarse con:

var dllsSearchPath = @"[dlls search path for new app domain]";
AppDomain newDomain = AppDomain.CreateDomain("newDomain", new Evidence(), dllsSearchPath, "", true);

De esta forma, todas las DLL se resolverán automáticamente desde dllsSearchPath.

Nir
fuente
¿Por qué tengo que cargar el ensamblado usando una clase de proxy? ¿Cuál es la diferencia en comparación con cargarlo usando Assembly.LoadFrom (string). Me interesan los detalles técnicos, desde la perspectiva de CLR. Le agradecería mucho si pudiera darme una respuesta.
Dennis Kassel
Utiliza la clase de proxy para evitar que el nuevo ensamblado se cargue en su dominio de llamada. Si usa Assembly.LoadFrom (cadena), el dominio de la persona que llama intentará cargar las nuevas referencias de ensamblado y no las encontrará porque no busca ensamblados en "[AsmPath]". ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir
11

En su nuevo AppDomain, intente configurar un controlador de eventos AssemblyResolve . Ese evento se llama cuando falta una dependencia.

David
fuente
No es así. En realidad, obtiene una excepción en la línea en la que está registrando este evento en el nuevo AppDomain. Tienes que registrar este evento en el AppDomain actual.
user1004959
Lo hace si la clase se hereda de MarshalByRefObject. No es así si la clase está marcada solo con el atributo [Serializable].
user2126375
5

Debe manejar los eventos AppDomain.AssemblyResolve o AppDomain.ReflectionOnlyAssemblyResolve (según la carga que esté realizando) en caso de que el ensamblado al que se hace referencia no esté en el GAC o en la ruta de prueba de CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve

Dustin Campbell
fuente
¿Entonces tengo que indicar el montaje solicitado manualmente? ¿Incluso está en la nueva AppBase de AppDomain? ¿Hay alguna forma de no hacer eso?
abatishchev
5

Me tomó un tiempo entender la respuesta de @ user1996230, así que decidí proporcionar un ejemplo más explícito. En el siguiente ejemplo, hago un proxy para un objeto cargado en otro AppDomain y llamo a un método en ese objeto desde otro dominio.

class ProxyObject : MarshalByRefObject
{
    private Type _type;
    private Object _object;

    public void InstantiateObject(string AssemblyPath, string typeName, object[] args)
    {
        assembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + AssemblyPath); //LoadFrom loads dependent DLLs (assuming they are in the app domain's base directory
        _type = assembly.GetType(typeName);
        _object = Activator.CreateInstance(_type, args); ;
    }

    public void InvokeMethod(string methodName, object[] args)
    {
        var methodinfo = _type.GetMethod(methodName);
        methodinfo.Invoke(_object, args);
    }
}

static void Main(string[] args)
{
    AppDomainSetup setup = new AppDomainSetup();
    setup.ApplicationBase = @"SomePathWithDLLs";
    AppDomain domain = AppDomain.CreateDomain("MyDomain", null, setup);
    ProxyObject proxyObject = (ProxyObject)domain.CreateInstanceFromAndUnwrap(typeof(ProxyObject).Assembly.Location,"ProxyObject");
    proxyObject.InstantiateObject("SomeDLL","SomeType", new object[] { "someArgs});
    proxyObject.InvokeMethod("foo",new object[] { "bar"});
}
Grouma
fuente
Algunos pequeños errores tipográficos en el código, y debo admitir que no creía que funcionaría, pero esto me salvó la vida. Gracias una tonelada.
Owen Ivory
4

La clave es el evento AssemblyResolve generado por AppDomain.

[STAThread]
static void Main(string[] args)
{
    fileDialog.ShowDialog();
    string fileName = fileDialog.FileName;
    if (string.IsNullOrEmpty(fileName) == false)
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
        if (Directory.Exists(@"c:\Provisioning\") == false)
            Directory.CreateDirectory(@"c:\Provisioning\");

        assemblyDirectory = Path.GetDirectoryName(fileName);
        Assembly loadedAssembly = Assembly.LoadFile(fileName);

        List<Type> assemblyTypes = loadedAssembly.GetTypes().ToList<Type>();

        foreach (var type in assemblyTypes)
        {
            if (type.IsInterface == false)
            {
                StreamWriter jsonFile = File.CreateText(string.Format(@"c:\Provisioning\{0}.json", type.Name));
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                jsonFile.WriteLine(serializer.Serialize(Activator.CreateInstance(type)));
                jsonFile.Close();
            }
        }
    }
}

static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    string[] tokens = args.Name.Split(",".ToCharArray());
    System.Diagnostics.Debug.WriteLine("Resolving : " + args.Name);
    return Assembly.LoadFile(Path.Combine(new string[]{assemblyDirectory,tokens[0]+ ".dll"}));
}
Leslie Marshall
fuente
0

He tenido que hacer esto varias veces y he investigado muchas soluciones diferentes.

La solución que encuentro más elegante y fácil de lograr se puede implementar como tal.

1. Crea un proyecto que puedas crear con una interfaz simple

la interfaz contendrá las firmas de los miembros a los que desee llamar.

public interface IExampleProxy
{
    string HelloWorld( string name );
}

Es importante mantener este proyecto limpio y ligero. Es un proyecto al que ambos AppDomainpueden hacer referencia y nos permitirá no hacer referencia al Assemblyque deseamos cargar en un dominio separado de nuestro ensamblaje de cliente.

2. Ahora cree un proyecto que tenga el código que desea cargar por separado AppDomain.

Este proyecto, al igual que el proyecto de cliente, hará referencia al proyecto de proxy y usted implementará la interfaz.

public interface Example : MarshalByRefObject, IExampleProxy
{
    public string HelloWorld( string name )
    {
        return $"Hello '{ name }'";
    }
}

3. A continuación, en el proyecto del cliente, cargue el código en otro AppDomain.

Entonces, ahora creamos un nuevo AppDomain. Puede especificar la ubicación base para referencias de ensamblaje. El sondeo buscará ensamblados dependientes en GAC y en el directorio actual y la AppDomainbase loc.

// set up domain and create
AppDomainSetup domaininfo = new AppDomainSetup
{
    ApplicationBase = System.Environment.CurrentDirectory
};

Evidence adevidence = AppDomain.CurrentDomain.Evidence;

AppDomain exampleDomain = AppDomain.CreateDomain("Example", adevidence, domaininfo);

// assembly ant data names
var assemblyName = "<AssemblyName>, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null|<keyIfSigned>";
var exampleTypeName = "Example";

// Optional - get a reflection only assembly type reference
var @type = Assembly.ReflectionOnlyLoad( assemblyName ).GetType( exampleTypeName ); 

// create a instance of the `Example` and assign to proxy type variable
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( assemblyName, exampleTypeName );

// Optional - if you got a type ref
IExampleProxy proxy= ( IExampleProxy )exampleDomain.CreateInstanceAndUnwrap( @type.Assembly.Name, @type.Name );    

// call any members you wish
var stringFromOtherAd = proxy.HelloWorld( "Tommy" );

// unload the `AppDomain`
AppDomain.Unload( exampleDomain );

si es necesario, hay un montón de formas diferentes de cargar un ensamblaje. Puede utilizar esta solución de una forma diferente. Si tiene el nombre calificado de ensamblado, entonces me gusta usar el, CreateInstanceAndUnwrapya que carga los bytes de ensamblaje y luego crea una instancia de su tipo para usted y devuelve un objectque puede convertir simplemente a su tipo de proxy o, si no, en un código fuertemente tipado, podría use el tiempo de ejecución del lenguaje dinámico y asigne el objeto devuelto a una dynamicvariable escrita y luego simplemente llame a los miembros directamente.

Ahí tienes.

Esto permite cargar un ensamblado al que el proyecto de su cliente no tiene referencia en un AppDomain y llamar a los miembros desde el cliente.

Para probar, me gusta usar la ventana Módulos en Visual Studio. Le mostrará el dominio de ensamblado de su cliente y todos los módulos que se cargan en ese dominio, así como su nuevo dominio de aplicación y qué ensamblajes o módulos se cargan en ese dominio.

La clave es asegurarse de codificar o derivar MarshalByRefObject o serializable.

`MarshalByRefObject le permitirá configurar la vida útil del dominio en el que se encuentra. Por ejemplo, digamos que desea que el dominio se destruya si no se ha llamado al proxy en 20 minutos.

Espero que esto ayude.

SimperT
fuente
Hola, si mal no recuerdo, el problema principal era cómo cargar todas las dependencias de forma recursiva, de ahí la pregunta. Pruebe su código cambiando HelloWorld para que devuelva una clase de tipo Foo, FooAssemblyque tenga una propiedad de tipo Bar, BarAssembly, es decir, 3 ensamblados en total. ¿Continuaría funcionando?
abatishchev
Sí, es necesario enumerar el directorio adecuado en la etapa de prueba de ensamblaje. AppDomain tiene una ApplicationBase, sin embargo, no la probé. También los archivos de configuración pueden especificar directorios de sondeo de ensamblado, como app.config, que un dll puede usar y configurar para copiar en las propiedades. Además, si tiene control sobre la construcción del ensamblaje que desea cargar en un dominio de aplicación separado, las referencias pueden obtener un HintPath que especifique dónde buscarlo. Si todo eso fallara, me suscribiría al nuevo evento AppDomains AssemblyResolve y cargaría manualmente los ensamblados. Un montón de ejemplos para eso.
SimperT