Risolvere i problemi relativi ai riferimenti agli assembly

Una delle attività più importanti di MSBuild e del processo di compilazione .NET consiste nella risoluzione dei riferimenti agli assembly, che si verifica nell'attività ResolveAssemblyReference . Questo articolo illustra alcuni dettagli sul funzionamento ResolveAssemblyReference e come risolvere gli errori di compilazione che possono verificarsi quando ResolveAssemblyReference non è in grado di risolvere un riferimento. Per analizzare gli errori di riferimento agli assembly, è possibile installare Structured Log Viewer per visualizzare i log di MSBuild. Gli screenshot di questo articolo sono tratti dal Visualizzatore log strutturato.

Lo scopo di ResolveAssemblyReference consiste nell'accettare tutti i riferimenti specificati nei .csproj file (o altrove) tramite l'elemento ed eseguirne il <Reference> mapping ai percorsi dei file di assembly nel file system.

I compilatori possono accettare solo un .dll percorso nel file system come riferimento, quindi ResolveAssemblyReference converte stringhe come mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 presenti nei file di progetto in percorsi come C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll, che vengono quindi passati al compilatore tramite l'opzione /r .

Determina inoltre ResolveAssemblyReference il set completo (in realtà la chiusura transitiva in termini di teoria del grafo) di tutti i .dll riferimenti e .exe in modo ricorsivo e per ognuno di essi determina se deve essere copiato nella directory di output di compilazione o meno. Non esegue la copia effettiva (gestita in un secondo momento, dopo il passaggio di compilazione effettivo), ma prepara un elenco di elementi di file da copiare.

ResolveAssemblyReference viene richiamato dalla ResolveAssemblyReferences destinazione:

Screenshot del visualizzatore di log che mostra quando viene chiamato ResolveAssemblyReferences nel processo di compilazione.

Se si nota l'ordinamento, ResolveAssemblyReferences avviene prima Compiledi e, naturalmente, CopyFilesToOutputDirectory si verifica dopo Compile.

Nota

ResolveAssemblyReferencel'attività viene richiamata nel file Microsoft.Common.CurrentVersion.targets standard .targets nelle cartelle di installazione di MSBuild. È anche possibile esplorare le destinazioni di MSBuild di .NET SDK online all'indirizzo https://github.com/dotnet/msbuild/blob/a936b97e30679dcea4d99c362efa6f732c9d3587/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1991-L2140. Questo collegamento mostra esattamente dove viene richiamata l'attività ResolveAssemblyReference nel .targets file.

Input ResolveAssemblyReference

ResolveAssemblyReference è completo sulla registrazione degli input:

Screenshot che mostra i parametri di input per l'attività ResolveAssemblyReference.

Il Parameters nodo è standard per tutte le attività, ma registra anche ResolveAssemblyReference il proprio set di informazioni in Input (che è fondamentalmente uguale Parametersa in , ma strutturato in modo diverso).

Gli input più importanti sono Assemblies e AssemblyFiles:

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

Assemblies usa il contenuto dell'elemento Reference MSBuild al momento in cui ResolveAssemblyReference viene richiamato per il progetto. Tutti i metadati e i riferimenti all'assembly, inclusi i riferimenti NuGet, devono essere contenuti in questo elemento. A ogni riferimento è associato un set completo di metadati:

Screenshot che mostra i metadati in un riferimento all'assembly.

AssemblyFiles proviene dall'elemento ResolveProjectReference di output di destinazione denominato _ResolvedProjectReferencePaths. ResolveProjectReference viene eseguito prima ResolveAssemblyReference e converte gli elementi in percorsi di assembly compilati <ProjectReference> su disco. AssemblyFiles Conterrà quindi gli assembly compilati da tutti i progetti a cui si fa riferimento del progetto corrente:

Screenshot che mostra AssemblyFiles.

Un altro input utile è il parametro booleano FindDependencies , che accetta il valore dalla _FindDependencies proprietà :

FindDependencies="$(_FindDependencies)"

È possibile impostare questa proprietà su false nella compilazione per disattivare l'analisi degli assembly di dipendenza transitiva.

Algoritmo ResolveAssemblyReference

L'algoritmo semplificato per l'attività ResolveAssemblyReference è il seguente:

  1. Input del log.
  2. Controllare la variabile di MSBUILDLOGVERBOSERARSEARCHRESULTS ambiente. Impostare questa variabile su qualsiasi valore per ottenere log più dettagliati.
  3. Inizializzare la tabella dell'oggetto references.
  4. Leggere il file della cache dalla obj directory (se presente).
  5. Calcolare la chiusura delle dipendenze.
  6. Compilare le tabelle di output.
  7. Scrivere il file della cache nella obj directory .
  8. Registrare i risultati.

L'algoritmo accetta l'elenco di input degli assembly (sia dai metadati che dai riferimenti al progetto), recupera l'elenco di riferimenti per ogni assembly che elabora (leggendo i metadati) e compila un set completo (chiusura transitiva) di tutti gli assembly a cui si fa riferimento e li risolve da varie posizioni (inclusi gac, AssemblyFoldersEx e così via).

Gli assembly a cui si fa riferimento vengono aggiunti all'elenco in modo iterativo finché non vengono aggiunti altri nuovi riferimenti. Quindi l'algoritmo si arresta.

I riferimenti diretti forniti all'attività sono denominati riferimenti primari. Gli assembly indiretti aggiunti al set a causa di un riferimento transitivo sono denominati Dependency. Il record per ogni assembly indiretto tiene traccia di tutti gli elementi primari ("radice") che ne hanno portato all'inclusione e ai relativi metadati corrispondenti.

Risultati dell'attività ResolveAssemblyReference

ResolveAssemblyReference fornisce la registrazione dettagliata dei risultati:

Screenshot che mostra i risultati di ResolveAssemblyReference nel visualizzatore log strutturato.

Gli assembly risolti sono suddivisi in due categorie: Riferimenti primari e Dipendenze. I riferimenti primari sono stati specificati in modo esplicito come riferimenti del progetto da compilare. Le dipendenze sono state dedotte dai riferimenti dei riferimenti in modo transitivo.

Importante

ResolveAssemblyReference legge i metadati dell'assembly per determinare i riferimenti di un determinato assembly. Quando il compilatore C# genera un assembly, aggiunge solo riferimenti agli assembly effettivamente necessari. Pertanto, potrebbe verificarsi che quando si compila un determinato progetto, il progetto potrebbe specificare un riferimento non necessario che non verrà inserito nell'assembly. È ok aggiungere riferimenti al progetto che non sono necessari; vengono ignorati.

Metadati dell'elemento CopyLocal

I riferimenti possono anche avere o meno i CopyLocal metadati. Se il riferimento ha CopyLocal = true, verrà copiato in un secondo momento nella directory di output in base alla CopyFilesToOutputDirectory destinazione. In questo esempio, DataFlow è CopyLocal impostato su true, mentre Immutable non:

Screenshot che mostra le impostazioni CopyLocal per alcuni riferimenti.

Se i CopyLocal metadati mancano completamente, si presuppone che sia true per impostazione predefinita. Quindi ResolveAssemblyReference , per impostazione predefinita, tenta di copiare le dipendenze nell'output, a meno che non trovi un motivo per cui non. ResolveAssemblyReference registra i motivi per cui ha scelto o meno un riferimento CopyLocal specifico.

Nella tabella seguente vengono enumerati tutti i possibili motivi per CopyLocal la decisione. È utile conoscere queste stringhe per poterle cercare nei log di compilazione.

CopyLocal state Descrizione
Undecided Lo stato locale di copia è indecidato al momento.
YesBecauseOfHeuristic Il riferimento dovrebbe essere CopyLocal='true' dovuto perché non era "no" per nessun motivo.
YesBecauseReferenceItemHadMetadata Il riferimento deve avere CopyLocal='true' perché l'elemento di origine ha Private='true'
NoBecauseFrameworkFile Il riferimento deve avere CopyLocal='false' perché si tratta di un file framework.
NoBecausePrerequisite Il riferimento deve avere CopyLocal='false' perché è un file prerequisito.
NoBecauseReferenceItemHadMetadata Il riferimento deve avere CopyLocal='false' perché l'attributo Private è impostato su 'false' nel progetto.
NoBecauseReferenceResolvedFromGAC Il riferimento deve avere CopyLocal='false' perché è stato risolto dalla GAC.
NoBecauseReferenceFoundInGAC Comportamento legacy, CopyLocal='false' quando l'assembly viene trovato nella GAC (anche quando è stato risolto altrove).
NoBecauseConflictVictim Il riferimento deve avere CopyLocal='false' perché ha perso un conflitto tra un file di assembly con lo stesso nome.
NoBecauseUnresolved Il riferimento non è stato risolto. Non è possibile copiarlo nella directory bin perché non è stato trovato.
NoBecauseEmbedded Il riferimento è stato incorporato. Non deve essere copiato nella directory bin perché non verrà caricato in fase di esecuzione.
NoBecauseParentReferencesFoundInGAC La proprietà copyLocalDependenciesWhenParentReferenceInGac è impostata su false e tutti gli elementi di origine padre sono stati trovati nella GAC.
NoBecauseBadImage Il file di assembly fornito non deve essere copiato perché è un'immagine non valida, probabilmente non gestita, probabilmente non è un assembly.

Metadati dell'elemento privato

Una parte importante della determinazione CopyLocal è costituita dai Private metadati di tutti i riferimenti primari. Ogni riferimento (primario o dipendenza) ha un elenco di tutti i riferimenti primari (elementi di origine) che hanno contribuito a tale riferimento da aggiungere alla chiusura.

  • Se nessuno degli elementi di origine specifica Private i metadati, CopyLocal viene impostato True su (o non impostato, che per impostazione predefinita è True)
  • Se uno degli elementi di origine specifica Private=true, CopyLocal è impostato su True
  • Se nessuno degli assembly di origine specifica Private=true e almeno uno specifica Private=false, CopyLocal è impostato su False

Quale riferimento imposta Private su false?

L'ultimo punto è un motivo spesso usato per CopyLocal essere impostato su 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 non indica quale riferimento è impostato su Private false, ma il visualizzatore di log strutturato aggiunge Private metadati agli elementi specificati in precedenza:

Screenshot che mostra l'opzione Private impostata su false nel visualizzatore di log strutturato.

Ciò semplifica le indagini e indica esattamente quale riferimento ha causato l'impostazione della dipendenza in questione con CopyLocal=false.

Global Assembly Cache

La Global Assembly Cache (GAC) svolge un ruolo importante per determinare se copiare i riferimenti all'output. Ciò è sfortunato perché il contenuto della GAC è specifico del computer e questo comporta problemi per le compilazioni riproducibili (dove il comportamento è diverso in base allo stato del computer, ad esempio la GAC).

Sono state apportate recenti correzioni per ResolveAssemblyReference alleviare la situazione. È possibile controllare il comportamento da questi due nuovi input in ResolveAssemblyReference:

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

AssemblySearchPaths

Esistono due modi per personalizzare l'elenco di ricerche nei percorsi ResolveAssemblyReference nel tentativo di individuare un assembly. Per personalizzare completamente l'elenco, è possibile impostare la proprietà AssemblySearchPaths in anticipo. L'ordine è importante; se un assembly si trova in due posizioni, ResolveAssemblyReference si arresta dopo averlo trovato nella prima posizione.

Per impostazione predefinita, sono disponibili dieci ricerche in posizioni ResolveAssemblyReference (quattro se si usa .NET SDK) e ognuna può essere disabilitata impostando il flag pertinente su false:

  • La ricerca di file dal progetto corrente è disabilitata impostando la AssemblySearchPath_UseCandidateAssemblyFiles proprietà su false.
  • La ricerca della proprietà del percorso di riferimento (da un .user file) è disabilitata impostando la AssemblySearchPath_UseReferencePath proprietà su false.
  • L'uso del percorso hint dell'elemento è disabilitato impostando la AssemblySearchPath_UseHintPathFromItem proprietà su false.
  • L'uso della directory con il runtime di destinazione di MSBuild è disabilitato impostando la AssemblySearchPath_UseTargetFrameworkDirectory proprietà su false.
  • La ricerca di cartelle di assembly da AssemblyFolders.config è disabilitata impostando la AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath proprietà su false.
  • La ricerca nel Registro di sistema è disabilitata impostando la AssemblySearchPath_UseRegistry proprietà su false.
  • La ricerca di cartelle di assembly registrate legacy è disabilitata impostando la AssemblySearchPath_UseAssemblyFolders proprietà su false.
  • La ricerca nella GAC è disabilitata impostando la AssemblySearchPath_UseGAC proprietà su false.
  • Il trattamento dell'elemento Include del riferimento come nome file reale è disabilitato impostando la AssemblySearchPath_UseRawFileName proprietà su false.
  • Il controllo della cartella di output dell'applicazione è disabilitato impostando la AssemblySearchPath_UseOutDir proprietà su false.

Si è verificato un conflitto

Una situazione comune è che MSBuild genera un avviso relativo a versioni diverse dello stesso assembly usato da riferimenti diversi. La soluzione comporta spesso l'aggiunta di un reindirizzamento di binding al file app.config.

Un modo utile per analizzare questi conflitti consiste nel cercare in MSBuild Structured Log Viewer "Si è verificato un conflitto". Vengono visualizzate informazioni dettagliate sui riferimenti necessari alle versioni dell'assembly in questione.