.NET: Tratamento de Dependências

Quem não encontrou problemas devido ao redirecionamento de montagem? Provavelmente, todos que desenvolveram um aplicativo relativamente grande enfrentarão mais cedo ou mais tarde esse problema.

Agora, trabalho no JetBrains, no projeto JetBrains Rider, e estou envolvido na tarefa de migrar o Rider para o .NET Core. Anteriormente envolvido em infraestrutura compartilhada no Circuit, uma plataforma de hospedagem de aplicativos baseada em nuvem.



Sob a cena, está a transcrição do meu relatório da conferência DotNext 2019 em Moscou, onde falei sobre as dificuldades ao trabalhar com assemblies no .NET e mostrei com exemplos práticos o que acontece e como lidar com isso.


Em todos os projetos em que trabalhei como desenvolvedor .NET, tive que lidar com vários problemas ao conectar dependências e carregar assemblies. Nós vamos falar sobre isso.

Estrutura do cargo:


  1. Problemas de dependência
  2. Carga rigorosa da plataforma

  3. .NET Core

  4. Depurar downloads de assembly


Quais são alguns problemas de dependência?


Quando eles começaram a desenvolver o .NET Framework no início dos anos 2000, o problema do inferno da Dependência já era conhecido quando os desenvolvedores de todas as bibliotecas permitiam quebrar alterações, e essas bibliotecas se tornaram incompatíveis para uso com o código já compilado. Como resolver esse problema? A primeira solução é óbvia. Sempre mantenha compatibilidade com versões anteriores. Obviamente, isso não é muito realista, porque quebrar as mudanças é muito fácil de inserir no código. Por exemplo:



Quebrando Alterações e Bibliotecas .NET

Este é um exemplo específico para o .NET. Temos um método e decidimos adicionar um parâmetro com um valor padrão. O código continuará a ser compilado se o montarmos novamente, mas binários serão dois métodos completamente diferentes: um método tem zero argumentos, o segundo método tem um argumento. Se o desenvolvedor dentro da dependência quebrou a compatibilidade com versões anteriores dessa maneira, não poderemos usar o código que foi compilado com essa dependência na versão anterior.

A segunda solução para problemas de dependência é adicionar controle de versão de bibliotecas, assemblies - qualquer coisa. Pode haver regras de versão diferentes, o ponto é que, de alguma forma, podemos distinguir versões diferentes da mesma biblioteca uma da outra, e você pode entender se a atualização será interrompida ou não. Infelizmente, assim que apresentamos as versões, um tipo diferente de problema aparece.



Versão infernal é a incapacidade de usar uma dependência que é compatível com binários, mas ao mesmo tempo possui uma versão que não se encaixava no tempo de execução ou outro componente que verifica essas versões. No .NET, uma manifestação típica da versão infernal é FileLoadException, embora o arquivo esteja no disco, mas, por algum motivo, não é carregado com o tempo de execução.



No .NET, os assemblies têm muitas versões diferentes - eles tentaram corrigir os infernos da versão de várias maneiras e ver o que aconteceu. Nós temos um pacote System.Collections.Immutable. Muitas pessoas o conhecem. Ele tem a versão mais recente do pacote NuGet 1.6.0. Ele contém uma biblioteca, um assembly com a versão 1.2.4.0. Você recebeu que você não possui uma versão 1.2.4.0 da biblioteca de compilação. Como entender que ele se encontra no pacote 1.6.0 NuGet? Não vai ser fácil. Além da versão do assembly, esta biblioteca possui várias outras versões. Por exemplo, Versão do arquivo de montagem, Versão de informações da montagem. Na verdade, este pacote NuGet contém três assemblies diferentes com as mesmas versões (para versões diferentes do .NET Standard).

Documentação do .NET
Opbuild padrão

Muita documentação foi escrita sobre como trabalhar com assemblies no .NET. Existe um Guia .NET para o desenvolvimento de aplicativos modernos para o .NET, levando em consideração o .NET Framework, .NET Standard, .NET Core, código aberto e tudo o que pode ser. Cerca de 30% de todo o documento é dedicado ao carregamento de montagens. Analisaremos problemas e exemplos específicos que possam surgir.

Por que tudo isso é necessário? Em primeiro lugar, para evitar pisar em um ancinho. Em segundo lugar, você pode facilitar a vida dos usuários de suas bibliotecas, pois com a biblioteca eles não terão os problemas de dependência aos quais estão acostumados. Também ajudará você a lidar com a migração de aplicativos complexos para o .NET Core. E ainda por cima, você pode se tornar um SRE, este é um engenheiro de redirecionamento sênior (obrigatório), para o qual todos da equipe vêm e perguntam como escrever outro redirecionamento.

Montagem rigorosa


O carregamento estrito do assembly é o principal problema que os desenvolvedores do .NET Framework estão enfrentando. É expresso em FileLoadException. Antes de passar para o carregamento restrito da montagem, deixe-me lembrá-lo de algumas coisas básicas.

Ao criar um aplicativo .NET, você acaba com algum artefato, geralmente localizado na Lixeira / Depuração ou na Lixeira / Liberação, e contém um determinado conjunto de assemblies de montagem e arquivos de configuração. Assemblies se referem um ao outro por nome, nome do assembly. É importante entender que os links de montagem estão localizados diretamente na montagem que faz referência a essa montagem; não há arquivos de configuração mágica nos quais as referências de montagem são gravadas. Embora possa parecer que esses arquivos existem. As referências estão nas próprias assembléias na forma binária.

No .NET, existe um processo de resolução de montagem - é quando a definição de montagem já é convertida em uma montagem real, que está em disco ou carregada em algum lugar da memória. A resolução de montagem é realizada duas vezes: no estágio de construção, quando você tem referências em * .csproj, e em tempo de execução, quando você tem referências dentro dos assemblies, e por algumas regras eles se transformam em assemblies que podem ser baixados.

// Nome simples
MyAssembly, Versão = 6.0.0.0,
Cultura = neutra, PublicKeyToken = null

// Nome forte
Newtonsoft.Json, Versão = 6.0.0.0,
Cultura = neutra, PublicKeyToken = 30ad4fe6b2a6aeed // PublicKey


Vamos para o problema. Nome da montagem, existem dois tipos principais. O primeiro tipo de nome de montagem é Nome simples. Eles são fáceis de identificar pelo fato de terem PublicKeyToken = null. Há um nome Forte, é fácil identificá-los pelo fato de que o PublicKeyToken não é nulo, mas sim algum valor.



Vamos dar um exemplo. Temos um programa que depende da biblioteca com os utilitários do MyUtils, e a versão do MyUtils é 9.0.0.0. O mesmo programa possui um link para outra biblioteca. Essa biblioteca também deseja usar o MyUtils, mas a versão 6.0.0.0. MyUtils versão 9.0.0.0 e versão 6.0.0.0 têm PublicKeyToken = null, ou seja, eles têm um nome simples. Qual versão cairá no artefato binário, 6.0.0.0 ou 9.0.0.0? 9ª versão. O MyLibrary pode usar o MyUtils versão 9.0.0.0, que entrou no artefato binário?



De fato, pode, porque MyUtils tem um nome Simples e, portanto, o carregamento de montagem Rigoroso não existe para ele.



Outro exemplo. Em vez de MyUtils, temos uma biblioteca completa do NuGet, que tem um nome Forte. A maioria das bibliotecas do NuGet tem um nome Forte.



No estágio de compilação, a versão 9.0.0.0 é copiada para o BIN, mas em tempo de execução obtemos o famoso FileLoadException. Para que MyLibrary, que deseja a versão 6.0.0.0 Newtonsoft.Json, possa usar a versão 9.0.0.0, você deve escrever o redirecionamento Binding App.config.

Redirecionamentos de ligação





Redirecionando versões de assembly

Declara que um assembly com esse nome e publicKeyToken deve ser redirecionado de um intervalo de versões para um intervalo de versões. Parece ser um registro muito simples, mas, no entanto, está localizado aqui App.config, mas pode estar em outros arquivos. Há um arquivo machine.configdentro do .NET Framework, dentro do tempo de execução, no qual é definido um conjunto padrão de redirecionamentos, que podem diferir de versão para versão do .NET Framework. Pode acontecer que no 4.7.1 nada funcione para você, mas no 4.7.2 ele já funciona, ou vice-versa. Você precisa ter em mente que os redirecionamentos podem vir não apenas do seu .App.confige isso deve ser levado em consideração durante a depuração.

Simplificamos a escrita de redirecionamentos


Ninguém quer escrever redirecionamentos de encadernação com as mãos. Vamos dar esta tarefa ao MSBuild!



Como habilitar e desabilitar o redirecionamento automático de ligação

Algumas dicas sobre como simplificar o trabalho com o redirecionamento de ligação. Dica um: Habilite a geração automática de redirecionamento de ligação no MSBuild. Ativado por propriedade em *.csproj. Ao criar um projeto, ele cairá em um artefato binário App.config, que indica redirecionamentos para versões de bibliotecas que estão no mesmo artefato. Isso funciona apenas para aplicativos em execução, aplicativo de console, WinExe. Para bibliotecas, isso não funciona, porque para bibliotecasApp.configna maioria das vezes, simplesmente não é relevante, porque é relevante para um aplicativo que inicia e carrega os próprios conjuntos. Se você fez uma configuração para a biblioteca, no aplicativo algumas dependências também podem diferir daquelas que estavam ao criar a biblioteca, e acontece que a configuração da biblioteca não faz muito sentido. No entanto, às vezes para configurações de bibliotecas ainda fazem sentido.



A situação quando escrevemos testes. Os testes geralmente são encontrados na ClassLibrary e também precisam de redirecionamentos. As estruturas de teste são capazes de reconhecer que a biblioteca com testes possui uma configuração de dll e trocar os redirecionamentos neles pelo código dos testes. Você pode gerar esses redirecionamentos automaticamente. Se tivermos um formato antigo*.csproj, não no estilo SDK, você pode seguir o caminho simples, alterar o OutputType para Exe e adicionar um ponto de entrada vazio, isso forçará o MSBuild a gerar redirecionamentos. Você pode ir para o outro lado e usar o hack. Você pode adicionar outra propriedade a *.csproj, o que faz o MSBuild considerar que, para esse OutputType, você ainda precisa gerar redirecionamentos de ligação. Esse método, embora pareça um hack, permitirá gerar redirecionamentos para bibliotecas que não podem ser refeitas no Exe e para outros tipos de projetos (exceto testes).

Para o novo formato, os *.csprojpróprios redirecionamentos serão gerados se você usar o Microsoft.NET.Test.Sdk moderno.

Terceira dica: não use a geração de redirecionamento Binding com o NuGet. O NuGet tem a capacidade de gerar redirecionamento de ligação para bibliotecas que passam de pacotes para as versões mais recentes, mas essa não é a melhor opção. Todos esses redirecionamentos precisarão ser adicionados App.confige confirmados e, se você gerar redirecionamentos usando o MSBuild, os redirecionamentos serão gerados durante a compilação. Se você os confirmar, pode haver conflitos de mesclagem. Você mesmo pode simplesmente esquecer de atualizar o redirecionamento de Ligação no arquivo e, se eles forem gerados durante a compilação, você não esquecerá.



Resolver referência de montagem
gerar redirecionamentos de encadernação

Lição de casa para quem quer entender melhor como a geração de redirecionamentos Binding funciona: descubra como funciona, veja isso no código. Vá para o diretório .NET, esbarre em qualquer lugar com a propriedade name, que é usada para habilitar a geração. Geralmente, essa é uma abordagem comum; se houver alguma propriedade estranha para o MSBuild, você poderá aproveitar o uso. Felizmente, a propriedade geralmente é usada nas configurações XML, e você pode facilmente encontrar seu uso.

Se você examinar o que há nesses destinos XML, verá que essa propriedade dispara duas tarefas do MSBuild. A primeira tarefa é chamada ResolveAssemblyReferencese gera um conjunto de redirecionamentos que são gravados nos arquivos. A segunda tarefa GenerateBindingRedirectsgrava os resultados da primeira tarefa emApp.config. Existe uma lógica XML que corrige levemente a operação da primeira tarefa e remove alguns redirecionamentos desnecessários ou adiciona novos.

Alternativa às configurações XML


Nem sempre é conveniente manter os redirecionamentos na configuração XML. Podemos ter uma situação em que o aplicativo baixa o plug-in, e esse plug-in usa outras bibliotecas que exigem redirecionamentos. Nesse caso, podemos não estar cientes do conjunto de redirecionamentos de que precisamos ou talvez não desejemos gerar XML. Em tal situação, podemos criar um AppDomain e, quando ele é criado, ainda transferir para onde o XML com os redirecionamentos necessários está localizado. Também podemos lidar com erros de carregamento de montagem diretamente no tempo de execução. O Rantime .NET oferece essa oportunidade.

AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) => 
{ 
   var name = eventArgs.Name; 
   var requestingAssembly = eventArgs.RequestingAssembly; 
   
   return Assembly.LoadFrom(...); // PublicKeyToken should be equal
};


Tem um evento, é chamado CurrentDomain.AssemblyResolve. Ao assinar este evento, receberemos erros sobre todos os downloads de montagem com falha. Obtemos o nome da montagem que não foi carregada e obtemos a montagem que solicitou o carregamento da primeira montagem. Aqui, podemos carregar manualmente o assembly do lugar certo, por exemplo, descartando a versão, apenas retirando-a do arquivo e retornando esse evento do manipulador. Ou retorne null se não tivermos o que retornar, se não pudermos carregar a montagem. PublicKeyToken deve ser o mesmo; assemblies com diferentes PublicKeyToken não são de forma alguma amigos um do outro.



Este evento se aplica a apenas um domínio de aplicativo. Se nosso plugin criar um AppDomain dentro de si, esse redirecionamento no tempo de execução não funcionará neles. De alguma forma, você precisa se inscrever neste evento em todo o AppDomain que o plug-in criou. Podemos fazer isso usando o AppDomainManager.

AppDomainManager é um assembly separado que contém uma classe que implementa uma interface específica e um dos métodos dessa interface permitirá que você inicialize qualquer novo AppDomain criado no aplicativo. Depois que o AppDomain é criado, esse método será chamado. Nele você pode se inscrever neste evento.

Carregamento rigoroso de montagem e .NET Core


No .NET Core, não há problema chamado "Carregamento estrito de assembly", devido ao fato de os assemblies assinados exigirem exatamente a versão solicitada. Há outro requisito. Para todos os assemblies, independentemente de serem ou não assinados pelo nome Strong, é verificado se a versão que foi carregada em tempo de execução é maior ou igual à anterior. Se estivermos na situação de um aplicativo com plug-ins, é possível que o plug-in tenha sido criado, por exemplo, a partir de uma nova versão do SDK, e o aplicativo para o qual ele é baixado use a versão antiga do SDK até o momento e, em vez de desmoronar, também podemos assinar este evento, mas já no .NET Core, e também carregar o assembly que temos. Podemos escrever este código:
AppDomain.CurrentDomain.AssemblyResolve += (s, eventArgs) => 
{ 
     CheckForRecursion(); 
     var name = eventArgs.Name;
     var requestingAssembly = eventArgs.RequestingAssembly; 
    
     name.Version = new Version(0, 0); 
     
     return Assembly.Load(name); 
};


Temos o nome do assembly que não inicializou, anulamos a versão e a chamamos Assembly.Loadda mesma versão. Não haverá recursão aqui, porque eu já verifiquei a recursão.



Foi necessário fazer o download do MyUtils versão 0.0.2.0. No BIN, temos o MyUtils versão 0.0.1.0. Fizemos um redirecionamento da versão 0.0.2.0 para a versão 0.0. A versão 0.0.1.0 não será carregada conosco. Será emitida uma saída para nós, que não foi possível carregar o conjunto com a versão 0.0.2 16–1 . 2 16-1 .

new Version(0, 0) == new Version(0, 0, -1, -1) 

class Version { 
     readonly int _Build; 
     readonly int _Revision; 
     readonly int _Major; 
     readonly int _Minor; 
} 
(ushort) -1 == 65535


Na classe Versão, nem todos os componentes são obrigatórios e, em vez dos componentes opcionais –1, são armazenados, mas em algum lugar interno ocorre um estouro e os 2 16–1 são obtidos . Se estiver interessado, você pode tentar encontrar exatamente onde ocorre o estouro.



Se você trabalha com montagens de reflexão e deseja obter todos os tipos, pode ser que nem todos os tipos possam obter o seu método GetTypes. Um assembly tem uma classe que herda de outra classe que está em um assembly que não está carregado.

static IEnumerable GetTypesSafe(this Assembly assembly) 
{ 
    try 
    { 
        return assembly.GetTypes(); 
    }
    catch (ReflectionTypeLoadException e) 
   { 
        return e.Types.Where(x => x != null); 
    } 
}



Nesse caso, o problema será que um ReflectionTypeLoadException será lançado. Dentro, ReflectionTypeLoadExceptionhá uma propriedade na qual existem esses tipos que ainda conseguiram ser carregados. Nem todas as bibliotecas populares levam isso em consideração. O AutoMapper, pelo menos uma de suas versões, se confrontado com ReflectionTypeLoadException, caiu, em vez de ir e escolher os tipos por dentro da exceção.

Nomeação forte


Montagens com nome forte

Vamos falar sobre o que causa o carregamento restrito de montagem, este é o nome Forte.
Nome forte é a assinatura do assembly por alguma chave privada usando criptografia assimétrica. PublicKeyToken é o hash da chave pública deste assembly.

A nomeação forte permite distinguir entre diferentes montagens que têm o mesmo nome. Por exemplo, MyUtils não é um nome exclusivo, pode haver vários assemblies com o mesmo nome, mas se você assinar Nome forte, eles terão PublicKeyToken diferente e podemos distingui-los dessa maneira. O nome forte é necessário para alguns cenários de carregamento de montagem.

Por exemplo, para instalar um assembly no Global Assembly Cache ou fazer o download de várias versões lado a lado de uma só vez. Mais importante, assemblies nomeados fortes só podem fazer referência a outros assemblies nomeados fortes. Como alguns usuários desejam assinar suas construções com o nome Strong, os desenvolvedores da biblioteca também assinam suas bibliotecas, para facilitar a instalação dos usuários, para que os usuários não precisem assinar novamente essas bibliotecas.

Nome forte: Legado?


Nomeação forte e bibliotecas .NET A

Microsoft diz explicitamente no MSDN que você não deve usar o nome Forte para fins de segurança, pois eles fornecem apenas para distinguir diferentes assemblies com o mesmo nome. A chave de montagem não pode ser alterada de forma alguma; se você a alterou, quebrará os redirecionamentos para todos os seus usuários. Se houver uma parte privada da chave do nome Forte vazada para acesso público, não será possível retirar esta assinatura de nenhuma maneira. O formato de arquivo SNK no qual o nome Strong está localizado não oferece essa oportunidade e outros formatos para armazenar chaves contêm pelo menos um link para a Lista de revogação de certificados CRL, pelo qual é possível entender que esse certificado não é mais válido. Não há nada parecido no SNK.

O guia de código aberto tem as seguintes recomendações. Em primeiro lugar, adicionalmente, para fins de segurança, use outras tecnologias. Em segundo lugar, se você tem uma biblioteca de código aberto, geralmente é sugerido que você comprometa a parte privada da chave no repositório, para que seja mais fácil para as pessoas bifurcarem sua biblioteca, reconstruí-la e colocá-la em um aplicativo pronto. Em terceiro lugar, nunca mude o nome Forte. Muito destrutivo. Apesar de ser muito destrutivo e estar escrito sobre isso no guia de código-fonte aberto, a Microsoft às vezes tem problemas com suas próprias bibliotecas.



Há uma biblioteca chamada System.Reactive. Anteriormente, esses eram vários pacotes NuGet, um deles é o Rx-Linq. Este é apenas um exemplo, o mesmo para o restante dos pacotes. Na segunda versão, foi assinado com uma chave da Microsoft. Na terceira versão, ele foi para o repositório no projeto github.com/dotnet e começou a ter uma assinatura do .NET Foundation. A biblioteca, de fato, mudou o nome Strong. O pacote NuGet foi renomeado, mas o assembly é chamado dentro da mesma exatamente como antes. Como redirecionar da segunda versão para a terceira? Este redirecionamento não pode ser feito.

Validação de nome forte


Como desativar o recurso de desvio de nome forte

Outro argumento de que o nome Strong já é algo do passado e permanece puramente formal é que eles não são validados. Temos um assembly assinado e queremos corrigir algum tipo de bug, mas não temos acesso às fontes. Podemos usar o dnSpy - este é um utilitário que permite descompilar e consertar os assemblies já compilados. Tudo vai funcionar para nós. Como, por padrão, o desvio de validação de nome forte está ativado, ou seja, ele apenas verifica se o PublicKeyToken é igual e se a integridade da assinatura em si não é verificada. Pode haver estudos ambientais nos quais a assinatura ainda é verificada, e aqui um exemplo vívido é o IIS. A integridade da assinatura é verificada no IIS (o desvio de validação de nome forte está desativado por padrão) e tudo será interrompido se editarmos o assembly assinado.

Adição:Você pode desativar a verificação de assinatura para a montagem usando sinal público. Com ele, apenas a chave pública é usada para assinatura, o que garante a segurança do nome do assembly. As chaves públicas usadas pela Microsoft estão publicadas aqui .
No Rider, o sinal público pode ser ativado nas propriedades do projeto.





Quando alterar as versões do conjunto de arquivos

O guia de código-fonte aberto também oferece algumas políticas de controle de versão, cujo objetivo é reduzir o número de redirecionamentos de ligação necessários e as alterações para os usuários no NET Framework. Esta política de controle de versão é que não devemos alterar a versão do assembly constantemente. Obviamente, isso pode levar a problemas com a instalação no GAC, para que a imagem nativa instalada não corresponda ao assembly e você precise executar a compilação JIT novamente, mas, na minha opinião, isso é menos grave que os problemas com o controle de versão. No caso do CrossGen, os assemblies nativos não são instalados globalmente - não haverá problemas.

Por exemplo, o pacote NuGet Newtonsoft.Json, possui várias versões: 12.0.1, 12.0.2 e assim por diante - todos esses pacotes têm um assembly com a versão 12.0.0.0. A recomendação é que a versão do assembly seja atualizada quando uma versão principal do pacote NuGet for alterada.

achados


Siga as dicas para o .NET Framework: gere redirecionamentos manualmente e tente usar a mesma versão das dependências em todos os projetos em sua solução. Isso deve minimizar significativamente o número de redirecionamentos. Você precisará de nomes fortes apenas se tiver um cenário específico de carregamento de construção, onde for necessário, ou estiver desenvolvendo uma biblioteca e desejar simplificar a vida dos usuários que realmente precisam de nomes fortes. Não mude Nome forte.

.NET Standard


Passamos para o .NET Standard. Está bastante relacionado à versão infernal no .NET Framework. O .NET Standard é uma ferramenta para escrever bibliotecas compatíveis com várias implementações da plataforma .NET. As implementações referem-se ao .NET Framework, .NET Core, Mono, Unity e Xamarin.



* Link para documentação

Esta é a tabela de suporte do .NET Standard para várias versões de diferentes versões dos tempos de execução. E aqui podemos ver que o .NET Framework não oferece suporte à .NET Standard versão 2.1. O lançamento do .NET Framework, que dará suporte ao .NET Standard 2.1 e posterior, ainda não está planejado. Se você estiver desenvolvendo uma biblioteca e desejar que ela funcione para usuários no .NET Framework, será necessário ter um destino para o .NET Standard 2.0. Além do fato de o .NET Framework não suportar a versão mais recente do .NET Standard, vamos prestar atenção ao asterisco. O .NET Framework 4.6.1 oferece suporte ao .NET Standard 2.0, mas com um asterisco. Existe uma nota de rodapé diretamente na documentação, onde eu consegui essa tabela.



Considere um projeto de exemplo. Um aplicativo no .NET Framework que possui uma dependência direcionada ao .NET Standard. Algo assim: ConsoleApp e ClassLibrary. Biblioteca de destino .NET Standard. Quando montarmos esse projeto, será assim em nosso BIN.



Teremos uma centena de DLLs, das quais apenas uma relacionada ao aplicativo, tudo o mais veio para dar suporte ao .NET Standard. O fato é que o .NET Standard 2.0 apareceu mais tarde que o .NET Framework 4.6.1, mas ao mesmo tempo se mostrou compatível com a API, e os desenvolvedores decidiram adicionar o suporte ao Standard 2.0 ao .NET 4.6.1. Fizemos isso não de forma nativa (por inclusão netstandard.dllno próprio tempo de execução), mas de maneira que o .NET Standard * .dll e todas as outras fachadas de montagem sejam colocadas diretamente no BIN.



Se observarmos as dependências da versão do .NET Framework que visamos e o número de bibliotecas que caíram no BIN, veremos que não há muitas delas na 4.7.1 e, desde a 4.7.2, não há bibliotecas adicionais, e .NET O padrão é suportado lá nativamente.



Este é um tweet de um dos desenvolvedores do .NET, que descreve esse problema e recomenda o uso do .NET Framework versão 4.7.2 se tivermos bibliotecas do .NET Standard. Nem mesmo com a versão 2.0 aqui, mas com a versão 1.5.

achados


Se possível, aumente o Target Framework em seu projeto para pelo menos 4.7.1, preferencialmente 4.7.2. Se você estiver desenvolvendo uma biblioteca para facilitar a vida dos usuários da biblioteca, faça um Destino separado para o .NET Framework, evitará um grande número de dlls que podem entrar em conflito com alguma coisa.

.NET Core


Vamos começar com uma teoria geral. Discutiremos como lançamos o JetBrains Rider no .NET Core e por que deveríamos falar sobre isso. O Rider é um projeto muito grande, possui uma enorme solução corporativa com um grande número de projetos diferentes, um sistema complexo de dependências, você não pode simplesmente pegá-lo e migrar para outro tempo de execução ao mesmo tempo. Para fazer isso, temos que usar alguns hacks, que também serão analisados.

Aplicativo .NET Core


Como é um aplicativo típico do .NET Core? Depende de como exatamente é implantado, para o que está indo. Nós podemos ter vários cenários. A primeira é uma implantação dependente da estrutura. É o mesmo que no .NET Framework quando o aplicativo usa o tempo de execução pré-instalado no computador. Pode ser uma implantação autônoma, é quando o aplicativo carrega um tempo de execução. E pode haver uma implantação de arquivo único, é quando obtemos um arquivo exe, mas no caso do .NET Core dentro desse arquivo exe, há um artefato do aplicativo Autônomo, esse é um arquivo de extração automática.



Consideraremos apenas a implantação dependente da estrutura. Temos uma DLL com o aplicativo, existem dois arquivos de configuração, o primeiro dos quais é necessário, este runtimeconfig.jsonedeps.json. A partir do .NET Core 3.0, é gerado um arquivo exe, necessário para tornar o aplicativo mais conveniente para a execução, para que você não precise digitar o comando .NET se estiver no Windows. As dependências se enquadram nesse artefato, começando no .NET Core 3.0, no .NET Core 2.1 em que você precisa publicar ou usar outra propriedade *.csproj.

Estruturas compartilhadas, .runtimeconfig.json





.runtimeconfig.jsoncontém as configurações de tempo de execução necessárias para executá-lo. Indica em qual estrutura compartilhada o aplicativo será iniciado e se parece com isso. Indicamos que o aplicativo será executado sob "Microsoft.NETCore.App" versão 3.0.0; pode haver outra Estrutura Compartilhada. Outras configurações também podem estar aqui. Por exemplo, você pode ativar o coletor de lixo do servidor.



.runtimeconfig.jsongerado durante a montagem do projeto. E se queremos incluir o GC do servidor, precisamos modificar esse arquivo com antecedência, mesmo antes de montar o projeto ou adicioná-lo manualmente. Você pode adicionar suas configurações aqui assim. Podemos incluir propriedade em *.csproj, se essa propriedade for fornecida por desenvolvedores .NET, ou se a propriedade não for fornecida, podemos criar um arquivo chamadoruntimeconfig.template.jsone escreva as configurações necessárias aqui. Durante a montagem, outras configurações necessárias serão adicionadas a este modelo, por exemplo, a mesma Estrutura Compartilhada.



A Estrutura Compartilhada é um conjunto de tempo de execução e bibliotecas. De fato, o mesmo que o tempo de execução do .NET Framework, que costumava ser instalado apenas uma vez na máquina e para todos era uma versão. O Shared Framework e, diferentemente de um único tempo de execução do .NET Framework, podem ser versionados, aplicativos diferentes podem usar versões diferentes dos tempos de execução instalados. Também o Shared Framework pode ser herdado. A própria estrutura compartilhada pode ser visualizada nos locais do disco geralmente instalados no sistema.



Existem várias estruturas compartilhadas padrão, por exemplo, Microsoft.NETCore.App, que executa aplicativos de console convencionais, AspNetCore.App, para aplicativos Web, e WindowsDesktop.App, a nova estrutura compartilhada no .NET Core 3, que executa aplicativos de área de trabalho. no Windows Forms e WPF. Os dois últimos Shared Framework complementam essencialmente o primeiro necessário para aplicativos de console, ou seja, eles não carregam um tempo de execução totalmente novo, mas simplesmente complementam o existente com as bibliotecas necessárias. Parece que essa herança também existe nos diretórios do Framework compartilhado runtimeconfig.jsonnos quais o Framework compartilhado base é especificado.

Manifesto de dependência ( .deps.json)



Teste padrão - .NET Core O

segundo arquivo de configuração é este .deps.json. Este arquivo contém uma descrição de todas as dependências do aplicativo ou da Estrutura Compartilhada, ou da biblioteca, as bibliotecas .deps.jsontambém o possuem. Ele contém todas as dependências, incluindo transitivas. E o comportamento do tempo de execução do .NET Core difere dependendo se .deps.jsono aplicativo o possui ou não. Caso .deps.jsoncontrário, o aplicativo poderá carregar todos os assemblies que estão em sua Estrutura Compartilhada ou em seu diretório BIN. Se houver .deps.json, a validação será ativada. Se um dos assemblies listados .deps.jsonnão estiver, o aplicativo simplesmente não será iniciado. Você verá o erro apresentado acima. Se o aplicativo tentar carregar algum assembly em tempo de execução, o que.deps.json se, por exemplo, usando métodos de carregamento de montagem ou durante o processo de resolução de montagens, você verá um erro muito semelhante ao carregamento restrito de montagem.

Jetbrains rider


Rider é um IDE .NET. Nem todo mundo sabe que o Rider é um IDE que consiste em um front-end baseado no IntelliJ IDEA e escrito em Java e Kotlin, e um back-end. O back-end é essencialmente R #, que pode se comunicar com o IntelliJ IDEA. Esse back-end é um aplicativo .NET multiplataforma agora.
Para onde ele funciona? O Windows usa o .NET Framework, que está instalado no computador do usuário. Em outros sistemas de informação, no Linux e Mac, o Mono é usado.

Essa não é uma solução ideal quando há tempos de execução diferentes em todos os lugares, e eu quero ir para o próximo estado para que o Rider seja executado no .NET Core. Para melhorar o desempenho, porque no .NET Core todos os recursos mais recentes estão associados a isso. Para reduzir o consumo de memória. Agora, há um problema com o modo como o Mono trabalha com memória.

A mudança para o .NET Core permitirá que você abandone as tecnologias herdadas e não suportadas e corrija algumas correções para os problemas encontrados no tempo de execução. A mudança para o .NET Core permitirá que você controle a versão do tempo de execução, ou seja, o Rider não será mais executado no .NET Framework instalado no computador do usuário, mas em uma versão específica do .NET Core, que pode ser banida, na forma de uma implantação autônoma. A transição para o .NET Core eventualmente permitirá o uso de novas APIs importadas especificamente no Core.

Agora, o objetivo é lançar um protótipo, iniciá-lo, apenas para verificar como ele funcionará, quais são os possíveis pontos de falha, quais componentes terão que ser reescritos novamente, o que exigirá processamento global.

Recursos que dificultam a tradução do Rider para o .NET Core


O Visual Studio, mesmo que o R # não esteja instalado, trava de falta de memória em soluções grandes, nas quais existem projetos com * .csproj no estilo SDK . O estilo SDK * .csproj é uma das principais condições para uma realocação completa do .NET Core.

Isso é um problema porque o Rider é baseado em R #, eles vivem no mesmo repositório, os desenvolvedores de R # desejam usar o Visual Studio para desenvolver seu próprio produto em seu produto, a fim de torná-lo um alimento. No R #, existem links para bibliotecas específicas para a estrutura com a qual você precisa fazer algo. No Windows, podemos usar o Framework para aplicativos de desktop e, no Linux e Mac, o Mock já é usado para bibliotecas do Windows com funcionalidade mínima.

Decisão


Decidimos permanecer nos antigos por enquanto *.csproj, montando sob o Framework completo, mas como os assemblies do Framework e Core são compatíveis com binários, execute-os no Core. Não usamos recursos incompatíveis, adicionamos todos os arquivos de configuração necessários manualmente e baixamos versões especiais de dependências para o .NET Core, se houver.

Para quais hacks você teve que ir?


Um truque: queremos chamar um método que esteja disponível apenas no Framework, por exemplo, esse método é necessário em R #, mas não no Core. O problema é que, se não houver método, o método que o chama durante a compilação JIT cairá mais cedo MissingMethodException. Ou seja, um método que não existe estragou o método que o chama.

static void Method() { 
  if (NetFramework) 
     CallNETFrameworkOnlyMethod();

  ... 
} 
[MethodImpl(MethodImplOptions.NoInlining)] 
static void CallNETFrameworkOnlyMethod() { 
  NETFrameworkOnlyMethod(); 
}


A solução está aqui: fazemos chamadas para métodos incompatíveis em métodos separados. Há mais um problema: esse método pode ficar embutido, portanto, marcamos com um atributo NoInlining.

Hack número dois: precisamos ser capazes de carregar assemblies em caminhos relativos. Temos um assembly para o Framework, existe uma versão especial para o .NET Core. Como fazemos o download da versão do .NET Core para o .NET Core?



Eles vão nos ajudar .deps.json. Vamos dar uma olhada na .deps.jsonbiblioteca System.Diagnostics.PerformanceCounter. Tal biblioteca é notável em termos de.deps.json. Possui uma seção de tempo de execução, na qual é indicada uma versão da biblioteca com seu caminho relativo. Nesta biblioteca, o assembly será carregado em todos os tempos de execução e apenas lança as execuções. Se, por exemplo, ele for carregado no Linux, o PerformanceCounter não funcionará no design do Linux e uma PlatformNotSupportedException voará de lá. Há também .deps.jsonuma seção runtimeTargets e aqui já está indicada a versão deste assembly especificamente para Windows, onde o PerformanceCounter deve funcionar.

Se pegarmos a seção de tempo de execução e escrevermos nela o caminho relativo para a biblioteca que queremos carregar, isso não nos ajudará. A seção de tempo de execução realmente define o caminho relativo dentro do pacote NuGet, e não em relação ao BIN. Se procurarmos por esse assembly no BIN, somente o nome do arquivo será usado a partir daí. A seção runtimeTargets já contém um caminho relativo honesto, um caminho honesto em relação ao BIN. Vamos prescrever um caminho relativo para nossos assemblies na seção runtimeTargets. Em vez do identificador de tempo de execução, que é "win" aqui, podemos usar outro que gostamos. Por exemplo, escreveremos o identificador de tempo de execução “any” e esse assembly será carregado geralmente em todas as plataformas. Ou então, escreveremos “unix”, e ele inicializará no Linux e no Mac, e assim por diante.

Próximo corte: queremos fazer o download no Linux e no Mac Mock para criar o WindowsBase. O problema é que o assembly chamado WindowsBase já está presente na Estrutura Compartilhada Microsoft.NETCore.App, mesmo que não estejamos no Windows. No Windows Shared Framework, o Microsoft.WindowsDesktop.AppWindowsBase redefine a versão em que está NETCore.App. Vamos dar uma olhada .deps.jsonnesses Framework, mais precisamente nas seções que descrevem o WindowsBase.



Aqui está a diferença:



se alguma biblioteca entra em conflito e está presente em várias .deps.json, o máximo delas é selecionado para o par que consiste em assemblyVersione fileVersion. O guia .NET diz que fileVersioné necessário apenas mostrá-lo no Windows Explorer, mas não é, ele se encaixa.deps.json. Este é o único caso que eu conheço quando a versão prescrita em .deps.json, assemblyVersione fileVersion, é realmente usada. Em todos os outros casos, vi um comportamento em que, independentemente das versões .deps.jsonescritas, o assembly continuava carregando de qualquer maneira.



Quarto hack. Tarefa: temos um arquivo .deps.json para os dois hacks anteriores e precisamos dele apenas para dependências específicas. Como eles são .deps.jsongerados no modo semi-manual, temos um script que, de acordo com alguma descrição do que deve chegar lá, o gera durante a compilação, queremos manter isso o .deps.jsonmínimo possível para que possamos entender o que está nele. Queremos desativar a validação e permitir o download de montagens que estão no BIN, mas que não estão descritas em .deps.json.

Solução: ative a configuração personalizada em runtimeconfig. Essa configuração é realmente necessária para compatibilidade com versões anteriores do .NET Core 1.0.

achados


Então, .runtime.jsone .deps.jsonno .NET Core - esses são tipos de análogos App.config. App.configpermitem fazer o mesmo, por exemplo, carregar montagens de maneiras relativas. Usando .deps.json, reescrevendo-o manualmente, você pode personalizar o carregamento de assemblies no .NET Core, se você tiver um cenário muito complexo.

Depurar downloads de assembly


Eu falei sobre alguns tipos de problemas, então você precisa ser capaz de depurar problemas ao carregar assemblies. O que pode ajudar nisso? Primeiro, os tempos de execução escrevem logs sobre como eles carregam assemblies. Em segundo lugar, você pode olhar mais de perto as execuções que voam para você. Você também pode se concentrar em eventos de tempo de execução.

Logs de fusão





De volta ao básico: Usando o Fusion Log Viewer para depurar erros obscuros
Fusion

O mecanismo para carregar assemblies no .NET Framework é chamado Fusion e ele sabe como registrar o que fez no disco. Para habilitar o log, você precisa adicionar configurações especiais ao registro. Isso não é muito conveniente, por isso faz sentido usar utilitários, como o Fusion Log Viewer e o Fusion ++. O Fusion Log Viewer é um utilitário padrão que acompanha o Visual Studio e pode ser iniciado na linha de comando do Visual Studio, Prompt de Comando do Visual Studio Developer. O Fusion ++ é um análogo de código aberto desta ferramenta com uma interface melhor.



O Fusion Log Viewer fica assim. Isso é pior que o WinDbg porque essa janela nem se estende. No entanto, você pode perfurar as marcas de verificação aqui, embora nem sempre seja óbvio qual conjunto de marcas de verificação está correto.



O Fusion ++ possui um botão "Iniciar registro" e, em seguida, o botão "Interromper registro" é exibido. Nele, você pode ver todos os registros sobre o carregamento de montagens, ler os logs sobre o que exatamente estava acontecendo. Esses logs são parecidos com isso de maneira concisa.



Esta é uma isenção do carregamento rigoroso da montagem. Se olharmos para os logs do Fusion, veremos que precisávamos baixar a versão 9.0.0.0 depois de processarmos todas as configurações. Encontramos um arquivo no qual suspeita-se que temos a montagem de que precisamos. Vimos que a versão 6.0.0.0 está neste arquivo. Temos um aviso de que comparamos os nomes completos das assembléias e elas diferem na versão principal. E, em seguida, ocorreu um erro - incompatibilidade de versão.

Eventos de tempo de execução





Registrando eventos de tempo de execução

No Mono, você pode ativar o registro usando variáveis ​​de ambiente, e os registros serão gravados em stdoute stderr. Não é tão conveniente, mas a solução está funcionando.



Análise padrão -
Documentação do .NET Core / documentos de design / rastreamento de host.O

.NET Core também possui uma variável de ambiente especial COREHOST_TRACEque inclui o logon stderr. Com o .NET Core 3.0, você pode gravar logs em um arquivo, especificando o caminho para ele em uma variável COREHOST_TRACEFILE.


Há um evento que é acionado quando os conjuntos falham ao carregar. Este é um evento AssembleResolve. Há um segundo evento útil, esse FirstChanceException. Você pode se inscrever e obter um erro sobre o carregamento de assemblies, mesmo que alguém tenha escrito try..catch e tenha perdido todas as execuções no local em queFileLoadExceptionocorreu. Se o aplicativo já tiver sido compilado, você poderá iniciá-lo perfviewe monitorar as execuções do .NET, e poderá encontrar as relacionadas ao download de arquivos.

achados


Transfira o trabalho para ferramentas, ferramentas de desenvolvimento, para um IDE, para MSBuild, o que permite gerar redirecionamentos. Você pode mudar para o .NET Core, depois esquecerá o que é o Strict Assembly Loading, e poderá usar a nova API exatamente como queremos alcançar no Rider. Se você conectar a biblioteca .NET Standard, aumente a versão de destino do .NET Framework para pelo menos 4.7.1. Se você parece estar em uma situação sem esperança, procure por hacks, use-os ou crie seus próprios hacks para situações sem esperança. E arme-se com ferramentas de depuração.

Eu recomendo fortemente que você leia os seguintes links:



DotNext 2020 Piter . , 8 JUG Ru Group.

All Articles