Utiliser des AppDomain pour pouvoir décharger des assemblages

Si vous avez déjà utilisé le chargement dynamique d’assemblages .NET, vous savez certainement qu’une fois une librairie chargée dans le contexte mémoire de la CLR, il n’y a pas de méthode pour la décharger. Si vous jetez un œil à System.Reflection.Assembly, vous constaterez la présence des méthodes Load, LoadFile et LoadFrom, mais aucun Unload ou quoi que ce soit d’approchant.

C’est prévu comme ça, et vous aurez certainement lu que le seul moyen de contourner cette situation est de créer un AppDomain dédié dans lequel vous chargerez la librairie concerné. Le fait de décharger l’AppDomain supprime complètement son contexte, et vous aurez ainsi par ricochet déchargé complètement la librairie de la mémoire.

Le souci est que ceci ne se fait pas en appelant la méthode Load sur l’instance d’AppDomain, contrairement à ce qui parait intuitif.

Pour illustrer ceci, considérez le test unitaire ci-dessous. Comme le second attribut l’indique, il sort en exception UnauthorizedAccessException.

[TestMethod]
[ExpectedException(typeof(UnauthorizedAccessException))]
public void WrongWay()
{
    // On copie un fichier dans le répertoire d'exécution,
    // et on crée un domaine d'application
    File.Copy(
        @"..\..\..\SampleClassLibrary\bin\Debug\SampleClassLibrary.dll", 
        "SampleClassLibrary.dll",
        true);
    AppDomain domain = AppDomain.CreateDomain(
        "Domaine", 
        null, 
        AppDomain.CurrentDomain.SetupInformation);

    // On utilise la méthode Load de l'AppDomain pour charger un assemblage .NET,
    // en en profitant au passage pour valider que ça s'est bien passé
    Assembly assy = domain.Load("SampleClassLibrary");
    Assert.AreEqual("SampleClass", assy.GetTypes()[0].Name);

    // Même en déchargeant l'AppDomain, on ne peut pas supprimer la librairie,
    // et on reçoit une UnauthorizedAccessException !
    AppDomain.Unload(domain);
    File.Delete("SampleClassLibrary.dll");
}

Ce qui se passe est que la librairie est en fait chargée dans le contexte courant de l’application, et non dans l’AppDomain que nous avons créé. Le fait qu’on puisse manipuler les types directement depuis notre contexte principal aurait dû nous mettre la puce à l’oreille, mais il faut avouer que la présence d’une méthode Load sur l’instance d’AppDomain porte à confusion sur la tâche réalisée…

Evidemment, une fois la librairie chargée sur l’AppDomain principal, impossible de la décharger, et donc de libérer le lock sur le fichier, qu’on ne peut donc pas supprimer.

Le moyen de charger la librairie séparément dans le nouvel AppDomain est illustré dans le test unitaire suivant, qui passe correctement (si vous le lancez, n’oubliez pas de ne pas le faire dans le même tir de tests que le premier, sinon, en fonction de l’ordre d’exécution, vous aurez une erreur dûe au fait que le premier n’a pas libéré la librairie) :

[TestMethod]
public void RightWay()
{
    // Pour commencer, on fait exactement pareil que dans l'autre test
    File.Copy(
        @"..\..\..\SampleClassLibrary\bin\Debug\SampleClassLibrary.dll",
        "SampleClassLibrary.dll",
        true);
    AppDomain domain = AppDomain.CreateDomain(
        "Domaine", 
        null, 
        AppDomain.CurrentDomain.SetupInformation);

    // Mais cette fois-ci, on va utiliser le DoCallBack, et c'est dans le délégué
    // appelé que nous allons réaliser nos opérations de chargement et de test
    domain.DoCallBack(new CrossAppDomainDelegate(delegate() {
        Assembly assy = Assembly.Load("SampleClassLibrary");
        Assert.AreEqual("SampleClass", assy.GetTypes()[0].Name);
    }));

    // Cette fois-ci, lorsqu'on décharge l'AppDomain, on peut supprimer la librairie
    AppDomain.Unload(domain);
    File.Delete("SampleClassLibrary.dll");
}

Comme vous pouvez le constater, le code est similaire, aux seules différences près que :

  • on n’utilise plus AppDomain.Load, mais Assemby.Load,
  • cet appel est réalisé depuis un délégué et non plus dans le corps de la fonction,
  • le délégué est associé à la méthode DoCallBack de l’AppDomain.

Dans ce cas, le déchargement de l’AppDomain suffit bien à pouvoir supprimer la librairie qui avait été chargée dynamiquement, ce qui prouve qu’elle avait bien été chargée dans le contexte de cet AppDomain et seulement dans ce contexte.

Bien que d’une indigence rare sur tous les sujets ayant trait à la réflexion, la documentation MSDN nous donne le fin mot de l’histoire :

L’assembly est chargé dans les deux domaines car Assembly ne dérive pas de MarshalByRefObject et, par conséquent, la valeur de retour de la méthode Load ne peut pas être marshalée.

En fait, la seule bonne ressource sur ces questions est le blog de Suzanne Cook.

About JP Gouigoux

Jean-Philippe Gouigoux est Architecte Logiciel, MVP Connected Systems Developer. Il intervient régulièrement à l'Université de Bretagne Sud ainsi qu'à l'Agile Tour. Plus de détails sur la page "Curriculum Vitae" de ce blog.
This entry was posted in .NET, C# and tagged . Bookmark the permalink.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha Captcha Reload