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 .dll
y .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
:
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:
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:
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:
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:
- Entradas de registro.
- Compruebe la variable de entorno
MSBUILDLOGVERBOSERARSEARCHRESULTS
. Fije esta variable en cualquier valor para obtener registros más detallados. - Inicialice la tabla de objetos de referencias.
- Lea el archivo caché del directorio
obj
(si existe). - Procese el cierre de las dependencias.
- Genere las tablas de salida.
- Escriba el archivo caché en el directorio
obj
. - 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:
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
:
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á aTrue
(o no cambiará, entonces el valor predeterminado esTrue
) - Si alguno de los elementos de origen indica
Private=true
,CopyLocal
cambia aTrue
- Si ninguno de los ensamblados de origen indica
Private=true
y al menos uno indicaPrivate=false
,CopyLocal
cambia aFalse
¿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:
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 propiedadAssemblySearchPath_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.