Compartir a través de


Solución de problemas de referencias de ensamblado

Una de las tareas más importantes de MSBuild y el proceso de compilación de .NET es resolver las referencias de ensamblado, lo que sucede en la tarea ResolveAssemblyReference. En este artículo se explican algunos de los detalles de cómo ResolveAssemblyReference funciona y cómo solucionar errores de compilación que pueden producirse cuando ResolveAssemblyReference no puede resolver una referencia. Para investigar los errores de referencias de ensamblado, quizá le convendría instalar el Visor de registros estructurados para ver los registros de MSBuild. Las capturas de pantalla de este artículo proceden del Visor de registros estructurados.

La finalidad de ResolveAssemblyReference es tomar todas las referencias especificadas en archivos .csproj (o en otro lugar) a través del elemento <Reference> y asignarlas a rutas a los archivos para ensamblar archivos en el sistema de archivos.

Los compiladores solo pueden aceptar una ruta .dll en el sistema de archivos como referencia, por lo que ResolveAssemblyReference convierte las cadenas como mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 que aparecen en archivos de proyecto en rutas como C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll, que luego se pasan al compilador a través del modificador /r.

Además, ResolveAssemblyReference determina el conjunto completo (en realidad, el cierre transitivo en términos de teoría de gráficos) de todas las referencias .dlly .exe de forma iterativa y en cada uno de ellos determina si se debe copiar en el directorio final de compilación o no. No realiza la copia real (que se gestiona más adelante, después del paso de compilación en sí), sino que prepara una lista de elementos de los archivos que se van a copiar.

ResolveAssemblyReference se invoca del destino ResolveAssemblyReferences:

Captura de pantalla del visor de registros donde se ve cuándo se llama a ResolveAssemblyReferences en el proceso de compilación.

Si se fija en la ordenación, ResolveAssemblyReferences tiene lugar antes de Compile y, por supuesto, CopyFilesToOutputDirectory ocurre después de Compile.

Nota:

La tarea ResolveAssemblyReference la tarea se invoca en el archivo .targets estándar Microsoft.Common.CurrentVersion.targets en las carpetas de instalación de MSBuild. También puede examinar los destinos de MSBuild del SDK de .NET en línea en https://github.com/dotnet/msbuild/blob/a936b97e30679dcea4d99c362efa6f732c9d3587/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1991-L2140. En este vínculo se ve exactamente dónde se invoca la tarea ResolveAssemblyReference en el archivo .targets.

Entradas de ResolveAssemblyReference

ResolveAssemblyReference es exhaustivo en cuanto al registro de sus entradas:

Captura de pantalla donde se ven los parámetros de entrada de la tarea ResolveAssemblyReference.

El nodo Parameters es estándar en todas las tareas, pero además ResolveAssemblyReference registra su propio grupo de información en Entradas (que es básicamente el mismo que en Parameters, pero estructurado de forma diferente).

Las entradas más importantes son Assemblies y AssemblyFiles:

    <ResolveAssemblyReference
        Assemblies="@(Reference)"
        AssemblyFiles="@(_ResolvedProjectReferencePaths);@(_ExplicitReference)"

Assemblies usa el contenido del elemento Reference de MSBuild en el momento en que ResolveAssemblyReference se invoca en el proyecto. Todas las referencias de metadatos y ensamblados, incluidas las referencias de NuGet, deben estar contenidas en este elemento. Cada referencia tiene un variado conjunto de metadatos adjuntos:

Captura de pantalla donde se ven los metadatos en una referencia de ensamblado.

AssemblyFiles procede del elemento final ResolveProjectReference de destino denominado _ResolvedProjectReferencePaths. ResolveProjectReference se ejecuta antes de ResolveAssemblyReference y convierte elementos <ProjectReference> en rutas de ensamblados compilados en disco. Por tanto, AssemblyFiles contendrá los ensamblados creados en todos los proyectos a los que se hace referencia del proyecto actual:

Captura de pantalla de AssemblyFiles.

Otra entrada útil es el parámetro booleano FindDependencies, que toma su valor de la propiedad _FindDependencies:

FindDependencies="$(_FindDependencies)"

Puede establecer esta propiedad false en la compilación para desactivar el análisis de ensamblados de dependencia transitiva.

Algoritmo ResolveAssemblyReference

El algoritmo simplificado de la tarea ResolveAssemblyReference es el siguiente:

  1. Entradas de registro.
  2. Compruebe la variable de entorno MSBUILDLOGVERBOSERARSEARCHRESULTS. Fije esta variable en cualquier valor para obtener registros más detallados.
  3. Inicialice la tabla de objetos de referencias.
  4. Lea el archivo caché del directorio obj (si existe).
  5. Procese el cierre de las dependencias.
  6. Genere las tablas de salida.
  7. Escriba el archivo caché en el directorio obj.
  8. Registre los resultados.

El algoritmo toma la lista de entrada de ensamblados (tanto de metadatos como de referencias de proyecto), recupera la lista de referencias de cada ensamblado que procesa (leyendo los metadatos) y crea un conjunto entero (cierre transitivo) de todos los ensamblados a los que se hace referencia y los resuelve en varias ubicaciones (como la GAC, AssemblyFoldersEx, etc.).

Los ensamblados a los que se hace referencia se agregan a la lista de forma iterativa hasta que no se agregan más referencias nuevas. Tras esto, el algoritmo se detiene.

Las referencias directas que ha dado la tarea se denominan Referencias principales. Los ensamblados indirectos que se han agregado al conjunto correspondiente a una referencia transitiva se denominan Dependencia. El registro de cada ensamblado indirecto realiza un seguimiento de todos los elementos principales ("raíz") que han hecho que se insertaran, así como los metadatos correspondientes.

Resultados de la tarea ResolveAssemblyReference

ResolveAssemblyReference da un registro detallado de los resultados:

Captura de pantalla de los resultados de ResolveAssemblyReference en el visor de registros estructurados.

Los ensamblados resueltos se dividen en dos categorías: Referencias principales y Dependencias. Las Referencias principales se han indicado explícitamente como referencias del proyecto que se está compilando. Las Dependencias se han inferido a partir de las referencias de referencias de manera transitiva.

Importante

ResolveAssemblyReference lee los metadatos del ensamblado para determinar las referencias de un ensamblado determinado. Cuando el compilador de C# genera un ensamblado, solo agrega referencias a ensamblados que realmente son necesarios. Por ello, puede ocurrir que, al compilar un proyecto determinado, este puede señalar una referencia innecesaria que no se va a procesar en el ensamblado. Es correcto agregar referencias al proyecto que no sean necesarias; aunque se omiten.

Metadatos del elemento CopyLocal

Las referencias también pueden tener los metadatos CopyLocal o no. Si la referencia tiene CopyLocal = true, el destino lo copiará más adelante en el directorio final CopyFilesToOutputDirectory. En este ejemplo, DataFlow ha cambiado CopyLocal a true, mientras que esto no pasa con Immutable:

Captura de pantalla de la configuración de CopyLocal en algunas referencias.

Si faltan todos los metadatos de CopyLocal, se supone que su valor es true de forma predeterminada. Por tanto ResolveAssemblyReference intenta de forma predeterminada copiar las dependencias en lo que se genere a menos que encuentre una razón para no hacerlo. ResolveAssemblyReference registra las razones por las que se eligió una referencia determinada para ser CopyLocal o no.

Todas las posibles razones de decisión de CopyLocal aparecen en la tabla siguiente. Resulta útil conocer estas cadenas para poder buscarlas en los registros de compilación.

Estado de CopyLocal Descripción
Undecided El estado de CopyLocal no se sabe en estos momentos.
YesBecauseOfHeuristic La referencia debe tener CopyLocal='true' porque se determinó que "no" por algún motivo.
YesBecauseReferenceItemHadMetadata La referencia debe tener CopyLocal='true' porque el elemento de origen tiene Private='true'
NoBecauseFrameworkFile La referencia debe tener CopyLocal='false' porque es un archivo de plataforma.
NoBecausePrerequisite La referencia debe tener CopyLocal='false' porque es un archivo de prerrequisitos.
NoBecauseReferenceItemHadMetadata La referencia debe tener CopyLocal='false' porque el atributo Private está como "false" en el proyecto.
NoBecauseReferenceResolvedFromGAC La referencia debe tener CopyLocal='false' porque se ha resuelto a través de la GAC.
NoBecauseReferenceFoundInGAC En un entorno heredado, CopyLocal='false', cuando el ensamblado se encuentra en la GAC (aun cuando se ha resuelto en otro sitio).
NoBecauseConflictVictim La referencia debe tener CopyLocal='false' porque ha perdido un conflicto entre un archivo de ensamblado con el mismo nombre.
NoBecauseUnresolved La referencia no se ha resuelto. No se puede copiar en el directorio bin porque no se encontró.
NoBecauseEmbedded La referencia estaba incrustada. No se debe copiar en el directorio bin porque no se cargará en tiempo de ejecución.
NoBecauseParentReferencesFoundInGAC La propiedad copyLocalDependenciesWhenParentReferenceInGac pasa a false y todos los elementos de origen principales se han encontrado en la GAC.
NoBecauseBadImage El archivo de ensamblado facilitado no debe copiarse porque es una imagen incorrecta, posiblemente no administrada y posiblemente no sea un ensamblado en absoluto.

Metadatos de elementos privados

Una parte importante para determinar CopyLocal radica en los metadatos de Private de todas las referencias principales. Cada referencia (principal o dependencia) tiene una lista de todas las referencias principales (elementos de origen) que han contribuido a esa referencia que se va a agregar al cierre.

  • Si ninguno de los elementos de origen indica los metadatos de Private, CopyLocal cambiará a True (o no cambiará, entonces el valor predeterminado es True)
  • Si alguno de los elementos de origen indica Private=true, CopyLocal cambia a True
  • Si ninguno de los ensamblados de origen indica Private=true y al menos uno indica Private=false, CopyLocal cambia a False

¿Qué referencia cambia Private a false?

El último punto es una razón que suele usarse para cambiar CopyLocal a false: This reference is not "CopyLocal" because at least one source item had "Private" set to "false" and no source items had "Private" set to "true".

MSBuild no nos indica qué referencia ha cambiado Private a false, pero el visor de registros estructurado agrega los metadatos de Private a los elementos que lo habían especificado anteriormente:

Captura de pantalla de Private cambiado a false en el visor de registros estructurados.

Esto hace que se investigue con más facilidad e indica exactamente qué referencia provocó que la dependencia en cuestión pasara a CopyLocal=false.

Caché global de ensamblados

La caché global de ensamblados (GAC) desempeña un papel importante en decidir si se deben copiar referencias en el resultado. Esto no es conveniente porque el contenido de la GAC es específico de la máquina y esto da lugar a problemas en compilaciones reproducibles (donde los efectos difieren según cada equipo dependiente del estado de la máquina, como la GAC).

Se han incluido correcciones recientes para que ResolveAssemblyReference remedie esta situación. Puede controlar el funcionamiento mediante estas dos nuevas entradas en ResolveAssemblyReference:

    CopyLocalDependenciesWhenParentReferenceInGac="$(CopyLocalDependenciesWhenParentReferenceInGac)"
    DoNotCopyLocalIfInGac="$(DoNotCopyLocalIfInGac)"

AssemblySearchPaths

Hay dos maneras de personalizar las búsquedas ResolveAssemblyReference de listas de rutas al intentar localizar un ensamblado. Para personalizar totalmente la lista, la propiedad AssemblySearchPaths se puede crear con antelación. El orden es importante; si un ensamblado está en dos ubicaciones, ResolveAssemblyReference parará después de encontrarlo en la primera ubicación.

De forma predeterminada, hay diez búsquedas de ubicaciones de ResolveAssemblyReference (cuatro si usa el SDK de .NET) y cada una se puede deshabilitar pasando el indicador correspondiente a false:

  • La búsqueda de archivos del proyecto actual está deshabilitada si se pone la propiedad AssemblySearchPath_UseCandidateAssemblyFiles en false.
  • La búsqueda de propiedades de rutas de referencia (en un archivo .user) está deshabilitada si se pone la propiedad AssemblySearchPath_UseReferencePath en false.
  • El uso de la ruta de sugerencia del elemento se deshabilita al poner la propiedad AssemblySearchPath_UseHintPathFromItem en false.
  • El uso del directorio con el entorno de ejecución de destino de MSBuild se deshabilita al poner la propiedad AssemblySearchPath_UseTargetFrameworkDirectory en false.
  • La búsqueda de carpetas de ensamblados a través de AssemblyFolders.config se deshabilita al poner la propiedad AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath en false.
  • La búsqueda en el registro se deshabilita poniendo la propiedad AssemblySearchPath_UseRegistry en false.
  • La búsqueda de carpetas de ensamblado registradas heredadas se deshabilita al poner la propiedad AssemblySearchPath_UseAssemblyFolders en false.
  • La búsqueda en la GAC se deshabilita poniendo la propiedad AssemblySearchPath_UseGAC en false.
  • Si se trata la introducción de la referencia como un nombre de archivo real, se deshabilita al poner la propiedad AssemblySearchPath_UseRawFileName en false.
  • La comprobación de la carpeta de salida de la aplicación se deshabilita al poner la propiedad AssemblySearchPath_UseOutDir en false.

Se ha producido un conflicto

Una situación común es que MSBuild genere una advertencia sobre las distintas versiones del mismo ensamblado que usan las referencias diferentes. Para solucionarlo, se suele agregar una redirección de enlace al archivo app.config.

Una firma útil de examinar estos conflictos es buscar en el Visor de registros estructurados de MSBuild el mensaje "There was a conflict" ("Se ha producido un conflicto"). Da información detallada sobre qué referencias necesitaban las versiones del ensamblado en cuestión.