Comment charger un assembly dans AppDomain avec toutes les références de manière récursive?

113

Je veux charger un nouvel AppDomainassemblage qui a un arbre de références complexe (MyDll.dll -> Microsoft.Office.Interop.Excel.dll -> Microsoft.Vbe.Interop.dll -> Office.dll -> stdole.dll)

D'après ce que j'ai compris, lorsqu'un assemblage est en cours de chargement AppDomain, ses références ne sont pas chargées automatiquement et je dois les charger manuellement. Alors quand je fais:

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

et obtenu FileNotFoundException:

Impossible de charger le fichier ou l'assembly 'MyDll, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null' ou l'une de ses dépendances. Le système ne peut pas trouver le fichier spécifié.

Je pense que l'élément clé est l' une de ses dépendances .

Ok, je fais la prochaine avant domain.Load(AssemblyName.GetAssemblyName(path));

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

Mais obtenu à FileNotFoundExceptionnouveau, sur un autre assemblage (référencé).

Comment charger toutes les références de manière récursive?

Dois-je créer une arborescence de références avant de charger l'assembly racine? Comment obtenir les références d'un assemblage sans le charger?

abatishchev
la source
1
J'ai chargé des assemblages comme celui-ci plusieurs fois auparavant, je n'ai jamais eu à charger manuellement toutes ses références. Je ne suis pas sûr que la prémisse de cette question soit correcte.
Mick

Réponses:

68

Vous devez appeler CreateInstanceAndUnwrapavant que votre objet proxy ne s'exécute dans le domaine d'application étranger.

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

Notez également que si vous utilisez, LoadFromvous obtiendrez probablement une FileNotFoundexception car le résolveur d'assembly tentera de trouver l'assembly que vous chargez dans le GAC ou dans le dossier bin de l'application actuelle. Utilisez à la place LoadFilepour charger un fichier d'assemblage arbitraire - mais notez que si vous faites cela, vous devrez charger toutes les dépendances vous-même.

Jduv
la source
20
Consultez le code que j'ai écrit pour résoudre ce problème: github.com/jduv/AppDomainToolkit . Plus précisément, regardez la méthode LoadAssemblyWithReferences dans cette classe: github.com/jduv/AppDomainToolkit/blob/master/AppDomainToolkit/…
Jduv
3
J'ai constaté que bien que cela fonctionne la plupart du temps, dans certains cas, vous devez encore attacher un gestionnaire à l' AppDomain.CurrentDomain.AssemblyResolveévénement comme décrit dans cette réponse MSDN . Dans mon cas, j'essayais de m'accrocher au déploiement SpecRun fonctionnant sous MSTest, mais je pense que cela s'applique à de nombreuses situations dans lesquelles votre code pourrait ne pas fonctionner à partir de l'AppDomain "principal" - extensions VS, MSTest, etc.
Aaronaught
Ah intéressant. Je vais examiner cela et voir si je peux rendre cela un peu plus facile à travailler via ADT. Désolé que le code soit légèrement mort depuis un moment maintenant - nous avons tous des emplois de jour :).
Jduv
@Jduv ferait voter votre commentaire environ 100 fois si je le pouvais. Votre bibliothèque m'a aidé à résoudre un problème apparemment insoluble que j'avais avec le chargement d'assemblage dynamique sous MSBuild. Vous devriez en faire la promotion comme une réponse!
Philip Daniels
2
@Jduv êtes-vous sûr que cette assemblyvariable référencera l'assembly de "MyDomain"? Je pense que var assembly = value.GetAssembly(args[0]);vous allez charger votre args[0]dans les deux domaines et la assemblyvariable référencera la copie du domaine d'application principal
Igor Bendrup
14

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

Version C #:

Créez une classe de modérateur et héritez-en de MarshalByRefObject:

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

appel depuis le site client

ProxyDomain pd = new ProxyDomain();
Assembly assembly = pd.GetAssembly(assemblyFilePath);
rockvista
la source
6
Comment cette solution est-elle mise dans le contexte de la création d'un nouvel AppDomain, quelqu'un peut-il expliquer?
Tri Q Tran
2
Un MarshalByRefObjectpeut être passé autour des domaines d'application. Donc, je suppose que cela Assembly.LoadFromtente de charger l'assembly dans un nouveau domaine d'application, ce qui n'est possible que si l'objet appelant peut être passé entre ces domaines d'application. Cela s'appelle également la communication à distance comme décrit ici: msdn.microsoft.com/en-us/library/…
Christoph Meißner
32
Cela ne marche pas. Si vous exécutez le code et vérifiez AppDomain.CurrentDomain.GetAssemblies (), vous verrez que l'assembly cible que vous essayez de charger est chargé dans le domaine d'application actuel et non dans celui du proxy.
Jduv
41
C'est un non-sens complet. Hériter de MarshalByRefObjectne le fait pas se charger comme par magie AppDomain, il indique simplement au framework .NET de créer un proxy distant transparent au lieu d'utiliser la sérialisation lorsque vous décompressez la référence de l'une AppDomaindans l'autre AppDomain(la méthode typique étant la CreateInstanceAndUnwrapméthode). Je ne peux pas croire que cette réponse a plus de 30 votes positifs; le code ici juste une façon inutilement détournée d'appeler Assembly.LoadFrom.
Aaronaught le
1
Oui, cela ressemble à un non-sens complet, mais il a 28 votes et est marqué comme la réponse. Le lien fourni ne mentionne même pas MarshalByRefObject. Assez bizarre. Si cela fait vraiment quelque chose, j'aimerais que quelqu'un vous explique comment
Mick
12

Une fois que vous avez renvoyé l'instance d'assembly au domaine de l'appelant, le domaine de l'appelant essaiera de la charger! C'est pourquoi vous obtenez l'exception. Cela se produit dans votre dernière ligne de code:

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

Ainsi, tout ce que vous voulez faire avec l'assembly doit être fait dans une classe proxy - une classe qui hérite de MarshalByRefObject .

Tenez compte du fait que le domaine de l'appelant et le nouveau domaine créé doivent tous deux avoir accès à l'assembly de classe proxy. Si votre problème n'est pas trop compliqué, envisagez de laisser le dossier ApplicationBase inchangé, il sera donc identique au dossier du domaine de l'appelant (le nouveau domaine ne chargera que les assemblys dont il a besoin).

En code 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 vous devez charger les assemblys à partir d'un dossier différent de votre dossier de domaine d'application actuel, créez le nouveau domaine d'application avec un dossier de chemin de recherche de DLL spécifique.

Par exemple, la ligne de création de domaine d'application du code ci-dessus doit être remplacée par:

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

De cette façon, toutes les dll seront automatiquement résolues à partir de dllsSearchPath.

Nir
la source
Pourquoi dois-je charger l'assembly à l'aide d'une classe proxy? Quelle est la différence par rapport au chargement en utilisant Assembly.LoadFrom (string). Je suis intéressé par les détails techniques, du point de vue du CLR. Je vous serais très reconnaissant de bien vouloir fournir une réponse.
Dennis Kassel
Vous utilisez la classe proxy afin d'éviter que le nouvel assembly ne soit chargé dans votre domaine appelant. Si vous utilisez Assembly.LoadFrom (string), le domaine appelant essaiera de charger les nouvelles références d'assembly et ne les trouvera pas car il ne recherche pas d'assemblys dans le "[AsmPath]". ( msdn.microsoft.com/en-us/library/yx7xezcf%28v=vs.110%29.aspx )
Nir
11

Sur votre nouveau AppDomain, essayez de définir un gestionnaire d'événements AssemblyResolve . Cet événement est appelé lorsqu'une dépendance est manquante.

David
la source
Ce n'est pas le cas. En fait, vous obtenez une exception sur la ligne où vous enregistrez cet événement sur le nouvel AppDomain. Vous devez enregistrer cet événement sur l'AppDomain actuel.
user1004959
Il le fait si la classe est héritée de MarshalByRefObject. Ce n'est pas le cas si la classe est marquée uniquement avec l'attribut [Serializable].
user2126375
5

Vous devez gérer les événements AppDomain.AssemblyResolve ou AppDomain.ReflectionOnlyAssemblyResolve (selon la charge que vous effectuez) au cas où l'assembly référencé ne se trouve pas dans le GAC ou sur le chemin de détection du CLR.

AppDomain.AssemblyResolve

AppDomain.ReflectionOnlyAssemblyResolve

Dustin Campbell
la source
Je dois donc indiquer manuellement l'assemblage demandé? Même si c'est dans la nouvelle AppBase d'AppDomain? Y a-t-il un moyen de ne pas faire ça?
abatishchev
5

Il m'a fallu un certain temps pour comprendre la réponse de @ user1996230, j'ai donc décidé de fournir un exemple plus explicite. Dans l'exemple ci-dessous, je crée un proxy pour un objet chargé dans un autre AppDomain et j'appelle une méthode sur cet objet à partir d'un autre domaine.

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
la source
Quelques petites fautes de frappe dans le code, et je dois admettre que je ne pensais pas que cela fonctionnerait, mais cela m'a sauvé la vie. Merci beaucoup.
Owen Ivory
4

La clé est l'événement AssemblyResolve déclenché par 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
la source
0

J'ai dû faire cela plusieurs fois et j'ai recherché de nombreuses solutions différentes.

La solution que je trouve la plus élégante et la plus facile à réaliser peut être mise en œuvre telle quelle.

1. Créez un projet que vous pouvez créer une interface simple

l'interface contiendra les signatures de tous les membres que vous souhaitez appeler.

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

Il est important de garder ce projet propre et léger. C'est un projet que les deux AppDomainpeuvent référencer et qui nous permettra de ne pas référencer le que Assemblynous souhaitons charger dans un domaine séparé de notre assembly client.

2. Créez maintenant un projet contenant le code que vous souhaitez charger séparément AppDomain.

Ce projet comme avec le projet client référencera le proj proxy et vous implémenterez l'interface.

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

3. Ensuite, dans le projet client, chargez le code dans un autre AppDomain.

Alors, maintenant, nous créons un nouveau AppDomain. Peut spécifier l'emplacement de base des références d'assemblage. Le sondage vérifiera les assemblys dépendants dans GAC et dans le répertoire actuel et la AppDomainlocalisation 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 );

si vous en avez besoin, il existe une multitude de façons différentes de charger un assemblage. Vous pouvez utiliser une méthode différente avec cette solution. Si vous avez le nom qualifié d'assembly, j'aime utiliser le CreateInstanceAndUnwrapcar il charge les octets de l'assembly, puis instancie votre type pour vous et retourne un objectque vous pouvez simplement convertir en votre type de proxy ou si vous ne le faites pas en code fortement typé, vous pouvez utilisez le runtime de langage dynamique et affectez l'objet retourné à une dynamicvariable typée, puis appelez simplement les membres directement dessus.

Voilà.

Cela permet de charger un assembly auquel votre projet client n'a pas de référence dans un fichier séparé AppDomainet d'appeler les membres dessus à partir du client.

Pour tester, j'aime utiliser la fenêtre Modules dans Visual Studio. Il vous montrera votre domaine d'assembly client et quels modules sont chargés dans ce domaine, ainsi que votre nouveau domaine d'application et quels assemblys ou modules sont chargés dans ce domaine.

La clé est de s'assurer que le code dérive MarshalByRefObjectou est sérialisable.

`MarshalByRefObject vous permettra de configurer la durée de vie du domaine dans lequel il se trouve. Exemple, disons que vous voulez que le domaine soit détruit si le proxy n'a pas été appelé dans 20 minutes.

J'espère que ça aide.

SimperT
la source
Salut, si je me souviens bien, le problème principal était de savoir comment charger toutes les dépendances de manière récursive, d'où la question. Veuillez tester votre code en changeant HelloWorld pour renvoyer une classe de type Foo, FooAssemblyqui a une propriété de type Bar, BarAssembly, soit 3 assemblys au total. Cela continuerait-il de fonctionner?
abatishchev
Oui, le répertoire approprié doit être énuméré lors de l'étape d'analyse de l'assembly. AppDomain a une ApplicationBase, cependant, je ne l'ai pas testée. Vous pouvez également spécifier des fichiers de configuration d'assemblage, tels que app.config, qu'une DLL peut également utiliser, juste pour copier dans les propriétés. De plus, si vous contrôlez la création de l'assembly souhaitant charger dans un domaine d'application distinct, les références peuvent obtenir un HintPath qui spécifie de le rechercher. Si tout cela échouait, j'aurais pour conséquence de m'abonner au nouvel événement AppDomains AssemblyResolve et de charger manuellement les assemblys. Des tonnes d'exemples pour cela.
SimperT