.NET: Traitement de la dépendance

Qui n'a pas rencontré de problèmes dus à la redirection d'assembly? Très probablement, tous ceux qui ont développé une application relativement volumineuse seront tôt ou tard confrontés à ce problème.

Maintenant, je travaille chez JetBrains, dans le projet JetBrains Rider, et je suis impliqué dans la tâche de migration de Rider vers .NET Core. Auparavant engagé dans une infrastructure partagée dans Circuit, une plateforme d'hébergement d'applications basée sur le cloud.



Sous la cinématique se trouve la transcription de mon rapport de la conférence DotNext 2019 à Moscou, où j'ai parlé des difficultés lors de l'utilisation d'assemblys dans .NET et montré avec des exemples pratiques ce qui se passe et comment y faire face.


Dans tous les projets où je travaillais en tant que développeur .NET, j'ai dû faire face à divers problèmes de connexion des dépendances et de chargement des assemblys. Nous en parlerons.

Structure du poste:


  1. Problèmes de dépendance
  2. Chargement rigoureux du gréement

  3. .NET Core

  4. Téléchargements d'assemblages de débogage


Quels sont les problèmes de dépendance?


Lorsqu'ils ont commencé à développer le .NET Framework au début des années 2000, le problème de l'enfer de la dépendance était déjà connu, lorsque dans toutes les bibliothèques les développeurs autorisent la rupture des modifications, et ces bibliothèques deviennent incompatibles avec le code déjà compilé. Comment résoudre un tel problème? La première solution est évidente. Maintenez toujours la compatibilité descendante. Bien sûr, ce n'est pas très réaliste, car casser le changement est très facile à mettre en code. Par exemple:



rupture des modifications et bibliothèques .NET

Ceci est un exemple spécifique à .NET. Nous avons une méthode et nous avons décidé d'y ajouter un paramètre avec une valeur par défaut. Le code continuera à être compilé si nous le réassemblons, mais binaire ce sera deux méthodes complètement différentes: une méthode n'a aucun argument, la deuxième méthode a un argument. Si le développeur à l'intérieur de la dépendance a rompu la compatibilité descendante de cette manière, nous ne pourrons pas utiliser le code qui a été compilé avec cette dépendance sur la version précédente.

La deuxième solution aux problèmes de dépendance consiste à ajouter des versions de bibliothèques, d'assemblages - n'importe quoi. Il peut y avoir des règles de version différentes, le fait est que nous pouvons en quelque sorte distinguer différentes versions de la même bibliothèque les unes des autres, et vous pouvez comprendre si la mise à jour s'arrêtera ou non. Malheureusement, dès que nous introduisons les versions, un autre type de problème apparaît.



L'enfer des versions est l'impossibilité d'utiliser une dépendance qui est compatible binaire, mais qui a en même temps une version qui ne correspondait pas à l'exécution ou à un autre composant qui vérifie ces versions. Dans .NET, une manifestation typique de l'enfer de version est FileLoadException, bien que le fichier se trouve sur le disque, mais pour une raison quelconque, il n'est pas chargé avec runtime.



Dans .NET, les assemblys ont de nombreuses versions différentes - ils ont essayé de corriger les hells de version de différentes manières et de voir ce qui s'est passé. Nous avons un package System.Collections.Immutable. Beaucoup de gens le connaissent. Il dispose de la dernière version du package NuGet 1.6.0. Il contient une bibliothèque, un assemblage avec la version 1.2.4.0. Vous avez reçu que vous ne disposez pas d'une bibliothèque de build version 1.2.4.0. Comment comprendre qu'il se trouve dans le package NuGet 1.6.0? Ça ne sera pas facile. En plus de la version d'assemblage, cette bibliothèque a plusieurs autres versions. Par exemple, Version du fichier d'assemblage, Version des informations d'assemblage. Ce package NuGet contient en fait trois assemblys différents avec les mêmes versions (pour différentes versions de la norme .NET).

Documentation .NET
Norme Opbuild

De nombreuses documentations ont été écrites sur la façon de travailler avec des assemblys dans .NET. Il existe un guide .NET pour développer des applications modernes pour .NET prenant en compte le .NET Framework, .NET Standard, .NET Core, l'open source et tout ce qui peut être. Environ 30% du document entier est consacré au chargement des assemblages. Nous analyserons des problèmes spécifiques et des exemples pouvant survenir.

Pourquoi tout cela est-il nécessaire? Tout d'abord, pour éviter de monter sur un râteau. Deuxièmement, vous pouvez faciliter la vie des utilisateurs de vos bibliothèques car avec votre bibliothèque, ils n'auront pas les problèmes de dépendance auxquels ils sont habitués. Il vous aidera également à gérer la migration d'applications complexes vers .NET Core. Et pour couronner le tout, vous pouvez devenir un SRE, c'est un ingénieur de redirection senior (contraignant), auquel tout le monde dans l'équipe vient et demande comment écrire une autre redirection.

Assemblage strict Chargement


Le chargement d'assemblage strict est le principal problème auquel les développeurs sur .NET Framework sont confrontés. Elle s'exprime en FileLoadException. Avant de passer à l'assemblage strict se chargeant lui-même, permettez-moi de vous rappeler quelques éléments de base.

Lorsque vous créez une application .NET, vous vous retrouvez avec un artefact, qui se trouve généralement dans Bin / Debug ou Bin / Release, et contient un certain ensemble d'assemblys d'assemblage et de fichiers de configuration. Les assemblages se référeront les uns aux autres par leur nom, nom de l'assemblage. Il est important de comprendre que les liens d'assembly se trouvent directement dans l'assembly qui référence cet assembly; il n'y a pas de fichiers de configuration magiques dans lesquels les références d'assembly sont écrites. Même s'il peut vous sembler que de tels fichiers existent. Les références sont dans les assemblages eux-mêmes sous forme binaire.

Dans .NET, il existe un processus de résolution d'assembly - c'est lorsque la définition d'assembly est déjà convertie en un assembly réel, qui est sur le disque ou chargé quelque part en mémoire. La résolution d'assembly est effectuée deux fois: au stade de la construction, lorsque vous avez des références dans * .csproj, et à l'exécution, lorsque vous avez des références à l'intérieur des assemblys, et selon certaines règles, ils se transforment en assemblys qui peuvent être téléchargés.

// Nom simple
MyAssembly, Version = 6.0.0.0,
Culture = neutre, PublicKeyToken = null

// Nom fort
Newtonsoft.Json, Version = 6.0.0.0,
Culture = neutre, PublicKeyToken = 30ad4fe6b2a6aeed // PublicKey


Passons au problème. Nom de l'assemblage Il existe deux types principaux. Le premier type de nom d'assembly est le nom simple. Ils sont faciles à identifier par le fait qu'ils ont PublicKeyToken = null. Il y a un nom Strong, il est facile de les identifier par le fait que leur PublicKeyToken n'est pas nul, mais une certaine valeur.



Prenons un exemple. Nous avons un programme qui dépend de la bibliothèque avec les utilitaires MyUtils, et la version de MyUtils est 9.0.0.0. Le même programme a un lien vers une autre bibliothèque. Cette bibliothèque souhaite également utiliser MyUtils, mais la version 6.0.0.0. La version 9.0.0.0 et la version 6.0.0.0 de MyUtils ont PublicKeyToken = null, c'est-à-dire qu'elles ont un nom simple. Quelle version tombera dans l'artefact binaire, 6.0.0.0 ou 9.0.0.0? 9ème version. Ma bibliothèque peut-elle utiliser la version 9.0.0.0 de MyUtils, qui est entrée dans l'artefact binaire?



En fait, il peut, car MyUtils a un nom simple et, en conséquence, le chargement d'assembly Strict n'existe pas pour lui.



Un autre exemple. Au lieu de MyUtils, nous avons une bibliothèque complète de NuGet, qui a un nom fort. La plupart des bibliothèques de NuGet ont un nom fort.



Au stade de la construction, la version 9.0.0.0 est copiée sur BIN, mais au moment de l'exécution, nous obtenons la fameuse FileLoadException. Pour que MyLibrary, qui veut que la version 6.0.0.0 Newtonsoft.Json, puisse utiliser la version 9.0.0.0, vous devez aller écrire la redirection de liaison vers App.config.

Redirection de liaison





Redirection des versions d'assembly

Il indique qu'un assembly avec un tel nom et un tel publicKeyToken doit être redirigé d'une telle gamme de versions vers une telle gamme de versions. Il semble que ce soit un enregistrement très simple, mais il se trouve néanmoins ici App.config, mais pourrait se trouver dans d'autres fichiers. Il existe un fichier machine.configà l'intérieur du .NET Framework, à l'intérieur du runtime, dans lequel un ensemble standard de redirections est défini, qui peut différer d'une version à l'autre du .NET Framework. Il peut arriver que sur 4.7.1 rien ne fonctionne pour vous, mais sur 4.7.2 cela fonctionne déjà, ou vice versa. Vous devez garder à l'esprit que les redirections peuvent provenir non seulement de la vôtre .App.config, et cela doit être pris en compte lors du débogage.

Nous simplifions l'écriture des redirections


Personne ne veut écrire des redirections de liaison avec leurs mains. Donnons cette tâche à MSBuild!



Comment activer et désactiver la redirection de liaison automatique

Quelques conseils pour simplifier l'utilisation de la redirection de liaison. Astuce 1: Activez la génération automatique de redirection de liaison dans MSBuild. Allumé par la propriété en *.csproj. Lors de la construction d'un projet, il entrera dans un artefact binaire App.config, ce qui indique des redirections vers des versions de bibliothèques qui se trouvent dans le même artefact. Cela ne fonctionne que pour exécuter des applications, une application console, WinExe. Pour les bibliothèques, cela ne fonctionne pas, car pour les bibliothèquesApp.configle plus souvent, il n'est tout simplement pas pertinent, car il est pertinent pour une application qui lance et charge les assemblages elle-même. Si vous avez fait une configuration pour la bibliothèque, dans l'application, certaines dépendances peuvent également différer de celles qui étaient lors de la construction de la bibliothèque, et il s'avère que la configuration de la bibliothèque n'a pas beaucoup de sens. Néanmoins, parfois, pour les bibliothèques, les configurations ont toujours un sens.



La situation lorsque nous écrivons des tests. Les tests se trouvent généralement dans ClassLibrary et ils ont également besoin de redirections. Les frameworks de test sont capables de reconnaître que la bibliothèque avec les tests a une configuration dll et d'échanger les redirections qui s'y trouvent pour le code des tests. Vous pouvez générer ces redirections automatiquement. Si nous avons un ancien format*.csproj, pas de style SDK, vous pouvez utiliser la méthode simple, changer le OutputType en Exe et ajouter un point d'entrée vide, cela forcera MSBuild à générer des redirections. Vous pouvez aller dans l'autre sens et utiliser le hack. Vous pouvez ajouter une autre propriété à *.csproj, ce qui fait que MSBuild considère que pour ce OutputType, vous devez toujours générer des redirections de liaison. Cette méthode, bien qu'elle ressemble à un hack, vous permettra de générer des redirections pour les bibliothèques qui ne peuvent pas être refaites dans Exe, et pour d'autres types de projets (à l'exception des tests).

Pour le nouveau format, les *.csprojredirections seront générées elles-mêmes si vous utilisez Microsoft.NET.Test.Sdk moderne.

Troisième conseil: n'utilisez pas la génération de redirection de liaison avec NuGet. NuGet a la capacité de générer une redirection de liaison pour les bibliothèques qui passent des packages aux dernières versions, mais ce n'est pas la meilleure option. Toutes ces redirections devront être ajoutées App.configet validées, et si vous générez des redirections à l'aide de MSBuild, les redirections sont générées lors de la génération. Si vous les validez, vous pouvez avoir des conflits de fusion. Vous-même pouvez simplement oublier de mettre à jour la redirection de liaison dans le fichier, et si elles sont générées pendant la construction, vous n'oublierez pas.



Résoudre la référence d'
assembly Générer des redirections de liaison

Devoirs pour ceux qui veulent mieux comprendre comment fonctionne la génération de redirections de liaison: découvrez comment cela fonctionne, voyez ceci dans le code. Allez dans le répertoire .NET, allez bump partout avec la propriété name, qui est utilisée pour activer la génération. Il s'agit généralement d'une telle approche courante, s'il existe une propriété étrange pour MSBuild, vous pouvez profiter de son utilisation. Heureusement, la propriété est généralement utilisée dans les configurations XML, et vous pouvez facilement trouver leur utilisation.

Si vous examinez le contenu de ces cibles XML, vous verrez que cette propriété déclenche deux tâches MSBuild. La première tâche est appelée ResolveAssemblyReferenceset génère un ensemble de redirections qui sont écrites dans des fichiers. La deuxième tâche GenerateBindingRedirectsécrit les résultats de la première tâche dansApp.config. Il existe une logique XML qui corrige légèrement le fonctionnement de la première tâche et supprime certaines redirections inutiles ou en ajoute de nouvelles.

Alternative aux configurations XML


Il n'est pas toujours pratique de conserver les redirections dans la configuration XML. Nous pouvons avoir une situation où l'application télécharge le plugin, et ce plugin utilise d'autres bibliothèques qui nécessitent des redirections. Dans ce cas, nous ne connaissons peut-être pas l'ensemble des redirections dont nous avons besoin, ou nous ne souhaitons pas générer de XML. Dans une telle situation, nous pouvons créer un AppDomain et, quand il est créé, toujours y transférer où se trouve le XML avec les redirections nécessaires. Nous pouvons également gérer les erreurs de chargement des assemblages directement pendant l'exécution. Rantime .NET offre une telle opportunité.

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


Il a un événement, il s'appelle CurrentDomain.AssemblyResolve. En vous abonnant à cet événement, nous recevrons des erreurs concernant tous les téléchargements d'assembly ayant échoué. Nous obtenons le nom de l'assembly qui n'a pas été chargé et nous obtenons l'assembly d'assembly qui a demandé le premier assembly à charger. Ici, nous pouvons charger manuellement l'assembly au bon endroit, par exemple, en supprimant la version, en la prenant simplement du fichier et en renvoyant cet événement à partir du gestionnaire. Ou renvoyez null si nous n'avons rien à retourner, si nous ne pouvons pas charger l'assembly. PublicKeyToken doit être le même, les assemblys avec différents PublicKeyToken ne sont en aucun cas amis entre eux.



Cet événement s'applique à un seul domaine d'application. Si notre plugin crée un AppDomain en lui-même, alors cette redirection dans le runtime ne fonctionnera pas en eux. Vous devez en quelque sorte vous abonner à cet événement dans tous les AppDomain que le plugin a créés. Nous pouvons le faire en utilisant AppDomainManager.

AppDomainManager est un assembly séparé qui contient une classe qui implémente une interface spécifique, et l'une des méthodes de cette interface vous permettra d'initialiser tout nouveau AppDomain créé dans l'application. Une fois l'AppDomain créé, cette méthode sera appelée. Vous pouvez y souscrire à cet événement.

Chargement d'assemblage strict et .NET Core


Dans .NET Core, il n'y a pas de problème appelé «chargement d'assemblage strict», car les assemblys signés nécessitent exactement la version demandée. Il y a une autre exigence. Pour tous les assemblys, qu'ils soient signés par Strong name ou non, il est vérifié que la version chargée lors de l'exécution est supérieure ou égale à la précédente. Si nous sommes dans la situation d'une application avec des plugins, nous pouvons avoir une telle situation que le plugin a été collecté, par exemple, à partir d'une nouvelle version du SDK, et l'application dans laquelle il est téléchargé utilise jusqu'à présent l'ancienne version du SDK, et au lieu de s'effondrer nous pouvons également souscrire à cet événement, mais déjà dans .NET Core, et également charger l'assembly que nous avons. Nous pouvons écrire ce code:
AppDomain.CurrentDomain.AssemblyResolve += (s, eventArgs) => 
{ 
     CheckForRecursion(); 
     var name = eventArgs.Name;
     var requestingAssembly = eventArgs.RequestingAssembly; 
    
     name.Version = new Version(0, 0); 
     
     return Assembly.Load(name); 
};


Nous avons le nom de l'assembly qui n'a pas démarré, nous annulons la version et l'appelons à Assembly.Loadpartir de la même version. Il n'y aura pas de récursivité ici, car j'ai déjà vérifié la récursivité.



Il était nécessaire de télécharger la version 0.0.2.0 de MyUtils. Dans BIN, nous avons la version 0.0.1.0 de MyUtils. Nous avons fait une redirection de la version 0.0.2.0 vers la version 0.0. La version 0.0.1.0 ne se chargera pas avec nous. Une sortie s'envolera pour nous qu'il n'a pas été possible de charger l'assembly avec la version 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


Dans la classe Version, tous les composants ne sont pas obligatoires, et au lieu des composants facultatifs –1 sont stockés, mais quelque part à l'intérieur, un débordement se produit et les 2 16–1 sont obtenus . Si vous êtes intéressé, vous pouvez essayer de trouver exactement où le débordement se produit.



Si vous travaillez avec des assemblages de réflexion et souhaitez obtenir tous les types, il peut s'avérer que tous les types ne peuvent pas obtenir votre méthode GetTypes. Un assembly a une classe qui hérite d'une autre classe qui se trouve dans un assembly qui n'est pas chargé.

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



Dans ce cas, le problème sera qu'une exception ReflectionTypeLoadException sera levée. À l'intérieur, ReflectionTypeLoadExceptionil y a une propriété dans laquelle il y a ces types qui ont quand même réussi à être chargés. Toutes les bibliothèques populaires ne tiennent pas compte de cette chose. AutoMapper, au moins une de ses versions, s'il est confronté à ReflectionTypeLoadException, vient de tomber, au lieu d'aller chercher les types à l'intérieur de l'exception.

Nom fort


Assemblages à nom fort

Parlons des causes du chargement strict des assemblages, il s'agit du nom fort.
Le nom fort est la signature de l'assembly par une clé privée utilisant un cryptage asymétrique. PublicKeyToken est le hachage de clé publique de cet assembly.

Un nom fort vous permet de distinguer les différents assemblys qui portent le même nom. Par exemple, MyUtils n'est pas un nom unique, il peut y avoir plusieurs assemblys avec ce nom, mais si vous signez Strong name, ils auront différents PublicKeyToken et nous pouvons les distinguer de cette façon. Un nom fort est requis pour certains scénarios de chargement d'assembly.

Par exemple, afin d'installer un assembly dans le Global Assembly Cache ou de télécharger plusieurs versions de côte à côte à la fois. Plus important encore, les assemblys nommés forts ne peuvent référencer que d'autres assemblys nommés forts. Étant donné que certains utilisateurs souhaitent signer leurs versions avec un nom fort, les développeurs de bibliothèques signent également leurs bibliothèques, de sorte qu'il est plus facile pour les utilisateurs de les installer, de sorte que les utilisateurs n'aient pas à re-signer ces bibliothèques.

Nom fort: Legacy?


Noms forts et bibliothèques .NET

Microsoft dit explicitement sur MSDN que vous ne devez pas utiliser de nom fort à des fins de sécurité, qu'ils ne fournissent que pour distinguer différents assemblys portant le même nom. La clé d'assemblage ne peut en aucun cas être modifiée; si vous la modifiez, vous interromprez les redirections vers tous vos utilisateurs. Si vous avez une partie privée de la clé du nom fort divulgué à l'accès public, vous ne pouvez en aucun cas retirer cette signature. Le format de fichier SNK dans lequel se trouve le nom fort ne fournit pas une telle opportunité, et d'autres formats de stockage de clés contiennent au moins un lien vers la liste de révocation de certificats CRL, par laquelle on peut comprendre que ce certificat n'est plus valide. Il n'y a rien de tel dans SNK.

Le guide Open-source contient les recommandations suivantes. Premièrement, pour des raisons de sécurité, utilisez également d'autres technologies. Deuxièmement, si vous avez une bibliothèque open source, il est généralement suggéré de valider la partie privée de la clé dans le référentiel, afin qu'il soit plus facile pour les gens de bifurquer votre bibliothèque, de la reconstruire et de la placer dans une application prête à l'emploi. Troisièmement, ne changez jamais de nom fort. Trop destructeur. Malgré le fait qu'il soit trop destructeur et écrit à ce sujet dans le guide Open-source, Microsoft a parfois des problèmes avec ses propres bibliothèques.



Il existe une bibliothèque appelée System.Reactive. Auparavant, il s'agissait de plusieurs packages NuGet, l'un d'eux est Rx-Linq. Ceci est juste un exemple, le même pour les autres packages. Dans la deuxième version, il a été signé avec une clé Microsoft. Dans la troisième version, il est passé au référentiel du projet github.com/dotnet et a commencé à avoir une signature .NET Foundation. La bibliothèque, en fait, a changé de nom fort. Le package NuGet a été renommé, mais l'assembly est appelé à l'intérieur exactement comme avant. Comment rediriger de la deuxième version vers la troisième? Cette redirection ne peut pas être effectuée.

Validation forte du nom


Comment: désactiver la fonction de contournement de nom fort

Un autre argument selon lequel le nom fort est déjà une chose du passé et reste purement formel est qu'ils ne sont pas validés. Nous avons un assembly signé et nous voulons corriger une sorte de bogue, mais nous n'avons pas accès aux sources. Nous pouvons simplement prendre dnSpy - c'est un utilitaire qui vous permet de décompiler et de corriger les assemblys déjà compilés. Tout fonctionnera pour nous. Parce que par défaut, le contournement de la validation du nom fort est activé, c'est-à-dire qu'il vérifie uniquement que le PublicKeyToken est égal et que l'intégrité de la signature elle-même n'est pas vérifiée. Il peut y avoir des études environnementales dans lesquelles la signature est toujours vérifiée, et ici un exemple frappant est IIS. L'intégrité de la signature est vérifiée sur IIS (le contournement de la validation du nom fort est désactivé par défaut) et tout se cassera si nous modifions l'assembly signé.

Une addition:Vous pouvez désactiver la vérification de signature pour l'assembly à l' aide de signe public. Avec lui, seule la clé publique est utilisée pour la signature, ce qui garantit la sécurité du nom de l'assembly. Les clés publiques utilisées par Microsoft sont publiées ici .
Dans Rider, la signature publique peut être activée dans les propriétés du projet.





Quand changer les versions de l'assemblage de fichiers

Le guide open-source propose également une politique de gestion des versions, dont le but est de réduire le nombre de redirections de liaison et de modifications nécessaires pour les utilisateurs sur le Framework NET. Cette politique de version est que nous ne devons pas changer constamment la version de l'assembly. Bien sûr, cela peut entraîner des problèmes d'installation dans le GAC, de sorte que l'image native installée peut ne pas correspondre à l'assembly et vous devez effectuer à nouveau la compilation JIT, mais, à mon avis, c'est moins mal que les problèmes de version. Dans le cas de CrossGen, les assemblys natifs ne sont pas installés globalement - il n'y aura aucun problème.

Par exemple, le package NuGet Newtonsoft.Json, il a plusieurs versions: 12.0.1, 12.0.2, etc. - tous ces packages ont un assembly avec la version 12.0.0.0. La recommandation est que la version de l'assembly soit mise à jour lorsqu'une version majeure du package NuGet change.

résultats


Suivez les conseils pour le .NET Framework: générez des redirections manuellement et essayez d'utiliser la même version de dépendances dans tous les projets de votre solution. Cela devrait considérablement réduire le nombre de redirections. Vous avez besoin d'un nom fort uniquement si vous avez un scénario de chargement de build spécifique où il est nécessaire, ou si vous développez une bibliothèque et souhaitez simplifier la vie des utilisateurs qui ont vraiment besoin d'un nom fort. Ne changez pas le nom fort.

Norme .NET


Nous passons à .NET Standard. Il est assez étroitement lié à la version Hell dans le .NET Framework. .NET Standard est un outil pour écrire des bibliothèques compatibles avec diverses implémentations de la plate-forme .NET. Les implémentations font référence à .NET Framework, .NET Core, Mono, Unity et Xamarin.



* Lien vers la documentation

Il s'agit du tableau de prise en charge de .NET Standard pour différentes versions de différentes versions de runtimes. Et ici, nous pouvons voir que le .NET Framework ne prend en aucun cas en charge la version 2.1 .NET Standard. La sortie du .NET Framework, qui prendra en charge la norme .NET 2.1 et versions ultérieures, n'est pas encore prévue. Si vous développez une bibliothèque et souhaitez qu'elle fonctionne pour les utilisateurs sur .NET Framework, vous devrez avoir une cible pour .NET Standard 2.0. Outre le fait que .NET Framework ne prend pas en charge la dernière version de la norme .NET, prêtons attention à l'astérisque. Le .NET Framework 4.6.1 prend en charge .NET Standard 2.0, mais avec un astérisque. Il y a une telle note directement dans la documentation, où ai-je obtenu ce tableau.



Prenons un exemple de projet. Une application sur le .NET Framework qui a une dépendance ciblant la norme .NET. Quelque chose comme ça: ConsoleApp et ClassLibrary. Bibliothèque cible .NET Standard. Lorsque nous assemblerons ce projet, ce sera comme ça dans notre BIN.



Nous aurons une centaine de DLL, dont une seule liée à l'application, tout le reste est venu pour prendre en charge la norme .NET. Le fait est que .NET Standard 2.0 est apparu plus tard que .NET Framework 4.6.1, mais en même temps, il s'est avéré être compatible avec l'API, et les développeurs ont décidé d'ajouter la prise en charge de Standard 2.0 à .NET 4.6.1. Nous ne l'avons pas fait en natif (en l'incluant netstandard.dlldans le runtime lui-même), mais de telle manière que .NET Standard * .dll et toutes les autres façades d'assemblage sont placés directement dans BIN.



Si nous regardons les dépendances de la version du .NET Framework que nous ciblons et le nombre de bibliothèques qui sont tombées dans le BIN, nous verrons qu'il n'y en a pas tellement en 4.7.1, et depuis 4.7.2 il n'y a plus de bibliothèques supplémentaires, et .NET La norme y est prise en charge nativement.



Il s'agit d'un tweet de l'un des développeurs .NET, qui décrit ce problème et recommande d'utiliser le .NET Framework version 4.7.2 si nous avons des bibliothèques .NET Standard. Pas même avec la version 2.0 ici, mais avec la version 1.5.

résultats


Si possible, augmentez le cadre cible de votre projet à au moins 4.7.1, de préférence 4.7.2. Si vous développez une bibliothèque pour faciliter la vie des utilisateurs de bibliothèque, créez une cible distincte pour le .NET Framework, cela évitera un grand nombre de DLL qui peuvent entrer en conflit avec quelque chose.

.NET Core


Commençons par une théorie générale. Nous verrons comment nous avons lancé JetBrains Rider sur .NET Core et pourquoi nous devrions en parler. Rider est un très grand projet, il a une énorme solution d'entreprise avec un grand nombre de projets différents, un système complexe de dépendances, vous ne pouvez pas simplement le prendre et migrer vers un autre runtime à la fois. Pour ce faire, nous devons utiliser des hacks, que nous analyserons également.

Application .NET Core


À quoi ressemble une application .NET Core typique? Cela dépend de la façon exacte dont il est déployé, de ce qu'il va finalement faire. Nous pouvons avoir plusieurs scénarios. Le premier est un déploiement dépendant de Framework. C'est la même chose que dans le .NET Framework lorsque l'application utilise le runtime préinstallé sur l'ordinateur. Il peut s'agir d'un déploiement autonome, c'est lorsque l'application porte un runtime. Et il peut y avoir un déploiement à fichier unique, c'est lorsque nous obtenons un fichier exe, mais dans le cas de .NET Core à l'intérieur de ce fichier exe, il y a un artefact d'application autonome, il s'agit d'une archive auto-extractible.



Nous ne considérerons que le déploiement dépendant du Framework. Nous avons une DLL avec l'application, il y a deux fichiers de configuration, le premier est requis, ceci runtimeconfig.jsonetdeps.json. À partir de .NET Core 3.0, un fichier exe est généré qui est nécessaire pour rendre l'application plus pratique à exécuter, de sorte que vous n'avez pas besoin d'entrer la commande .NET si nous sommes sous Windows. Les dépendances tombent dans cet artefact, à commencer par .NET Core 3.0, dans .NET Core 2.1, vous devez publier ou utiliser une autre propriété dans *.csproj.

Cadres partagés, .runtimeconfig.json





.runtimeconfig.jsoncontient les paramètres d'exécution nécessaires à son exécution. Il indique sous quel Framework partagé l'application sera lancée, et cela ressemble à ceci. Nous indiquons que l'application s'exécutera sous «Microsoft.NETCore.App» version 3.0.0, il peut y avoir un autre Framework partagé. D'autres paramètres peuvent également être ici. Par exemple, vous pouvez activer le garbage collector du serveur.



.runtimeconfig.jsongénéré lors de l'assemblage du projet. Et si nous voulons inclure le GC du serveur, nous devons en quelque sorte modifier ce fichier à l'avance, avant même d'assembler le projet, ou de l'ajouter à la main. Vous pouvez ajouter vos paramètres ici comme ceci. Nous pouvons inclure une propriété dans *.csproj, si cette propriété est fournie par les développeurs .NET, ou si la propriété n'est pas fournie, nous pouvons créer un fichier appeléruntimeconfig.template.jsonet écrivez ici les paramètres nécessaires. Lors de l'assemblage, d'autres paramètres nécessaires seront ajoutés à ce modèle, par exemple, le même Framework partagé.



Le Framework partagé est un ensemble de runtime et de bibliothèques. En fait, la même chose que le runtime .NET Framework, qui était installé juste une fois sur la machine et pour tous était une version. Shared Framework et, contrairement à un seul environnement d'exécution .NET Framework, peuvent être versionnés, différentes applications peuvent utiliser différentes versions des environnements d'exécution installés. Le cadre partagé peut également être hérité. Le Shared Framework lui-même peut être affiché dans les emplacements du disque tels qu'ils sont généralement installés sur le système.



Il existe plusieurs Framework partagés standard, par exemple, Microsoft.NETCore.App, qui exécute les applications de console conventionnelles, AspNetCore.App, pour les applications Web et WindowsDesktop.App, le nouveau Framework partagé dans .NET Core 3, qui exécute les applications de bureau sur Windows Forms et WPF. Les deux derniers Framework partagés complètent essentiellement le premier nécessaire pour les applications console, c'est-à-dire qu'ils ne portent pas un tout nouveau runtime, mais complètent simplement celui existant avec les bibliothèques nécessaires. Cet héritage semble se trouver également dans les répertoires Shared Framework runtimeconfig.jsondans lesquels le Shared Framework de base est spécifié.

Dépendance manifest ( .deps.json)



Sondage par défaut - .NET Core Le

deuxième fichier de configuration est le suivant .deps.json. Ce fichier contient une description de toutes les dépendances de l'application ou du Framework partagé, ou de la bibliothèque, les bibliothèques l' .deps.jsonont également. Il contient toutes les dépendances, y compris les transitives. Et le comportement du runtime .NET Core diffère selon que .deps.jsonl'application en dispose ou non. Sinon .deps.json, l'application pourra charger tous les assemblys qui se trouvent dans son Framework partagé ou dans son répertoire BIN. Si tel .deps.jsonest le cas, la validation est activée. Si l'un des assemblys répertoriés dans .deps.jsonne l'est pas, l'application ne démarre tout simplement pas. Vous verrez l'erreur présentée ci-dessus. Si l'application essaie de charger un assembly en runtime, ce qui.deps.json si, par exemple, en utilisant des méthodes de chargement d'assemblage ou pendant le processus de résolution d'assemblages, vous verrez une erreur très similaire au chargement d'assemblage strict.

Pilote Jetbrains


Rider est un IDE .NET. Tout le monde ne sait pas que Rider est un IDE composé d'un frontend basé sur IntelliJ IDEA et écrit en Java et Kotlin, et un backend. Le backend est essentiellement R #, qui peut communiquer avec IntelliJ IDEA. Ce backend est maintenant une application .NET multiplateforme.
Où cela fonctionne-t-il? Windows utilise le .NET Framework, qui est installé sur l'ordinateur de l'utilisateur. Sur d'autres systèmes d'information, sous Linux et Mac, Mono est utilisé.

Ce n'est pas une solution idéale lorsqu'il y a des temps d'exécution différents partout, et je veux passer à l'état suivant pour que Rider s'exécute sur .NET Core. Afin d'améliorer les performances, car dans .NET Core, toutes les dernières fonctionnalités y sont associées. Pour réduire la consommation de mémoire. Il y a maintenant un problème avec la façon dont Mono fonctionne avec la mémoire.

Le passage à .NET Core vous permettra d'abandonner les technologies héritées et non prises en charge et vous permettra de faire passer des correctifs pour les problèmes détectés lors de l'exécution. Le passage à .NET Core vous permettra de contrôler la version du runtime, c'est-à-dire que Rider ne s'exécutera plus sur le .NET Framework installé sur l'ordinateur de l'utilisateur, mais sur une version spécifique de .NET Core, qui peut être interdite, en tant que déploiement autonome. La transition vers .NET Core permettra à terme d'utiliser de nouvelles API importées spécifiquement dans Core.

Maintenant, l'objectif est de lancer un prototype, de le lancer, juste pour vérifier comment cela fonctionnera, quels sont les points de défaillance potentiels, quels composants devront être réécrits à nouveau, ce qui nécessitera un traitement global.

Fonctionnalités qui rendent difficile la traduction de Rider en .NET Core


Visual Studio, même si R # n'y est pas installé, se bloque à partir de la mémoire insuffisante sur les grandes solutions, dans lesquelles se trouvent des projets avec le style SDK * .csproj . Le style SDK * .csproj est l'une des conditions principales pour une relocalisation complète de .NET Core.

C'est un problème parce que Rider est basé sur R #, ils vivent dans le même référentiel, les développeurs R # veulent utiliser Visual Studio pour développer leur propre produit dans leur produit afin de le rendre alimentaire. Dans R #, il existe des liens de bibliothèques spécifiques pour le framework avec lequel vous devez faire quelque chose. Sous Windows, nous pouvons utiliser le Framework pour les applications de bureau, et sous Linux et Mac, Mock est déjà utilisé pour les bibliothèques Windows avec des fonctionnalités minimales.

Décision


Nous avons décidé de rester sur les anciens pour le moment *.csproj, assembler sous le Framework complet, mais comme les assemblages du Framework et du Core sont compatibles binaires, exécutez-les sur Core. Nous n'utilisons pas de fonctionnalités incompatibles, ajoutons manuellement tous les fichiers de configuration nécessaires et téléchargeons les versions spéciales des dépendances pour .NET Core, le cas échéant.

À quels hacks avez-vous dû aller?


Un hack: nous voulons appeler une méthode qui n'est disponible que dans le Framework, par exemple, cette méthode est nécessaire en R #, mais pas sur Core. Le problème est que s'il n'y a pas de méthode, la méthode qui l'appelle lors de la compilation JIT tombera plus tôt MissingMethodException. Autrement dit, une méthode qui n'existe pas a ruiné la méthode qui l'appelle.

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

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


La solution est là: nous appelons des méthodes incompatibles dans des méthodes distinctes. Il y a un autre problème: une telle méthode peut devenir inline, donc nous la marquons avec un attribut NoInlining.

Hack numéro deux: nous devons être capables de charger des assemblages dans des chemins relatifs. Nous avons un assemblage pour le Framework, il existe une version spéciale pour .NET Core. Comment télécharger la version .NET Core pour .NET Core?



Ils nous aideront .deps.json. Voyons .deps.jsonla bibliothèque System.Diagnostics.PerformanceCounter. Une telle bibliothèque est remarquable par sa.deps.json. Il a une section d'exécution, dans laquelle une version de la bibliothèque avec son chemin d'accès relatif est indiquée. Cette bibliothèque, l'assembly sera chargé sur tous les runtimes, et il ne fait que lancer les exécutions. Si, par exemple, il se charge sur Linux, PerformanceCounter ne fonctionne pas sur la conception sous Linux et une PlatformNotSupportedException s'en échappe. Il y a également .deps.jsonune section runtimeTargets dans ceci et ici est déjà indiquée la version de cet assembly spécifiquement pour Windows, où PerformanceCounter devrait fonctionner.

Si nous prenons la section runtime et y écrivons le chemin relatif vers la bibliothèque que nous voulons charger, cela ne nous aidera pas. La section d'exécution définit en fait le chemin relatif à l'intérieur du package NuGet, et non par rapport au BIN. Si nous recherchons cet assemblage dans BIN, seul le nom de fichier sera utilisé à partir de là. La section runtimeTargets contient déjà un chemin relatif honnête, un chemin honnête relatif au BIN. Nous allons prescrire un chemin relatif pour nos assemblys dans la section runtimeTargets. Au lieu de l'identifiant d'exécution, qui est «gagnant» ici, nous pouvons en prendre un autre que nous aimons. Par exemple, nous écrirons l'identifiant d'exécution «any», et cet assembly sera généralement chargé sur toutes les plateformes. Ou nous écrirons «unix», et il démarrera sous Linux, et sur Mac, et ainsi de suite.

Prochain hack: nous voulons télécharger sur Linux et sur Mac Mock pour construire WindowsBase. Le problème est que l'assembly nommé WindowsBase est déjà présent dans le Framework partagé Microsoft.NETCore.App, même si nous ne sommes pas sous Windows. Sur Windows Shared Framework, Microsoft.WindowsDesktop.AppWindowsBase redéfinit la version qui se trouve dans NETCore.App. Regardons .deps.jsonces Framework, plus précisément les sections qui décrivent WindowsBase.



Voici la différence:



si certaines bibliothèques entrent en conflit et sont présentes dans plusieurs .deps.json, le maximum d'entre elles est sélectionné pour la paire composée de assemblyVersionet fileVersion. Le guide .NET indique qu'il fileVersionsuffit de l'afficher dans l'Explorateur Windows, mais ce n'est pas le cas, il tombe dans.deps.json. C'est le seul cas que je connaisse quand la version prescrit .deps.json, assemblyVersionet fileVersion, sont effectivement utilisés. Dans tous les autres cas, j'ai vu un comportement qui, quelles que .deps.jsonsoient les versions écrites, l'assembly continuerait de toute façon à se charger.



Quatrième hack. Tâche: nous avons un fichier .deps.json pour les deux hacks précédents, et nous en avons besoin uniquement pour des dépendances spécifiques. Puisqu'ils sont .deps.jsongénérés en mode semi-manuel, nous avons un script qui, selon une description de ce qui devrait y arriver, le génère pendant la construction, nous voulons garder cela aussi .deps.jsonminimal que possible afin que nous puissions comprendre ce qu'il contient. Nous voulons désactiver la validation et autoriser le téléchargement des assemblys qui se trouvent dans le BIN mais qui ne sont pas décrits dans .deps.json.

Solution: activez la configuration personnalisée dans runtimeconfig. Ce paramètre est réellement nécessaire pour la compatibilité descendante avec .NET Core 1.0.

résultats


Donc, .runtime.jsonet .deps.jsonsur .NET Core - ce sont des sortes d'analogues App.config. App.configvous permettent de faire les mêmes choses, par exemple, de charger des assemblages de manière relative. À l'aide de la .deps.jsonréécriture manuelle, vous pouvez personnaliser le chargement des assemblys sur .NET Core, si vous avez un scénario très complexe.

Téléchargements d'assemblages de débogage


J'ai parlé de certains types de problèmes, vous devez donc être en mesure de déboguer les problèmes de chargement des assemblys. Qu'est-ce qui peut aider? Tout d'abord, les runtimes écrivent des journaux sur la façon dont ils chargent les assemblys. Deuxièmement, vous pouvez regarder de plus près les exécutions qui vous arrivent. Vous pouvez également vous concentrer sur les événements d'exécution.

Journaux de fusion





Retour aux principes de base: Utilisation de Fusion Log Viewer pour déboguer les erreurs obscures
Fusion

Le mécanisme de chargement des assemblys dans le .NET Framework s'appelle Fusion, et il sait comment enregistrer ce qu'il a fait sur le disque. Pour activer la journalisation, vous devez ajouter des paramètres spéciaux au registre. Ce n'est pas très pratique, il est donc logique d'utiliser des utilitaires, à savoir Fusion Log Viewer et Fusion ++. Fusion Log Viewer est un utilitaire standard fourni avec Visual Studio et peut être lancé à partir de la ligne de commande Visual Studio, Visual Studio Developer Command Prompt. Fusion ++ est un analogue open source de cet outil avec une interface plus agréable.



Fusion Log Viewer ressemble à ceci. C'est pire que WinDbg car cette fenêtre ne s'étire même pas. Néanmoins, vous pouvez percer les coches ici, bien qu'il ne soit pas toujours évident de savoir quel ensemble de coches est correct.



Fusion ++ possède un bouton «Démarrer la journalisation», puis le bouton «Arrêter la journalisation» apparaît. Dans celui-ci, vous pouvez voir tous les enregistrements sur le chargement des assemblages, lire les journaux sur ce qui se passait exactement. Ces journaux ressemblent à ceci de manière concise.



Il s'agit d'une exemption du chargement d'assemblage strict. Si nous regardons les journaux de Fusion, nous verrons que nous devions télécharger la version 9.0.0.0 après avoir traité toutes les configurations. Nous avons trouvé un dossier dans lequel on soupçonne que nous avons l'assemblage dont nous avons besoin. Nous avons vu que la version 6.0.0.0 est dans ce fichier. Nous avons un avertissement que nous avons comparé les noms complets des assemblys, et ils diffèrent dans la version principale. Et puis une erreur s'est produite - incompatibilité de version.

Événements d'exécution





Journalisation des événements d'exécution

Sur Mono, vous pouvez activer la journalisation à l'aide de variables d'environnement et les journaux seront éventuellement écrits dans stdoutet stderr. Pas si pratique, mais la solution fonctionne.



Sondage par défaut -
Documentation .NET Core / documentation de conception / suivi d'hôte

. .NET Core possède également une variable d'environnement spéciale COREHOST_TRACEqui inclut la connexion stderr. Avec .NET Core 3.0, vous pouvez écrire des journaux dans un fichier en spécifiant le chemin d'accès à celui-ci dans une variable COREHOST_TRACEFILE.


Un événement se déclenche lorsque les assemblys ne se chargent pas. Ceci est un événement AssembleResolve. Il y a un deuxième événement utile, celui-ci FirstChanceException. Vous pouvez vous y abonner et obtenir une erreur sur le chargement des assemblys, même si quelqu'un a écrit try..catch et a raté toutes les exécutions à l'endroit oùFileLoadExceptioneu lieu. Si l'application a déjà été compilée, vous pouvez la démarrer perfviewet elle peut surveiller les exécutions .NET et y trouver celles liées au téléchargement des fichiers.

résultats


Transférez le travail vers des outils, vers des outils de développement, vers un IDE, vers MSBuild, ce qui vous permet de générer des redirections. Vous pouvez basculer vers .NET Core, puis vous oublierez ce qu'est le chargement d'assemblage strict et vous pourrez utiliser la nouvelle API comme nous le souhaitons dans Rider. Si vous connectez la bibliothèque .NET Standard, augmentez la version cible du .NET Framework à au moins 4.7.1. Si vous semblez être dans une situation désespérée, recherchez des hacks, utilisez-les ou inventez vos propres hacks pour des situations désespérées. Et armez-vous avec des outils de débogage.

Je vous recommande fortement de lire les liens suivants:



DotNext 2020 Piter . , 8 JUG Ru Group.

All Articles