Acerca de System.Runtime.Loader.AssemblyLoadContext

La clase AssemblyLoadContext se introdujo en .NET Core y no está disponible en .NET Framework. Este artículo complementa a la documentación de la API AssemblyLoadContext con información conceptual.

Resulta de interés para los desarrolladores que implementan cargas dinámicas, en particular los desarrolladores de marcos de cargas dinámicas.

¿Qué es AssemblyLoadContext?

Cada aplicación de .NET 5+ y .NET Core usa implícitamente AssemblyLoadContext. Es el proveedor del runtime para buscar y cargar las dependencias. Cada vez que se carga una dependencia, se invoca una instancia de AssemblyLoadContext para buscarla.

  • AssemblyLoadContext proporciona un servicio de búsqueda, carga y almacenamiento en caché de ensamblados administrados y otras dependencias.
  • Para admitir la carga y descarga de código dinámico, crea un contexto aislado con el fin de cargar el código y sus dependencias en su propia instancia de AssemblyLoadContext.

Reglas de control de versiones

Una única instancia de AssemblyLoadContext se limita a cargar exactamente una versión de un elemento Assembly por nombre de ensamblado simple. Cuando se resuelve una referencia de ensamblado en una instancia de AssemblyLoadContext que ya tiene un ensamblado con ese nombre cargado, la versión solicitada se compara con la versión cargada. La resolución solo se realizará correctamente si la versión cargada es igual o superior a la versión solicitada.

¿Cuándo necesita varias instancias de AssemblyLoadContext?

La restricción de que una instancia de AssemblyLoadContext solo puede cargar una versión de un ensamblado puede convertirse en un problema al cargar módulos de código dinámicamente. Cada módulo se compila de forma independiente y los módulos pueden depender de distintas versiones de un elemento Assembly. Esto resulta a menudo un problema cuando distintos módulos dependen de versiones diferentes de una biblioteca usada habitualmente.

Para admitir la carga de código de forma dinámica, la API AssemblyLoadContext proporciona versiones en conflicto de carga de un elemento Assembly en la misma aplicación. Cada instancia de AssemblyLoadContext proporciona un diccionario único que asigna cada AssemblyName.Name a una instancia de Assembly concreta.

También proporciona un mecanismo práctico para agrupar las dependencias relacionadas con un módulo de código para su descarga posterior.

Instancia de AssemblyLoadContext.Default

La instancia de AssemblyLoadContext.Default la rellena automáticamente el runtime en el inicio. Utiliza el sondeo predeterminado para buscar y encontrar todas las dependencias estáticas.

Resuelve los escenarios de carga de dependencias más comunes.

Dependencias dinámicas

AssemblyLoadContext tiene varios eventos y funciones virtuales que se pueden invalidar.

La instancia de AssemblyLoadContext.Default solo admite la invalidación de los eventos.

Los artículos Algoritmo de carga de ensamblado administrado, Algoritmo de carga de ensamblado satélite y Algoritmo de carga de biblioteca no administrada (nativa) hacen referencia a todos los eventos y funciones virtuales disponibles. En los artículos se muestra la posición relativa de cada evento y función en los algoritmos de carga. En este artículo no se reproduce esa información.

En esta sección se tratan los principios generales de las funciones y eventos relevantes.

  • Ser reiterativo. Una consulta para una dependencia concreta siempre debe tener como resultado la misma respuesta. Se debe devolver la misma instancia de dependencia cargada. Este requisito es fundamental para la coherencia de la memoria caché. En el caso concreto de los ensamblados administrados, se crea una caché de Assembly. La clave de caché es un nombre de ensamblado sencillo, AssemblyName.Name.
  • Normalmente no se inician. Se espera que estas funciones devuelvan null, en lugar de iniciarse, cuando no se pueda encontrar la dependencia solicitada. La generación finalizará prematuramente la búsqueda y propagará una excepción al autor de la llamada. El inicio se debe restringir a errores inesperados, como un ensamblado dañado o una condición de memoria insuficiente.
  • Evitar la recursividad. Tenga en cuenta que estas funciones y controladores implementan las reglas de carga para buscar dependencias. Su implementación no debe llamar a las API que desencadenan la recursividad. Normalmente, el código debería llamar a las funciones de carga AssemblyLoadContext que requieran una ruta de acceso concreta o un argumento de referencia de memoria.
  • Cargar en el elemento AssemblyLoadContext correcto. La elección de dónde cargar las dependencias es específica de la aplicación. Estos eventos y funciones implementan la opción. Cuando el código llama a las funciones de carga por ruta de acceso AssemblyLoadContext, les llama en la instancia en la que se quiere cargar el código. En algún momento, devolver null y permitir que AssemblyLoadContext.Default controle la carga puede ser la opción más sencilla.
  • Tenga en cuenta las carreras de subprocesos. La carga la pueden desencadenar varios subprocesos. AssemblyLoadContext controla las carreras de subprocesos mediante la adición atómica de ensamblados a su caché. La instancia del que pierde la carrera se descarta. En la lógica de implementación, no agregue lógica adicional que no controle correctamente varios subprocesos.

¿Cómo se aíslan las dependencias dinámicas?

Cada instancia de AssemblyLoadContext representa un ámbito único para instancias de Assembly y definiciones de Type.

No existe aislamiento binario entre estas dependencias. Solo están aisladas por no encontrarse entre sí por nombre.

En cada AssemblyLoadContext:

Dependencias compartidas

Las dependencias se pueden compartir fácilmente entre instancias de AssemblyLoadContext. El modelo general es que un elemento AssemblyLoadContext cargue una dependencia. El otro comparte la dependencia mediante el uso de una referencia en el ensamblado cargado.

Este uso compartido es necesario para los ensamblados en runtime. Estos ensamblados solo se pueden cargar en AssemblyLoadContext.Default. Lo mismo se requiere en el caso de marcos como ASP.NET, WPF o WinForms.

Se recomienda cargar las dependencias compartidas en AssemblyLoadContext.Default. Este uso compartido es el modelo de diseño común.

El uso compartido se implementa en la codificación de la instancia de AssemblyLoadContext personalizada. AssemblyLoadContext tiene varios eventos y funciones virtuales que se pueden invalidar. Cuando alguna de estas funciones devuelve una referencia a una instancia de Assembly que se ha cargado en otra instancia de AssemblyLoadContext, se comparte la instancia de Assembly. El algoritmo de carga estándar se aplaza a AssemblyLoadContext.Default para cargar con el fin de simplificar el patrón común de uso compartido. Para más información, consulte Algoritmo de carga de ensamblado administrado.

Incidencias de conversión de tipos

Cuando dos instancias de AssemblyLoadContext contienen definiciones de tipo con el mismo name, no son del mismo tipo. Son del mismo tipo si y solo si proceden de la misma instancia de Assembly.

Para complicar las cosas, los mensajes de excepción sobre estos tipos no coincidentes pueden ser confusos. Se hace referencia a los tipos en los mensajes de excepción por sus nombres de tipo simple. En este caso, el mensaje de excepción común tendría el formato siguiente:

El objeto de tipo "IsolatedType" no se puede convertir al tipo "IsolatedType".

Depuración de incidencias de conversión de tipos

Dado un par de tipos no coincidentes, es importante conocer también lo siguiente:

Dado dos objetos a y b, la evaluación en el depurador de lo siguiente resultará útil:

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

Resolución de incidencias de conversión de tipos

Existen dos modelos de diseño para resolver estas incidencias de conversión de tipos.

  1. Usar tipos comunes compartidos. Este tipo compartido puede ser un tipo en runtime primitivo o puede implicar la creación de un nuevo tipo compartido en un ensamblado compartido. A menudo, el tipo compartido es una interfaz definida en un ensamblado de aplicación. Para más información, lea sobre cómo se comparten las dependencias.

  2. Use técnicas de serialización para realizar la conversión de un tipo a otro.