Compartilhar via


Solucionar problemas com referências de assembly

Uma das tarefas mais importantes no MSBuild e no processo de build do .NET é resolver referências de assembly, o que acontece na ResolveAssemblyReference tarefa. Este artigo explica alguns dos detalhes de como ResolveAssemblyReference funciona e como solucionar problemas de falhas de build que podem acontecer quando ResolveAssemblyReference não é possível resolver uma referência. Para investigar falhas de referência do assembly, convém instalar o Visualizador de Log Estruturado para exibir logs do MSBuild. As capturas de tela neste artigo são tiradas do Visualizador de Log Estruturado.

A finalidade de ResolveAssemblyReference é usar todas as referências especificadas em .csproj arquivos (ou em outro lugar) por meio do item <Reference> e mapeá-las para caminhos de arquivos de assembly no sistema de arquivos.

Os compiladores só podem aceitar um .dll caminho no sistema de arquivos como referência, portanto ResolveAssemblyReference converte cadeias de caracteres como mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 que aparecem em arquivos de projeto em caminhos como C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.dll, que são passados para o compilador por meio do /r switch.

ResolveAssemblyReference Além disso, determina conjunto completo (na verdade, o fechamento transitivo em termos de teoria do grafo) de todas as referências .dll e .exe recursivamente, e, para cada uma delas, determina se deve ser copiada para o diretório de saída de build ou não. Ele não faz a cópia real (que é tratada posteriormente, após a etapa de compilação real), mas prepara uma lista de itens de arquivos para copiar.

ResolveAssemblyReference é invocado a partir do ResolveAssemblyReferences alvo:

Captura de tela do visualizador de log mostrando quando ResolveAssemblyReferences é chamado no processo de build.

Se você observar a ordenação, ResolveAssemblyReferences ocorrerá antes Compilee, claro, CopyFilesToOutputDirectory ocorrerá depois Compile.

Observação

ResolveAssemblyReferencea tarefa é invocada no arquivo .targets padrão Microsoft.Common.CurrentVersion.targets nas pastas de instalação do MSBuild. Você também pode navegar online pelos destinos do MSBuild do SDK .NET em https://github.com/dotnet/msbuild/blob/a936b97e30679dcea4d99c362efa6f732c9d3587/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1991-L2140. Este link mostra exatamente onde a ResolveAssemblyReference tarefa é invocada no .targets arquivo.

Entradas ResolveAssemblyReference

ResolveAssemblyReference é abrangente sobre o registro em log de suas entradas:

Captura de tela mostrando parâmetros de entrada para a tarefa ResolveAssemblyReference.

O nó Parameters é padrão para todas as tarefas, mas, além disso, ResolveAssemblyReference registra seu próprio conjunto de informações em Entradas (que é basicamente o mesmo que em Parameters, mas estruturado de forma diferente).

As entradas mais importantes são Assemblies e AssemblyFiles.

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

Assemblies usa o conteúdo do Reference item MSBuild no momento ResolveAssemblyReference em que é invocado para o projeto. Todas as referências de metadados e de assemblies, incluindo as suas referências do NuGet, devem estar contidas nesse elemento. Cada referência tem um conjunto avançado de metadados anexados a ela:

Captura de tela exibindo metadados em uma referência de assembly.

AssemblyFiles vem do item de saída do destino ResolveProjectReference chamado _ResolvedProjectReferencePaths. ResolveProjectReference é executado antes ResolveAssemblyReference e converte <ProjectReference> itens em caminhos de assemblies criados em disco. Portanto, o AssemblyFiles conterá os assemblies criados por todos os projetos referenciados do projeto atual.

Captura de tela mostrando AssemblyFiles.

Outra entrada útil é o parâmetro booliano FindDependencies , que obtém seu valor da _FindDependencies propriedade:

FindDependencies="$(_FindDependencies)"

Você pode definir essa propriedade false em seu build para desativar a análise de assemblies de dependência transitiva.

Algoritmo ResolveAssemblyReference

O algoritmo simplificado para a tarefa é o ResolveAssemblyReference seguinte:

  1. Entradas de log.
  2. Verifique a variável de ambiente MSBUILDLOGVERBOSERARSEARCHRESULTS. Defina essa variável como qualquer valor para obter logs mais detalhados.
  3. Inicialize o objeto de tabela de referências.
  4. Leia o arquivo de cache do obj diretório (se presente).
  5. Compute o fechamento de dependências.
  6. Crie as tabelas de saída.
  7. Escreva o arquivo de cache no obj diretório.
  8. Registre os resultados em log.

O algoritmo recebe a lista de entrada de assemblies (tanto de metadados quanto de referências de projeto), recupera a lista de referências para cada assembly que processa (lendo metadados) e cria um conjunto completo (fechamento transitivo) de todos os assemblies referenciados, resolvendo-os de vários locais (incluindo GAC, AssemblyFoldersEx e assim por diante).

Os assemblies referenciados são adicionados à lista iterativamente até que não sejam adicionadas mais novas referências. Em seguida, o algoritmo é interrompido.

As referências diretas que você forneceu à tarefa são chamadas de referências primárias. Assemblies indiretos que foram adicionados ao conjunto devido a uma referência transitiva são chamados de Dependência. O registro de cada montagem indireta rastreia todos os itens primários ("raiz") que resultaram em sua inclusão e seus metadados correspondentes.

Resultados da tarefa "ResolveAssemblyReference"

ResolveAssemblyReference fornece registro em log detalhado dos resultados:

Captura de tela mostrando os resultados de ResolveAssemblyReference no visualizador de log estruturado.

Os assemblies resolvidos são divididos em duas categorias: referências principais e dependências. As referências primárias foram especificadas explicitamente como referências do projeto que está sendo criado. As dependências foram inferidas de referências de referências transitivamente.

Importante

ResolveAssemblyReference lê os metadados do assembly para determinar as referências de um determinado assembly. Quando o compilador C# emite um assembly, ele só adiciona referências a assemblies que são realmente necessários. Portanto, pode ocorrer que, ao compilar um determinado projeto, ele especifique uma referência desnecessária que não será incorporada ao assembly. Não há problema em adicionar referências ao projeto que não são necessárias; eles são ignorados.

Metadados do item CopyLocal

As referências também podem ter os CopyLocal metadados ou não. Se a referência tiver CopyLocal = true, ela será copiada posteriormente para o diretório de saída pelo alvo CopyFilesToOutputDirectory. Neste exemplo, DataFlow está com CopyLocal configurado como verdadeiro, enquanto Immutable não está:

Captura de tela mostrando as configurações do CopyLocal para algumas referências.

Se os CopyLocal metadados estiverem totalmente ausentes, ele será considerado verdadeiro por padrão. Portanto, ResolveAssemblyReference por padrão, tenta copiar dependências para a saída, a menos que encontre um motivo para não fazer isso. ResolveAssemblyReference registra os motivos pelos quais escolheu uma referência específica para ser CopyLocal ou não.

Todos os motivos possíveis para a decisão CopyLocal são enumerados na tabela a seguir. É útil conhecer essas cadeias de caracteres para poder pesquisá-las em logs de build.

Estado de CopyLocal Description
Undecided O estado de cópia local ainda está pendente.
YesBecauseOfHeuristic A referência deve conter CopyLocal='true' porque não houve um motivo justificado para 'não'.
YesBecauseReferenceItemHadMetadata A Referência deve ter CopyLocal='true' porque seu item de origem tem Private='true'
NoBecauseFrameworkFile A referência deve ter CopyLocal='false' porque é um arquivo de estrutura.
NoBecausePrerequisite A referência deve ter CopyLocal='false' porque é um arquivo de pré-requisito.
NoBecauseReferenceItemHadMetadata A referência deve ter CopyLocal='false' porque o Private atributo está definido como 'false' no projeto.
NoBecauseReferenceResolvedFromGAC A referência deve ter CopyLocal='false' porque foi resolvida do GAC.
NoBecauseReferenceFoundInGAC Comportamento herdado, CopyLocal='false' quando a assemblagem é encontrada no GAC (mesmo quando ele foi resolvido em outro local).
NoBecauseConflictVictim A referência deve ter CopyLocal='false' porque perdeu um conflito entre um arquivo de assembly com o mesmo nome.
NoBecauseUnresolved A referência não foi resolvida. O arquivo não pode ser copiado para a pasta bin porque não foi encontrado.
NoBecauseEmbedded A referência foi inserida. Ele não deve ser copiado para o diretório bin porque ele não será carregado em runtime.
NoBecauseParentReferencesFoundInGAC A propriedade copyLocalDependenciesWhenParentReferenceInGac é definida como false e todos os itens de origem pai foram encontrados no GAC.
NoBecauseBadImage O arquivo de assembly fornecido não deve ser copiado porque é uma imagem ruim, possivelmente não gerenciada, ou possivelmente nem é um assembly.

Metadados de um item privado

Uma parte importante da determinação CopyLocal são os Private metadados em todas as referências primárias. Cada referência (primária ou dependência) tem uma lista de todas as referências primárias (itens de origem) que contribuíram para que essa referência fosse adicionada ao resultado final.

  • Se nenhum dos itens de origem especificar Private metadados, CopyLocal será definido como True (ou não definido, que usa como padrão True)
  • Se qualquer um dos itens de origem especificar Private=true, CopyLocal será definido como True
  • Se nenhum dos assemblies de origem especificar Private=true e pelo menos um especificar Private=false, CopyLocal será definido como False

Qual referência configurou 'Private' para falso?

O último ponto é um motivo frequentemente usado para CopyLocal ser definido como 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".

O MSBuild não nos diz qual referência foi definida Private como false, mas o visualizador de log estruturado adiciona Private metadados aos itens que a especificaram acima:

Captura de tela mostrando Privado definido como false no visualizador de log estruturado.

Isso simplifica as investigações e informa exatamente qual referência fez com que a dependência em questão fosse definida com CopyLocal=false.

Cache de assemblies global

O GAC (Cache de Assembly Global) desempenha um papel importante ao decidir se as referências à saída devem ser copiadas. Isso é lamentável porque o conteúdo do GAC é específico da máquina, o que resulta em problemas para builds reproduzíveis (onde o comportamento difere em máquinas diferentes dependendo do estado da máquina, como no caso do GAC).

Houve correções recentes feitas em ResolveAssemblyReference para aliviar a situação. Você pode controlar o comportamento por meio de duas novas entradas em ResolveAssemblyReference:

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

AssemblySearchPaths

Há duas maneiras de personalizar a lista de caminhos ResolveAssemblyReference ao tentar localizar um assembly. Para personalizar totalmente a lista, a propriedade AssemblySearchPaths pode ser definida com antecedência. A ordem importa; se um assembly estiver em dois locais, ResolveAssemblyReference será interrompido depois que ele o encontrar no primeiro local.

AssemblySearchPaths é uma lista delimitada por ponto-e-vírgula. Ele dá suporte a um conjunto de espaços reservados embutidos (por exemplo, {HintPathFromItem} e {GAC}) que são expandidos para locais reais durante a resolução.

Por padrão, projetos não estilo SDK usam a seguinte ordem de caminho de pesquisa:

  1. Arquivos de assembly do candidato ({CandidateAssemblyFiles})
  2. A ReferencePath propriedade ($(ReferencePath))
  3. Caminhos sugeridos de <Reference> itens ({HintPathFromItem})
  4. O diretório do framework de destino ({TargetFrameworkDirectory})
  5. Pastas de assembly de AssemblyFolders.config ($(AssemblyFoldersConfigFileSearchPath))
  6. O registro ({Registry:...})
  7. Pastas de assembly registradas obsoletas ({AssemblyFolders})
  8. O GAC (Cache de Assembly Global) ({GAC})
  9. Tratar o <Reference Include="..."> valor como um nome de arquivo real ({RawFileName})
  10. O diretório de saída ($(OutDir))

O SDK do .NET define um padrão menor (excluindo, por padrão, buscas no diretório de saída, Registro e GAC):

  • {CandidateAssemblyFiles}
  • {HintPathFromItem}
  • {TargetFrameworkDirectory}
  • {RawFileName}

Para ver o valor efetivo para o seu build, inspecione a entrada SearchPaths registrada por ResolveAssemblyReference, por exemplo, no MSBuild Structured Log Viewer, ou pré-processe o projeto com msbuild /pp.

Cada entrada pode ser desabilitada definindo o sinalizador relevante como false:

  • A pesquisa de arquivos do projeto atual é desabilitada definindo a AssemblySearchPath_UseCandidateAssemblyFiles propriedade como false.
  • A pesquisa da propriedade do caminho de referência (do arquivo .user) é desabilitada definindo a propriedade AssemblySearchPath_UseReferencePath como false.
  • O uso do caminho de dica do item é desabilitado ao definir a propriedade AssemblySearchPath_UseHintPathFromItem como false.
  • O uso do diretório com o runtime de destino do MSBuild é desabilitado definindo a propriedade AssemblySearchPath_UseTargetFrameworkDirectory como "false".
  • A pesquisa nas pastas de assembly do AssemblyFolders.config é desativada ao definir a propriedade AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath como false.
  • A pesquisa no registro é desabilitada definindo a AssemblySearchPath_UseRegistry propriedade como false.
  • A pesquisa em pastas de assembly registradas herdadas é desabilitada ao definir a AssemblySearchPath_UseAssemblyFolders propriedade como false.
  • A procura no GAC está desabilitada definindo a propriedade AssemblySearchPath_UseGAC como false.
  • Tratar a inclusão da referência como um nome de arquivo real é desabilitado definindo a AssemblySearchPath_UseRawFileName propriedade como false.
  • A verificação da pasta de saída do aplicativo está desabilitada definindo a AssemblySearchPath_UseOutDir propriedade como false.

Houve um conflito

Uma situação comum é que o MSBuild fornece um aviso sobre versões diferentes do mesmo assembly que está sendo usado por referências diferentes. A solução geralmente envolve a adição de um redirecionamento de associação ao arquivo app.config.

Uma maneira útil de investigar esses conflitos é pesquisar no Visualizador de Log Estruturado do MSBuild "Houve um conflito". Ele mostra informações detalhadas sobre quais referências precisavam de quais versões do assembly em questão.