À propos de System.Runtime.Loader.AssemblyLoadContext

La classe AssemblyLoadContext a été introduite dans .NET Core et n’est pas disponible dans .NET Framework. Cet article complète la documentation de l’API AssemblyLoadContext avec des informations conceptuelles.

Cet article concerne les développeurs qui implémentent le chargement dynamique, en particulier les développeurs de framework de chargement dynamique.

Qu’est-ce qu’AssemblyLoadContext ?

Chaque application .NET 5+ et .NET Core utilise AssemblyLoadContext implicitement. Il s’agit du fournisseur d’exécution pour localiser et charger des dépendances. Chaque fois qu’une dépendance est chargée, une instance AssemblyLoadContext est appelée pour la localiser.

  • AssemblyLoadContext fournit un service de localisation, de chargement et de mise en cache d’assemblys managés et d’autres dépendances.
  • Pour prendre en charge le chargement et le déchargement de code dynamique, il crée un contexte isolé pour le chargement du code et ses dépendances dans leur propre instance AssemblyLoadContext.

Règles de contrôle de version

Une instance AssemblyLoadContext est limitée au chargement d’une version d’un Assembly par nomd’assembly simple. Lorsqu’une référence d’assembly est résolue par rapport à une instance AssemblyLoadContext qui a déjà un assembly de ce nom chargé, la version demandée est comparée à la version chargée. La résolution réussit uniquement si la version chargée est égale ou supérieure à la version demandée.

Quand avez-vous besoin de plusieurs instances AssemblyLoadContext ?

La restriction voulant qu’une seule instance AssemblyLoadContext ne puisse charger qu’une seule version d’un assembly peut devenir un problème lors du chargement dynamique des modules de code. Chaque module est compilé indépendamment et les modules peuvent dépendre de différentes versions d’un Assembly. Il s’agit souvent d’un problème lorsque différents modules dépendent de différentes versions d’une bibliothèque couramment utilisée.

Pour prendre en charge le code de chargement dynamique, l’API AssemblyLoadContext s’occupe du chargement de versions conflictuelles de Assembly dans la même application. Chaque instance AssemblyLoadContext fournit un dictionnaire unique qui mappe chaque AssemblyName.Name à une instance Assembly spécifique.

Il fournit également un mécanisme pratique pour regrouper les dépendances liées à un module de code pour le déchargement ultérieur.

Instance AssemblyLoadContext.Default

L’instance AssemblyLoadContext.Default est automatiquement remplie par le runtime au démarrage. La détection par défaut est utilisée pour localiser et rechercher toutes les dépendances statiques.

Les scénarios de chargement de dépendances les plus courants sont résolus.

Dépendances dynamiques

AssemblyLoadContext a différents événements et fonctions virtuelles qui peuvent être substitués.

L’instance AssemblyLoadContext.Default prend uniquement en charge la substitution des événements.

Les articles Algorithme de chargement d’assembly managé, Algorithme de chargement d’assembly satellite et Algorithme de chargement de bibliothèque non managée (native) traite des événements et fonctions virtuelles disponibles. Les articles montrent la position relative de chaque événement et fonction dans les algorithmes de chargement. Cet article ne partage pas ces informations.

Cette section aborde les principes généraux des événements et fonctions pertinents.

  • Répétabilité. Une requête pour une dépendance spécifique doit toujours entraîner la même réponse. La même instance de dépendance chargée doit être retournée. Cette exigence est fondamentale pour la cohérence du cache. Pour les assemblys managés en particulier, nous créons un cache Assembly. La clé de cache est un nom d’assembly simple : AssemblyName.Name
  • Ne se lèvent généralement pas. Ls fonctions devraient avec une valeur null et non pas se lever lorsqu’elles ne parviennent pas à trouver la dépendance demandée. La levée met fin prématurément à la recherche et propage une exception à l’appelant. La levée doit être limitée à des erreurs inattendues telles qu’un assembly endommagé ou une condition de mémoire insuffisante.
  • Éviter la récursivité. N’oubliez pas que ces fonctions et gestionnaires implémentent les règles de chargement pour localiser les dépendances. Votre implémentation ne doit pas appeler les API qui déclenchent la récursivité. Votre code doit appeler des fonctions de chargement AssemblyLoadContext qui nécessitent un chemin d’accès ou un argument de référence de mémoire spécifique.
  • Charger dans le bon AssemblyLoadContext. Le choix de l’emplacement de chargement des dépendances est spécifique à l’application. Le choix est implémenté par ces événements et fonctions. Lorsque votre code appelle des fonctions de chargement par chemin d’accès AssemblyLoadContext, appelez-les sur l’instance où vous souhaitez charger le code. Parfois, retourner null et laisser AssemblyLoadContext.Default gérer la charge peut être l’option la plus simple.
  • Tenez compte des concurrences de threads. Le chargement peut être déclenché par plusieurs threads. AssemblyLoadContext gère les concurrences de threads en ajoutant atomiquement des assembly à son cache. L’instance du perdant de la concurrence est ignorée. Dans votre logique d’implémentation, n’ajoutez pas de logique supplémentaire qui ne gère pas correctement plusieurs threads.

Comment les dépendances dynamiques sont-elles isolées ?

Chaque instance AssemblyLoadContext représente une étendue unique pour les instances Assembly et les définitions Type.

Il n’existe aucune isolation binaire entre ces dépendances. Elles ne sont isolées que parce qu’elles ne se trouvent pas par nom.

Dans chaque AssemblyLoadContext :

Dépendances partagées

Les dépendances peuvent facilement être partagées entre les instances AssemblyLoadContext. Le modèle général permet à AssemblyLoadContext de charger une dépendance. L’autre partage la dépendance à l’aide d’une référence à l’assembly chargé.

Ce partage est requis pour les runtime d’assembly. Ces assembly ne peuvent être chargés que dans le AssemblyLoadContext.Default. Il en est de même pour les frameworks tels que ASP.NET, WPFou WinForms.

Il est recommandé de charger les dépendances partagées dans AssemblyLoadContext.Default. Ce partage est le modèle de conception courant.

Le partage est implémenté dans le codage de l’instance personnalisée AssemblyLoadContext. AssemblyLoadContext a différents événements et fonctions virtuelles qui peuvent être substitués. Quand l’une de ces fonctions retourne une référence à une instance Assembly chargée dans une autre instance AssemblyLoadContext, l’instance Assembly est partagée. L’algorithme de charge standard s’en remet à AssemblyLoadContext.Default pour le chargement afin de simplifier le modèle de partage commun. Pour plus d’informations, consultez Algorithme de chargement d’assembly managé.

Problèmes de conversion de type

Lorsque deux instances AssemblyLoadContext contiennent des définitions de type name identiques, elles ne sont pas du même type. Elles sont du même type si et uniquement si elles proviennent de la même instance Assembly.

Pour compliquer le tout, les messages d’exception relatifs à ces types incompatibles peuvent être déroutants. Les types sont référencés dans les messages d’exception par leurs noms de types simples. Le message d’exception habituel dans ce cas se présente comme suit :

L’objet de type « IsolatedType » ne peut pas être converti en type « IsolatedType ».

Problèmes de conversion de type de débogage

Il est important de savoir les choses suivantes sur une paire de types incompatibles :

Face à deux objets a et b, l’évaluation des éléments suivants dans le débogueur sera utile :

// In debugger look at each assembly's instance, Location, and FullName
a.GetType().Assembly
b.GetType().Assembly
// In debugger look at each AssemblyLoadContext's instance and name
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(a.GetType().Assembly)
System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(b.GetType().Assembly)

Résoudre les problèmes de conversion de type

Il existe deux modèles de conception pour résoudre ces problèmes de conversion de type.

  1. Utilisez des types partagés courants. Ce type partagé peut être un type d’exécution primitif ou impliquer la création d’un nouveau type partagé dans un assembly partagé. Souvent, le type partagé est une interface définie dans un assembly d’application. Pour plus d’informations, découvrez comment les dépendances sont partagées.

  2. Utilisez des techniques de marshaling pour convertir un type à un autre.