.NET:依赖关系处理

谁没有由于程序集重定向而遇到问题?最有可能的是,开发相对较大的应用程序的每个人迟早都会遇到此问题。

现在,我在JetBrains Rider项目的JetBrains工作,并参与了将Rider迁移到.NET Core的任务。以前在Circuit(基于云的应用程序托管平台)中从事共享基础结构的工作。



在过场动画的下面是我在DotNext 2019莫斯科会议上的报告的笔录,在那次会议上我谈到了在.NET中使用程序集时遇到的困难,并通过实际示例演示了会发生什么以及如何处理它。


在我作为.NET开发人员工作的所有项目中,我都不得不处理连接依赖项和加载程序集的各种问题。我们将讨论这个。

职位结构:


  1. 依赖问题
  2. 严格的钻机装载

  3. .NET核心

  4. 调试程序集下载


有哪些依赖性问题?


当他们在2000年代初开始开发.NET Framework时,所有库中的开发人员都允许中断更改,并且这些库变得与已编译的代码不兼容时,就已经知道Dependency hell问题。如何解决这样的问题?第一个解决方案是显而易见的。始终保持向后兼容性。当然,这不是很现实,因为打破变更很容易放入代码中。例如:



重大更改和.NET库

这是特定于.NET的示例。我们有一个方法,我们决定为其添加一个带有默认值的参数。如果我们重新组装代码,则代码将继续编译,但是对于二进制文件,它将是两种完全不同的方法:一种方法的参数为零,第二种方法的参数为一个。如果依赖项中的开发人员以这种方式破坏了向后兼容性,那么我们将无法使用在先前版本中使用此依赖项编译的代码。

解决依赖关系问题的第二种方法是添加库,程序集的版本控制-任何东西。版本控制规则可能不同,关键是我们可以以某种方式将同一库的不同版本区分开来,并且您可以了解更新是否中断。不幸的是,一旦我们引入了版本,就会出现另一种问题。



版本地狱是无法使用二进制兼容的依赖项,但同时具有的版本与运行时不匹配,或者具有其他组件无法检查这些版本。在.NET中,尽管文件位于磁盘上,但典型的地狱版本是FileLoadException,但由于某种原因它并未随运行时一起加载。



在.NET中,程序集具有许多不同的版本-他们试图以各种方式修复版本地狱,并查看发生了什么。我们有一个包裹System.Collections.Immutable。很多人认识他。他拥有NuGet软件包1.6.0的最新版本。它包含一个库,以及版本为1.2.4.0的程序集。您已经收到您没有版本库1.2.4.0。如何理解它位于1.6.0 NuGet软件包中?没那么简单。除了程序集版本外,该库还有其他几个版本。例如,程序集文件版本,程序集信息版本。此NuGet软件包实际上包含具有相同版本的三个不同程序集(用于.NET Standard的不同版本)。

.NET文档
Opbuild标准

关于如何在.NET中使用程序集,已经写了很多文档。有一个.NET指南可用于开发.NET的现代应用程序,其中考虑了.NET Framework,.NET标准,.NET Core,开源以及所有可能的功能。整个文档中约有30%用于加载程序集。我们将分析可能出现的特定问题和示例。

为什么这一切都是必要的?首先,避免踩耙。其次,您可以使您的库用户的生活更加轻松,因为使用您的库,他们将不会遇到习惯的依赖问题。它还将帮助您应对将复杂应用程序迁移到.NET Core的问题。最重要的是,您可以成为一名SRE,这是一名高级(绑定)重定向工程师,团队中的每个人都来参加该会议,并询问如何编写另一个重定向。

严格装配


严格的程序集加载是.NET Framework开发人员面临的主要问题。用表示FileLoadException在继续进行严格的程序集加载之前,让我提醒您一些基本知识。

生成.NET应用程序时,输出是一些工件,通常位于Bin / Debug或Bin / Release中,并且包含一组特定的程序集程序集和配置文件。程序集将通过名称(程序集名称)相互引用。重要的是要了解,程序集链接直接位于引用该程序集的程序集中;没有用于写入程序集引用的魔术配置文件。即使在您看来,此类文件也存在。引用本身是二进制形式的程序集。

在.NET中,有一个程序集解析过程-当程序集定义已转换为真实程序集时,该程序集位于磁盘上或加载到内存中的某个位置。程序集解析执行两次:在构建阶段,当您在* .csproj中具有引用时,在运行时阶段,当您在程序集内部具有引用时,根据某些规则,它们将变为可下载的程序集。

//简单名称
MyAssembly,版本= 6.0.0.0,
文化=中性,PublicKeyToken = null

//强名
Newtonsoft.Json,版本= 6.0.0.0,
文化=中性,PublicKeyToken = 30ad4fe6b2a6aeed // PublicKey


让我们继续解决这个问题。程序集名称主要有两种。第一种程序集名称是简单名称。由于它们具有PublicKeyToken = null,因此很容易识别它们。有一个强名称,很容易通过它们的PublicKeyToken不是null而是某个值的事实来识别它们。



让我们举个例子。我们有一个依赖于MyUtils实用程序的库的程序,而MyUtils的版本是9.0.0.0。同一程序具有指向另一个库的链接。该库还希望使用MyUtils,但版本为6.0.0.0。 MyUtils 9.0.0.0版和6.0.0.0版的PublicKeyToken = null,也就是说,它们具有简单名称。哪个版本将属于二进制工件6.0.0.0或9.0.0.0?第9版。 MyLibrary可以使用MyUtils版本9.0.0.0(该版本已包含在二进制工件中)吗?



实际上,可以这样,因为MyUtils具有简单名称,因此不存在对其进行严格程序集加载的情况。



另一个例子。除了MyUtils,我们还有NuGet的完整库,其中有一个Strong名称。NuGet中的大多数库都有一个强名称。



在构建阶段,将9.0.0.0版复制到BIN,但是在运行时我们得到了著名的BIN FileLoadException为了使MyLibrary(希望将6.0.0.0 Newtonsoft.Json版更新为)能够使用9.0.0.0版,您必须编写Binding redirect到App.config

绑定重定向





重定向程序集版本

声明具有这样名称和publicKeyToken的程序集应从这样的版本范围重定向到这样的版本范围。它似乎是一个非常简单的记录,但是它位于此处App.config,但可以位于其他文件中。machine.config.NET Framework内部有一个文件,运行时内部有一个文件,其中定义了一些标准的重定向集,该版本可能因.NET Framework的版本而异。可能会发生在4.7.1上对您不起作用的情况,但在4.7.2上它已经起作用,反之亦然。您需要记住,重定向不仅可以来自您的重定向.App.config,而且在调试时应该考虑到这一点。

我们简化了重定向的编写


没有人愿意用手书写绑定重定向。让我们将此任务交给MSBuild!



如何启用和禁用自动绑定重定向

有关如何简化使用绑定重定向的一些技巧。提示一:在MSBuild中启用绑定重定向自动生成。由中的属性开启*.csproj。构建项目时,它将落入一个二进制工件中App.config,该工件指示重定向到相同工件中的库的版本。这仅适用于运行应用程序,控制台应用程序,WinExe。对于库,这不起作用,因为对于库App.config通常它根本不相关,因为它与启动和加载程序集本身的应用程序相关。如果为库进行了配置,则在应用程序中,某些依赖项可能与构建库时的依赖项有所不同,事实证明,库的配置没有多大意义。尽管如此,有时对于库配置仍然有意义。



我们编写测试时的情况。测试通常在ClassLibrary中找到,它们也需要重定向。测试框架能够识别带有测试的库具有dll-config,并将其中的重定向交换为测试中的代码。您可以自动生成这些重定向。如果我们有旧格式*.csproj,而不是SDK样式,您可以采用简单的方法,将OutputType更改为Exe并添加一个空的入口点,这将强制MSBuild生成重定向。您可以采用其他方式使用hack。您可以向添加另一个属性*.csproj,这使MSBuild认为对于此OutputType,您仍然需要生成绑定重定向。这种方法虽然看上去很hack,但可以让您为无法在Exe中重做的库以及其他类型的项目(测试除外)生成重定向。

对于新格式,*.csproj如果使用现代的Microsoft.NET.Test.Sdk 则会自行生成重定向。

第三个技巧:不要将绑定重定向生成与NuGet一起使用。 NuGet能够为从软件包传递到最新版本的库生成绑定重定向,但这不是最佳选择。所有这些重定向都必须添加App.config并提交,如果使用MSBuild生成重定向,则在生成过程中会生成重定向。如果提交它们,则可能存在合并冲突。您自己可以简单地忘记更新文件中的Binding重定向,并且如果它们是在构建期间生成的,您将不会忘记。



解决程序集引用
生成绑定重定向

那些想更好地了解绑定重定向的工作原理的家庭作业:了解其工作原理,请参见代码。转到.NET目录,使用name属性更改所有位置,以用于生成。通常这是一种常见的方法,如果MSBuild有一些奇怪的属性,则可以利用它的用法。幸运的是,属性通常在XML配置中使用,您可以轻松地找到它们的用法。

如果检查这些XML目标中的内容,您将看到此属性触发了两个MSBuild任务。第一个任务称为ResolveAssemblyReferences,它将生成一组重定向到文件的重定向。第二个任务GenerateBindingRedirects将第一个任务的结果写入App.configXML逻辑可以稍微纠正第一个任务的操作,并删除一些不必要的重定向或添加新的重定向。

XML配置的替代方法


在XML配置中保留重定向并不总是很方便。我们可能会遇到这种情况,即应用程序下载了该插件,而该插件使用了其他需要重定向的库。在这种情况下,我们可能不知道我们需要的重定向集,或者我们可能不想生成XML。在这种情况下,我们可以创建一个AppDomain,并在创建AppDomain时仍将其转移到具有必要重定向的XML所在的位置。我们还可以在运行时立即处理程序集加载错误。Rantime .NET提供了这样的机会。

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


它有一个事件,称为CurrentDomain.AssemblyResolve。订阅此事件,我们将收到有关所有失败的程序集下载的错误。我们得到未加载的程序集的名称,并得到请求第一个程序集加载的程序集程序集。在这里,我们可以从正确的位置手动加载程序集,例如,删除版本,仅从文件中获取版本,然后从处理程序中返回此事件。如果无法加载任何程序集,则返回空值,否则返回null。 PublicKeyToken应该是相同的,具有不同PublicKeyToken的程序集决不是彼此的朋友。



此事件仅适用于一个应用程序域。如果我们的插件在其内部创建了AppDomain,则运行时中的此重定向将无法在其中运行。您需要以某种方式在插件创建的所有AppDomain中订阅此事件。我们可以使用AppDomainManager做到这一点。

AppDomainManager是一个单独的程序集,其中包含实现特定接口的类,该接口的方法之一将允许您初始化在应用程序中创建的任何新AppDomain。创建AppDomain后,将调用此方法。您可以在其中订阅此活动。

严格的程序集加载和.NET Core


在.NET Core中,没有所谓的“严格程序集加载”问题,这是由于签名的程序集需要确切要求的版本。还有另一个要求。对于所有程序集,无论它们是否用“强名称”签名,都将检查运行时加载的版本是否大于或等于前一个版本。如果我们遇到的是带有插件的应用程序,那么我们可能会遇到这样的情况,例如,该插件是从新版本的SDK构建的,并且下载了该应用程序的应用程序到目前为止使用的是旧版本的SDK,而不是分崩离析,我们也可以订阅此事件,但已经在.NET Core中,并加载我们拥有的程序集。我们可以编写以下代码:
AppDomain.CurrentDomain.AssemblyResolve += (s, eventArgs) => 
{ 
     CheckForRecursion(); 
     var name = eventArgs.Name;
     var requestingAssembly = eventArgs.RequestingAssembly; 
    
     name.Version = new Version(0, 0); 
     
     return Assembly.Load(name); 
};


我们具有未引导的程序集的名称,我们使版本无效并Assembly.Load从相同版本调用它。这里将没有递归,因为我已经检查了递归。



必须下载MyUtils 0.0.2.0版。在BIN中,我们有MyUtils版本0.0.1.0。我们从版本0.0.2.0重定向到版本0.0。版本0.0.1.0将不会随我们一起加载。一个出口将飞向我们,表明无法使用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


在Version类中,并非所有组件都是强制性的,并且不是存储可选组件–1,而是在内部某处发生溢出,并获得2个16–1。如果有兴趣,您可以尝试准确找到溢出发生的位置。



如果您使用反射程序集并希望获取所有类型,则可能会发现并非所有类型都可以获取GetTypes方法。程序集具有一个类,该类继承自未加载的程序集中的另一个类。

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



在这种情况下,问题将是将引发ReflectionTypeLoadException。里面ReflectionTypeLoadException有一个属性,其中仍有那些类型仍然可以加载。并非所有流行的图书馆都考虑到这一点。AutoMapper,至少是它的一个版本,如果遇到ReflectionTypeLoadException,它就会掉下来,而不是从异常内部进行选择。

强大的命名


强名称程序集

让我们讨论导致严格程序集加载的原因,这是强名称。
强名称是使用非对称加密的某些私钥对程序集的签名。 PublicKeyToken是此程序集的公钥哈希。

通过强命名,您可以区分具有相同名称的不同程序集。例如,MyUtils不是唯一的名称,可能有多个具有相同名称的程序集,但是如果您签署强名称,则它们将具有不同的PublicKeyToken,我们可以通过这种方式来区分它们。在某些程序集加载方案中,必须使用强名。

例如,为了在全局程序集缓存中安装程序集或一次并排下载多个版本。最重要的是,强命名程序集只能引用其他强命名程序集。由于某些用户希望使用Strong名称签名其版本,因此库开发人员也会对其库进行签名,以便用户更轻松地安装它们,从而使用户不必重新签名这些库。

强名:旧版?


强命名和.NET库

Microsoft在MSDN上明确表示不应出于安全目的使用强名称,它们仅用于区分具有相同名称的不同程序集。组装密钥不能以任何方式更改;如果更改了密钥,则将中断对所有用户的重定向。如果您的“强名”密钥的私密部分已泄露给公众访问,则您将无法以任何方式撤回此签名。强名称所在的SNK文件格式不会提供这种机会,其他用于存储密钥的格式至少包含指向CRL证书吊销列表的链接,通过该链接可以理解该证书不再有效。 SNK中没有类似的东西。

开源指南具有以下建议。首先,出于安全目的,还使用其他技术。其次,如果您有一个开放源代码库,通常建议将密钥的私有部分提交给存储库,这样人们可以更轻松地分叉您的库,对其进行重建并将其放入现成的应用程序中。第三,永远不要更改强名称。破坏性太大。尽管它破坏力太大,并在《开放源代码指南》中对此进行了说明,但Microsoft有时仍会对其自己的库产生问题。



有一个名为System.Reactive的库。以前,这些是几个NuGet软件包,其中一个是Rx-Linq。这只是一个例子,其余的包也一样。在第二个版本中,它是用Microsoft密钥签名的。在第三个版本中,他移至github.com/dotnet项目中的存储库,并开始具有.NET Foundation签名。实际上,该库已更改为Strong名称。NuGet包已重命名,但程序集的内部调用与之前完全相同。如何从第二个版本重定向到第三个版本?此重定向无法完成。

强名验证


如何:禁用强名绕过功能

关于强名称已经是过去的事情并且仍然纯粹是形式的另一种说法是,它们未经验证。我们有一个已签名的程序集,我们想修复其中的某种错误,但是我们无权访问源代码。我们可以使用dnSpy-这是一个实用程序,可让您反编译并修复已编译的程序集。一切都会为我们工作。因为默认情况下,启用了强名称验证绕过,即,它仅检查PublicKeyToken是否相等,并且不检查签名本身的完整性。可能有一些环境研究仍在对其签名进行验证,这里有一个生动的例子是IIS。在IIS上检查签名完整性(默认情况下,禁用强名称验证旁路),并且如果我们编辑签名程序集,一切都会中断。

加成:您可以使用公共符号禁用程序集的签名验证有了它,仅使用公钥进行签名,从而确保了程序集名称的安全。 Microsoft使用的公共密钥发布在这里
在Rider中,可以在项目属性中启用公共符号。





何时更改文件汇编版本

开源指南还提供了一些版本控制策略,其目的是减少NET Framework上用户必需的绑定重定向和对其进行更改的次数。此版本控制政策是,我们不应不断更改程序集版本。当然,这可能会导致在GAC中进行安装时出现问题,从而使已安装的本机映像可能与程序集不对应,因此您必须再次执行JIT编译,但是在我看来,这比版本控制的问题少。对于CrossGen,本机程序集不会全局安装-不会有问题。

例如,NuGet软件包Newtonsoft.Json,它具有多个版本:12.0.1、12.0.2,等等-所有这些软件包都有一个版本为12.0.0.0的程序集。建议当NuGet软件包的主要版本更改时,应更新程序集版本。

发现


请遵循.NET Framework的提示:手动生成重定向,并尝试在解决方案的所有项目中使用相同版本的依赖项。这将大大减少重定向的数量。仅当您有需要的特定构建加载方案,或者正在开发库并希望为真正需要“强命名”的用户简化工作时,才需要“强命名”。请勿更改强名称。

.NET标准


我们通过.NET标准。它与.NET Framework中的版本地狱密切相关。 .NET Standard是用于编写与.NET平台的各种实现兼容的库的工具。实现是指.NET Framework,.NET Core,Mono,Unity和Xamarin。



*链接到文档

这是.NET Standard支持表,用于不同版本的运行时的各种版本。在这里我们可以看到.NET Framework完全不支持.NET Standard版本2.1。尚未计划支持.NET Standard 2.1和更高版本的.NET Framework版本。如果您要开发一个库并希望它可以在.NET Framework上为用户使用,则必须具有.NET Standard 2.0的目标。除了.NET Framework不支持最新版本的.NET Standard的事实之外,我们还要注意星号。 .NET Framework 4.6.1支持.NET Standard 2.0,但带有星号。在文档中直接有一个脚注,我从哪里获得此表。



考虑一个示例项目。 .NET Framework上的一个应用程序,具有一个针对.NET Standard的依赖项。这样的东西:ConsoleApp和ClassLibrary。目标库.NET标准。当我们将这个项目放在一起时,在我们的BIN中就是这样。



我们将在那里有一百个DLL,其中只有一个与应用程序相关,其他所有东西都是为了支持.NET Standard。事实是,.NET Standard 2.0的出现晚于.NET Framework 4.6.1,但同时它们证明是与API兼容的,因此开发人员决定将Standard 2.0支持添加到.NET 4.6.1。我们不是本机地(通过包含netstandard.dll在运行时本身中)进行此操作,而是将.NET Standard * .dll和所有其他程序集外观直接放置在BIN中。



如果我们查看我们所针对的.NET Framework版本的依赖性以及落入BIN的库的数量,我们会发现在4.7.1中并没有那么多,而且从4.7.2开始,根本没有其他库。.NET本地标准支持。



这是来自.NET开发人员的一则推文,它描述了此问题,如果拥有.NET Standard库,则建议使用.NET Framework 4.7.2版。这里甚至不是2.0版,而是1.5版。

发现


如果可能,将项目中的目标框架至少提高到4.7.1,最好是4.7.2。如果要开发一个库以使库用户的工作更轻松,请为.NET Framework创建一个单独的Target,它将避免大量可能与某些内容冲突的dll。

.NET核心


让我们从一般理论开始。我们将讨论如何在.NET Core上启动JetBrains Rider,以及为什么我们应该完全谈论它。Rider是一个非常大的项目,它具有一个庞大的企业解决方案,其中包含大量不同的项目,一个复杂的依赖系统,您不能只接受它并一次迁移到另一个运行时。为此,我们必须使用一些技巧,我们还将对此进行分析。

.NET Core应用程序


典型的.NET Core应用程序是什么样的?取决于部署的精确程度以及最终的用途。我们可以有几种情况。第一个是框架相关的部署。当应用程序使用计算机上预安装的运行时时,这与.NET Framework中的相同。它可以是一个独立的部署,这是应用程序带有运行时的时间。可能会有一个单文件部署,这是当我们获得一个exe文件时,但是在此exe文件中包含.NET Core的情况下,存在一个自包含应用程序的工件,这是一个自解压的存档。



我们将仅考虑依赖框架的部署。我们必须与该应用程序的DLL,有两个配置文件,其中第一个是必需的,这runtimeconfig.jsondeps.json从.NET Core 3.0开始,将生成一个exe文件,该文件使该应用程序更易于运行,因此,如果我们在Windows上,则无需输入.NET命令。从.NET Core 3.0开始,在.NET Core 2.1中,依赖关系就属于这种工件,您需要在.NET Core 2.1中发布或使用其他属性*.csproj

共享框架 .runtimeconfig.json





.runtimeconfig.json包含运行它所需的运行时设置。它指示将在哪个Shared Framework上启动应用程序,看起来像这样。我们指示该应用程序将在“ Microsoft.NETCore.App” 3.0.0版下运行,可能还有其他共享框架。其他设置也可能在这里。例如,您可以启用服务器垃圾收集器。



.runtimeconfig.json在项目组装过程中生成的。而且,如果要启用服务器GC,则甚至在组装项目或手动添加之前,都需要预先以某种方式修改此文件。您可以像这样在此处添加设置。*.csproj如果.NET开发人员提供了该属性,或者可以不提供该属性,我们可以在其中包含属性,我们可以创建一个名为runtimeconfig.template.json并在此处写下必要的设置。在组装过程中,其他必要的设置将被添加到该模板中,例如,相同的共享框架。



共享框架是一组运行时和库。实际上,它与.NET Framework运行时相同,以前仅在计算机上安装过一次,而对于所有版本来说都是一个版本。与单个.NET Framework运行时不同,可以对共享框架进行版本控制,不同的应用程序可以使用安装的运行时的不同版本。共享框架也可以被继承。可以在磁盘上通常安装在系统上的这些位置中查看共享框架本身。



有几种标准的共享框架,例如,运行常规控制台应用程序的Microsoft.NETCore.App,用于Web应用程序的AspNetCore.App和用于运行桌面应用程序的.NET Core 3中的新共享框架WindowsDesktop.App。在Windows Forms和WPF上。最后两个Shared Framework本质上是对控制台应用程序所需的第一个的补充,也就是说,它们不携带全新的运行时,而只是用必要的库来补充现有的运行时。这种继承看起来在Shared Framework目录runtimeconfig.json中也有指定基本Shared Framework的目录

依赖清单(.deps.json



默认探测-.NET Core

第二个配置文件是这个.deps.json。该文件包含对应用程序或Shared Framework或库的所有依赖项的描述,库.deps.json也有此文件。它包含所有依赖项,包括可传递的依赖项。 .NET Core运行时的行为.deps.json因应用程序是否具有而有所不同。如果.deps.json不是,应用程序将能够加载所有的,这是它的共享框架或在其BIN目录中的组件。如果存在.deps.json,则启用验证。如果.deps.json列出其中一个程序集,则该应用程序将无法启动。您将看到上面显示的错误。如果应用程序尝试在运行时加载某些程序集,.deps.json 例如,如果使用程序集加载方法或在程序集的解析过程中,您将看到与“严格的程序集加载”非常相似的错误。

Jetbrains骑士


Rider是.NET IDE。并非所有人都知道Rider是一个IDE,由基于IntelliJ IDEA的前端(用Java和Kotlin编写)和后端组成。后端本质上是R#,可以与IntelliJ IDEA通信。该后端现在是一个跨平台的.NET应用程序。
它在哪里运行? Windows使用安装在用户计算机上的.NET Framework。在其他信息系统上,在Linux和Mac上,使用Mono。

当到处都有不同的运行时时,这不是一个理想的解决方案,我想进入下一个状态,以便Rider在.NET Core上运行。为了提高性能,因为在.NET Core中所有最新功能都与此相关联。减少内存消耗。现在,Mono如何处理内存存在问题。

切换到.NET Core可以使您放弃旧的,不受支持的技术,并允许对运行时中发现的问题进行一些修复。切换到.NET Core将允许您控制运行时的版本,即Rider将不再在用户计算机上安装的.NET Framework上运行,而是在特定版本的.NET Core上运行,该版本可以被禁止作为独立的部署。向.NET Core的过渡最终将允许使用专门在Core中导入的新API。

现在的目标是启动一个原型,然后启动它,只是检查它如何工作,潜在的故障点是什么,必须再次重写哪些组件,这需要进行全局处理。

难以将Rider转换为.NET Core的功能


即使未安装R#,Visual Studio也会从大型解决方案的内存不足中崩溃,大型解决方案中包含SDK样式* .csproj的项目SDK样式* .csproj是完全.NET Core重定位的主要条件之一。

这是一个问题,因为Rider基于R#,它们生活在同一个存储库中,R#开发人员希望使用Visual Studio在其产品中开发自己的产品以使其成为食品。在R#中,有一些链接需要特定的框架库。在Windows上,我们可以将Framework用于桌面应用程序,而在Linux和Mac上,Mock已经用于具有最小功能的Windows库。

决断


我们决定暂时保留旧版本*.csproj,在完整的Framework上进行组装,但是由于Framework和Core的程序集是二进制兼容的,因此可以在Core上运行它们。我们不会使用不兼容的功能,请手动添加所有必需的配置文件,并下载.NET Core依赖项的特殊版本(如果有)。

您必须去什么黑客?


一个技巧:我们想调用一个仅在Framework中可用的方法,例如,R#中需要此方法,而Core中则不需要。问题在于,如果没有方法,那么在JIT编译期间调用该方法的方法会更早MissingMethodException即,不存在的方法破坏了调用它的方法。

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

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


解决方案在这里:我们将不兼容的方法调用到单独的方法中。还有一个问题:这种方法可能会内联,因此我们用attribute标记它NoInlining

技巧二:我们需要能够在相对路径中加载程序集。我们为框架提供了一个程序集,为.NET Core提供了一个特殊版本。我们如何下载.NET Core的.NET Core版本?



他们会帮助我们的.deps.json。让我们看一下.deps.jsonSystem.Diagnostics.PerformanceCounter库。这样的图书馆在.deps.json。它有一个运行时部分,其中指示了库的一个版本及其相对路径。在这个库中,程序集将在所有运行时中加载,并且仅引发执行。例如,如果它在Linux上加载,则PerformanceCounter在Linux上的设计上不起作用,并且PlatformNotSupportedException从那里飞行。此.deps.json部分中还有一个runtimeTargets部分此处已指示该程序集的版本专门用于Windows,PerformanceCounter应该在该版本上工作。

如果我们使用运行时部分并在其中写入要加载的库的相对路径,这将无济于事。运行时部分实际上在NuGet包内设置了相对路径,而不是相对于BIN。如果我们在BIN中查找此程序集,则仅从那里使用文件名。 runtimeTargets部分已经包含一个诚实的相对路径,一个相对于BIN的诚实路径。我们将在runtimeTargets部分中为程序集规定一个相对路径。代替运行时标识符(这里是“ win”),我们可以选择另一个我们喜欢的标识符。例如,我们将运行时标识符写为“ any”,并且该程序集通常会在所有平台上加载。或者,我们将编写“ unix”,它将在Linux,Mac等上启动。

下一个技巧:我们想在Linux和Mac Mock上下载以构建WindowsBase。问题在于Microsoft.NETCore.App,即使我们不在Windows上,共享框架中也已经存在名为WindowsBase的程序集。在Windows共享框架上,Microsoft.WindowsDesktop.AppWindowsBase重新定义中的版本NETCore.App。让我们看一下.deps.json这些框架,更准确地看一下描述WindowsBase的那些部分。



区别在于:



如果某些库发生冲突并且存在多个库,则为.deps.json组成的对中选择最大的。 .NET指南说,只需要在Windows资源管理器中显示它,但不是,它属于assemblyVersionfileVersionfileVersion.deps.json。这是我知道的版本中规定的唯一案例.deps.jsonassemblyVersion并且fileVersion,被实际使用。在所有其他情况下,我看到的行为是,无论.deps.json写入什么版本,程序集都将继续加载。



第四hack。任务:前两个hack有一个.deps.json文件,我们只需要特定的依赖项即可。由于它们是.deps.json在半手动模式下生成的,因此我们有一个脚本,根据对应该到达那里的内容的一些描述,可以在构建过程中生成它,因此我们希望将其保持在.deps.json最小程度,以便我们能够理解其中的内容。我们要禁用验证,并允许下载BIN中未描述的程序集.deps.json

解决方案:在runtimeconfig中启用自定义配置。为了与.NET Core 1.0向后兼容,实际上需要此设置。

发现


因此,.runtime.json.deps.json.NET的核心-这是一种类似物App.configApp.config让您执行相同的操作,例如以相对的方式加载程序集。.deps.json如果情况非常复杂,则可以使用手动重写它,来在.NET Core上自定义程序集的加载。

调试程序集下载


我讨论了一些类型的问题,因此您需要能够调试装入程序集的问题。有什么可以帮助呢?首先,运行时编写有关如何加载程序集的日志。其次,您可以更仔细地观察飞向您的执行。您还可以关注运行时事件。

融合日志





返璞归真:使用Fusion Log Viewer调试模糊错误
Fusion

在.NET Framework中加载程序集的机制称为Fusion,它知道如何将所做的操作记录到磁盘上。要启用日志记录,您需要向注册表添加特殊设置。这不是很方便,因此使用实用程序(即Fusion Log Viewer和Fusion ++)有意义。 Fusion Log Viewer是Visual Studio随附的标准实用程序,可以从Visual Studio命令行Visual Studio开发人员命令提示符启动。 Fusion ++是此工具的开源类似物,具有更好的界面。



Fusion Log Viewer看起来像这样。这比WinDbg更糟糕,因为此窗口甚至不会拉伸。尽管如此,您仍可以在此处打上对勾标记,尽管并不总是很明显哪个对勾标记是正确的。



Fusion ++具有一个“开始记录”按钮,然后出现“停止记录”按钮。在其中,您可以查看有关加载程序集的所有记录,并阅读有关实际发生情况的日志。这些日志以简洁的方式看起来像这样。



这是严格装配加载的豁免。如果我们查看Fusion日志,将会看到在处理完所有配置后需要下载9.0.0.0版本。我们发现一个文件中怀疑有我们需要的程序集。我们看到此文件中有6.0.0.0版。我们有一个警告,我们比较了程序集的全名,并且它们的主要版本不同。然后发生错误-版本不匹配。

运行时事件





记录运行时事件

在Mono上,您可以启用使用环境变量进行记录,并且最终会将日志写入stdoutstderr。不太方便,但是解决方案有效。



默认探测-.NET Core
文档/设计文档/主机跟踪

.NET Core还具有一个特殊的环境变量COREHOST_TRACE,其中包括登录stderr。使用.NET Core 3.0,您可以通过在变量中指定文件的路径来将日志写入文件COREHOST_TRACEFILE


当程序集无法加载时,将触发一个事件。这是一个事件AssembleResolve。还有第二个有用的事件this FirstChanceException。您可以订阅它,并得到关于加载程序集的错误,即使有人写了try..catch并错过了所有执行的地方FileLoadException发生了。如果应用程序已经编译过,则可以启动它perfview,并且它可以监视.NET执行,并在那里可以找到与下载文件有关的内容。

发现


将工作转移到工具,开发工具,IDE,MSBuild,这使您可以生成重定向。您可以切换到.NET Core,然后您将忘记什么是“严格的程序集加载”,并且您将能够使用新的API,就像我们想要在Rider中实现它一样。如果连接.NET Standard库,则将.NET Framework的目标版本提高到至少4.7.1。如果您似乎处在绝望的境地,请寻找黑客,使用它们,或者针​​对绝望的情况提出自己的骇客。并使用调试工具武装自己。

我强烈建议您阅读以下链接:



DotNext 2020 Piter . , 8 JUG Ru Group.

All Articles