Como carregar um assembly para AppDomain com todas as referências recursivamente?

113

Quero carregar em um novo AppDomainassembly que possui uma árvore de referências complexa (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

Pelo que eu entendi, quando um assembly está sendo carregado AppDomain, suas referências não são carregadas automaticamente, e eu tenho que carregá-las manualmente. Então, quando eu faço:

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));

e obteve FileNotFoundException:

Não foi possível carregar o arquivo ou assembly 'MyDll, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' ou uma de suas dependências. O sistema não pode encontrar o arquivo especificado.

Acho que a parte principal é uma de suas dependências .

Ok, eu faço a seguir antes domain.Load(AssemblyName.GetAssemblyName(path));

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

Mas consegui FileNotFoundExceptionnovamente, em outra montagem (referenciada).

Como carregar todas as referências recursivamente?

Preciso criar uma árvore de referências antes de carregar o assembly raiz? Como obter as referências de um assembly sem carregá-lo?

Abatishchev
fonte
1
Já carreguei assemblies como este muitas vezes antes, nunca tive que carregar manualmente todas as suas referências. Não tenho certeza se a premissa dessa pergunta está correta.
Mick

Respostas:

68

Você precisa invocar CreateInstanceAndUnwrapantes que seu objeto proxy seja executado no domínio de aplicativo externo.

 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);
        }
    }
}

Além disso, observe que, se você usar, LoadFromprovavelmente obterá uma FileNotFoundexceção porque o resolvedor de Assembly tentará localizar o assembly que você está carregando no GAC ou na pasta bin do aplicativo atual. Use LoadFilepara carregar um arquivo de montagem arbitrário em vez disso - mas observe que se você fizer isso, você mesmo precisará carregar quaisquer dependências.

Jduv
fonte
20
Verifique o código que escrevi para resolver esse problema: github.com/jduv/AppDomainToolkit . Especificamente, observe o método LoadAssemblyWithReferences nesta classe: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv
3
Descobri que, embora isso funcione na maioria das vezes, em alguns casos você ainda precisa anexar um manipulador ao AppDomain.CurrentDomain.AssemblyResolveevento, conforme descrito nesta resposta do MSDN . No meu caso, eu estava tentando me conectar à implantação do SpecRun em execução no MSTest, mas acho que se aplica a muitas situações em que seu código pode não ser executado no AppDomain "primário" - extensões VS,
MSTest
Ah, interessante. Vou analisar isso e ver se posso tornar isso um pouco mais fácil de trabalhar via ADT. Lamento que o código esteja um pouco morto por um tempo - todos nós temos empregos diários :).
Jduv
@Jduv aprovaria seu comentário cerca de 100 vezes se eu pudesse. Sua biblioteca me ajudou a resolver um problema aparentemente sem solução que eu estava tendo com o carregamento dinâmico de montagem no MSBuild. Você deve promovê-lo a uma resposta!
Philip Daniels
2
@Jduv tem certeza de que a assemblyvariável fará referência ao assembly de "MyDomain"? Acho que var assembly = value.GetAssembly(args[0]);você carregará seu args[0]em ambos os domínios e a assemblyvariável fará referência à cópia do domínio principal do aplicativo
Igor Bendrup
14

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

Versão C #:

Crie uma classe de moderador e herde-a de MarshalByRefObject:

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

chamada do site do cliente

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);
rockvista
fonte
6
Como essa solução é colocada no contexto da criação de um novo AppDomain, alguém pode explicar?
Tri Q Tran
2
A MarshalByRefObjectpode ser transmitido entre appdomains. Então eu acho que Assembly.LoadFromtenta carregar o assembly em um novo appdomain, o que só é possível, se o objeto de chamada puder ser passado entre esses appdomains. Isso também é chamado de remoting, conforme descrito aqui: msdn.microsoft.com/en-us/library/…
Christoph Meißner
32
Isso não funciona. Se você executar o código e verificar AppDomain.CurrentDomain.GetAssemblies (), verá que o assembly de destino que está tentando carregar está carregado no domínio de aplicativo atual e não no proxy.
Jduv
41
Isso é um absurdo completo. Herdar de MarshalByRefObjectnão faz com que ele seja carregado magicamente um no outro AppDomain, apenas informa ao .NET framework para criar um proxy remoto transparente em vez de usar a serialização quando você desembrulhar a referência de um AppDomainno outro AppDomain(o método típico é o CreateInstanceAndUnwrapmétodo). Não posso acreditar que esta resposta tenha mais de 30 votos positivos; o código aqui é apenas uma forma indireta de chamada Assembly.LoadFrom.
Aaronaught
1
Sim, parece um absurdo completo, mas tem 28 votos positivos e está marcado como a resposta. O link fornecido nem mesmo menciona MarshalByRefObject. Muito bizarro. Se isso realmente fizer alguma coisa, eu adoraria que alguém explicasse como
Mick
12

Depois de passar a instância do assembly de volta para o domínio do chamador, o domínio do chamador tentará carregá-la! É por isso que você obtém a exceção. Isso acontece na sua última linha de código:

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

Portanto, tudo o que você deseja fazer com a montagem, deve ser feito em uma classe proxy - uma classe que herda MarshalByRefObject .

Considere que o domínio do chamador e o novo domínio criado devem ter acesso ao assembly da classe de proxy. Se o seu problema não for muito complicado, considere deixar a pasta ApplicationBase inalterada, para que seja igual à pasta do domínio do chamador (o novo domínio carregará apenas os assemblies necessários).

Em código simples:

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);
        }
    }
}

Se você precisar carregar os assemblies de uma pasta diferente da pasta de domínio de aplicativo atual, crie o novo domínio de aplicativo com uma pasta de caminho de pesquisa dlls específica.

Por exemplo, a linha de criação de domínio de aplicativo do código acima deve ser substituída por:

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

Dessa forma, todas as dlls serão resolvidas automaticamente a partir de dllsSearchPath.

Nir
fonte
Por que tenho que carregar o assembly usando uma classe de proxy? Qual é a diferença em comparação com carregá-lo usando Assembly.LoadFrom (string). Estou interessado nos detalhes técnicos, do ponto de vista do CLR. Eu ficaria muito grato se você pudesse fornecer uma resposta.
Dennis Kassel
Você usa a classe proxy para evitar que o novo assembly seja carregado no domínio do chamador. Se você usar Assembly.LoadFrom (string), o domínio do chamador tentará carregar as novas referências de assembly e não as encontrará porque não procura por assemblies no "[AsmPath]". ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir
11

Em seu novo AppDomain, tente definir um manipulador de eventos AssemblyResolve . Esse evento é chamado quando uma dependência está faltando.

David
fonte
Não é verdade. Na verdade, você obtém uma exceção na linha em que está registrando este evento no novo AppDomain. Você tem que registrar este evento no AppDomain atual.
user1004959
Isso acontecerá se a classe for herdada de MarshalByRefObject. Isso não acontecerá se a classe estiver marcada apenas com o atributo [Serializable].
user2126375
5

Você precisa manipular os eventos AppDomain.AssemblyResolve ou AppDomain.ReflectionOnlyAssemblyResolve (dependendo de qual carga você está fazendo) no caso de o assembly referenciado não estar no GAC ou no caminho de sondagem do CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve

Dustin Campbell
fonte
Devo indicar a montagem solicitada manualmente? Mesmo que esteja no novo AppBase do AppDomain? Existe uma maneira de não fazer isso?
abatishchev,
5

Demorei um pouco para entender a resposta de @ user1996230, então decidi fornecer um exemplo mais explícito. No exemplo a seguir, faço um proxy para um objeto carregado em outro AppDomain e chamo um método nesse objeto de outro domínio.

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
fonte
Alguns pequenos erros de digitação no código, e tenho que admitir que não acreditei que funcionaria, mas isso salvou minha vida. Muito obrigado.
Owen Ivory
4

A chave é o evento AssemblyResolve gerado pelo 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
fonte
0

Eu tive que fazer isso várias vezes e pesquisei muitas soluções diferentes.

A solução que acho mais elegante e fácil de realizar pode ser implementada como tal.

1. Crie um projeto em que você pode criar uma interface simples

a interface conterá assinaturas de todos os membros que você deseja chamar.

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

É importante manter este projeto limpo e leve. É um projeto que ambos AppDomainpodem fazer referência e nos permitirá não fazer referência ao domínio Assemblyque desejamos carregar em seprate de nosso assembly cliente.

2. Agora crie um projeto que tenha o código que deseja carregar separadamente AppDomain.

Este projeto, assim como o proj cliente, fará referência ao proj proxy e você implementará a interface.

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

3. Em seguida, no projeto do cliente, carregue o código em outro AppDomain .

Então, agora criamos um novo AppDomain. Pode especificar o local de base para referências de montagem. A investigação verificará os assemblies dependentes no GAC e no diretório atual e no AppDomainloc de base.

// 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 );

se for necessário, existem várias maneiras diferentes de carregar uma montagem. Você pode usar uma maneira diferente com esta solução. Se você tiver o nome qualificado do assembly, eu gosto de usar o, CreateInstanceAndUnwrappois ele carrega os bytes do assembly e, em seguida, instancia o seu tipo para você e retorna um objectque você pode converter para o seu tipo de proxy ou, se não, para um código fortemente tipado, pode use o tempo de execução de linguagem dinâmica e atribua o objeto retornado a uma dynamicvariável digitada e, em seguida, chame os membros diretamente.

Aí está.

Isso permite carregar uma montagem que o projeto do seu cliente não tenha referência em uma AppDomain e chamar membros do cliente.

Para testar, gosto de usar a janela Módulos no Visual Studio. Ele mostrará seu domínio de assembly do cliente e quais módulos são carregados nesse domínio, bem como seu novo domínio de aplicativo e quais assemblies ou módulos são carregados nesse domínio.

A chave é certificar-se de que o código deriva MarshalByRefObject ou é serializável.

`MarshalByRefObject permitirá que você configure o tempo de vida do domínio em que está. Por exemplo, digamos que você deseja que o domínio seja destruído se o proxy não for chamado em 20 minutos.

Eu espero que isso ajude.

SimperT
fonte
Oi, se bem me lembro, o problema principal era como carregar todas as dependências recursivamente, daí a questão. Teste seu código alterando HelloWorld para retornar uma classe do tipo Foo, FooAssemblyque tem uma propriedade do tipo Bar, BarAssembly, ou seja, 3 assemblies no total. Continuaria a funcionar?
abatishchev
Sim, precisa do diretório adequado enumerado no estágio de sondagem de montagem. AppDomain tem um ApplicationBase, no entanto, não o testei. Além disso, você pode especificar os arquivos de configuração de diretórios de sondagem de montagem, como app.config, que uma dll também pode usar, basta definir para copiar nas propriedades. Além disso, se você tiver controle sobre a construção da montagem que deseja carregar em um domínio de aplicativo separado, as referências podem obter um HintPath que especifica onde procurá-lo. Se tudo isso falhasse, eu acabaria assinando o novo evento AppDomains AssemblyResolve e carreguei manualmente os assemblies. Toneladas de exemplo para isso.
SimperT de