Solucionar problemas de 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 tarefa ResolveAssemblyReference. Este artigo explica alguns dos detalhes de como ResolveAssemblyReference funciona e como solucionar problemas de falhas de compilação que podem acontecer quando ResolveAssemblyReference não é possível resolver uma referência. Para investigar falhas de referência de assembly, talvez você queira instalar o Visualizador de Logs Estruturados para exibir os logs do MSBuild. As capturas de tela neste artigo são tiradas do Visualizador de Logs Estruturados.

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

Os compiladores só podem aceitar um caminho .dll no sistema de arquivos como referência, portanto, ResolveAssemblyReference converte strings 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 então passados para o compilador por meio da opção /r.

Além disso, ResolveAssemblyReference determina o conjunto completo (na verdade, o fechamento transitivo em termos de teoria dos grafos) de todas as referências .dll e .exe recursivamente e, para cada um deles, determine se deve ser copiado para o diretório de saída de compilação 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 a serem copiados.

ResolveAssemblyReference é invocado a partir do destino ResolveAssemblyReferences:

Captura de tela do visualizador de log mostrando quando ResolveAssemblyReferences é chamado no processo de compilação.

Se você observar a ordenação, ResolveAssemblyReferences está acontecendo antes de Compile e, claro, CopyFilesToOutputDirectory acontece depois de Compile.

Observação

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

Entradas ResolveAssemblyReference

ResolveAssemblyReference é abrangente sobre o registro 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 também 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 item MSBuild Reference no momento em que ResolveAssemblyReference é invocado para o projeto. Todos os metadados e referências de assembly, incluindo suas referências NuGet, devem estar contidos neste item. Cada referência tem um rico conjunto de metadados anexados a ela:

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

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

Captura de tela mostrando AssemblyFiles.

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

FindDependencies="$(_FindDependencies)"

Você pode definir essa propriedade como false em sua compilação para desativar a análise de assemblies de dependência transitiva.

Algoritmo ResolveAssemblyReference

O algoritmo simplificado para a tarefa ResolveAssemblyReference é a 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 a tabela de objeto de referências.
  4. Leia o arquivo de cache do diretório obj (se houver).
  5. Calcule o fechamento de dependências.
  6. Crie as tabelas de saída.
  7. Grave o arquivo de cache no diretório obj.
  8. Registre os resultados.

O algoritmo usa a lista de entrada de assemblies (de metadados e 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 e os resolve de vários locais (incluindo o GAC, AssemblyFoldersEx e assim por diante).

Os assemblies referenciados são adicionados à lista iterativamente até que não sejam adicionadas mais novas referências. Então o algoritmo para.

As referências diretas que você forneceu para a tarefa são chamadas de referências Primárias. Os assemblies indiretos que foram adicionados ao conjunto devido a uma referência transitiva são chamados de Dependência. O registro de cada assembly indireto controla todos os itens primários ("raiz") que levaram à sua inclusão e seus metadados correspondentes.

Resultados da tarefa ResolveAssemblyReference

ResolveAssemblyReference fornece o registro 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 Primárias e Dependências. As referências primárias foram especificadas explicitamente como referências do projeto que está sendo construído. As dependências foram inferidas a partir de referências transitórias.

Importante

ResolveAssemblyReference lê metadados de assembly para determinar as referências de um determinado assembly. Quando o compilador C# emite um assembly, ele adiciona apenas referências a assemblies que são realmente necessários. Portanto, pode acontecer que, quando você compilar um determinado projeto, o projeto 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 ignoradas.

Metadados de item CopyLocal

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

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

Se os metadados CopyLocal estiverem totalmente ausentes, eles serão considerados verdadeiros 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 fazê-lo. ResolveAssemblyReference registra as razões pelas quais escolheu uma referência específica como CopyLocal ou não.

Todas as razões possíveis para a decisão de CopyLocal são enumeradas na tabela a seguir. É útil conhecer essas strings para poder procurá-las nos logs de compilação.

Estado CopyLocal Descrição
Undecided A cópia do estado local não está definida no momento.
YesBecauseOfHeuristic A referência deveria ter CopyLocal='true' porque não era "não" por algum motivo.
YesBecauseReferenceItemHadMetadata A referência deveria ter CopyLocal='true' porque seu item de origem tem Private='true'
NoBecauseFrameworkFile A referência deveria ter CopyLocal='false' porque é um arquivo de estrutura.
NoBecausePrerequisite A referência deveria ter CopyLocal='false' porque é um arquivo de pré-requisito.
NoBecauseReferenceItemHadMetadata A referência deveria ter CopyLocal='false' porque o atributo Private está definido como 'false' no projeto.
NoBecauseReferenceResolvedFromGAC A referência deveria ter CopyLocal='false' porque foi resolvida pelo GAC.
NoBecauseReferenceFoundInGAC Comportamento herdado, CopyLocal='false' quando o assembly é encontrado no GAC (mesmo quando foi resolvido em outro lugar).
NoBecauseConflictVictim A referência deveria ter CopyLocal='false' porque perdeu um conflito entre um arquivo de assembly com o mesmo nome.
NoBecauseUnresolved A referência não foi resolvida. Ela não pode ser copiada para o diretório bin porque não foi encontrada.
NoBecauseEmbedded A referência foi incorporada. Ela não deve ser copiada para o diretório bin porque não será carregada no 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 e não é um assembly.

Metadados de itens privados

Uma parte importante de determinar CopyLocal são os metadados Private 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 fechamento.

  • Se nenhum dos itens de origem especificar os metadados Private, CopyLocal será definido como True (ou não definido, cujo 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 definiu Private como false?

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 informa qual referência definiu Private como false, mas o visualizador de log estruturado adiciona metadados Private aos itens especificados acima:

Captura de tela mostrando Private 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 Cache de Assemblies Global (GAC) desempenha um papel importante para determinar se as referências devem ser copiadas para a saída. Isso é lamentável porque o conteúdo do GAC é específico da máquina, resultando em problemas para compilações reproduzíveis (em que o comportamento difere em várias máquinas dependendo do estado dela, como o GAC).

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

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

AssemblySearchPaths

Há duas maneiras de personalizar a lista de pesquisas de caminhos ResolveAssemblyReference na tentativa de localizar um assembly. Para personalizar totalmente a lista, a propriedade AssemblySearchPaths pode ser definida com antecedência. A ordem é importante. Se um assembly estiver em dois locais, ResolveAssemblyReference será interrompido depois de encontrá-lo no primeiro local.

Por padrão, há 10 pesquisas de ResolveAssemblyReference locais (4 se você usar o SDK do .NET), e cada uma pode ser desabilitada definindo o sinalizador relevante como false:

  • A pesquisa de arquivos do projeto atual é desativada definindo a propriedade AssemblySearchPath_UseCandidateAssemblyFiles como false.
  • A pesquisa da propriedade do caminho de referência (de um arquivo .user) é desativada definindo a propriedade AssemblySearchPath_UseReferencePath como false.
  • O uso do caminho de dica do item é desabilitado definindo 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 de pastas de assembly de AssemblyFolders.config é desabilitada definindo a propriedade AssemblySearchPath_UseAssemblyFoldersConfigFileSearchPath como false.
  • A pesquisa no registro é desabilitada definindo a propriedade AssemblySearchPath_UseRegistry como false.
  • A pesquisa de pastas de assembly registradas herdadas é desabilitada definindo a propriedade AssemblySearchPath_UseAssemblyFolders como false.
  • A procura no GAC é desabilitada definindo a propriedade AssemblySearchPath_UseGAC como false.
  • Tratar o Include da referência como um nome de arquivo real é desabilitado definindo a propriedade AssemblySearchPath_UseRawFileName como false.
  • A verificação da pasta de saída do aplicativo é desabilitada definindo a propriedade AssemblySearchPath_UseOutDir como false.

Houve um conflito

Uma situação comum é que o MSBuild fornece um aviso sobre versões diferentes do mesmo assembly sendo usadas 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 por "Houve um conflito". Ele mostra informações detalhadas sobre quais referências precisavam de quais versões do assembly em questão.