深入 .NET Core 基础 - 2:共享框架
原文地址:https://natemcmaster.com/blog/2018/08/29/netcore-primitives-2/
共享框架从 .NET Core 1.0 就成为基础部分。ASP.NET Core 从 .NET Core 2.1 开始也作为共享框架发布。你可能没有注意到该进展是否顺利。但是,这里有一些关于该设计的颠簸和讨论。本文将深入到共享框架,并探讨它的一些常见陷阱。
1. 基础
.NET Core 应用程序有两种运行模型:基于框架或者自包含。在我的 MacBook 上,最小的自包含 ASP.NET Core 应用程序的尺寸是 83MB 和 350 个文件。另一方面,最小的框架依赖应用的尺寸是 239KB 和 5 个文件。
可以通过下面的命令来生成两种应用程序
1
2
3
|
dotnet new web dotnet publish --runtime osx-x64 --output bin /self_contained_app/ dotnet publish --output bin /framework_dependent_app/ |
在应用程序运行的时候,两种模式的功能是等效的。所以为什么存在不同类型的模型?如微软的文档所述:
框架依赖的发布基于共享的系统范围的 .NET Core 版本......
而自包含的发布不依赖与目标系统上的共享组件。所有的组件......都包含在应用程序中。
该文档非常好地解释了每种模式的优点。
2. 共享框架
长短短说,.NET Core 共享框架是一个包含程序集 (*.dll 文件) 的,不在应用程序文件夹中的文件夹。这些程序集一起版本化和发布。该文件夹是 "共享的系统范围的 .NET Core 版本" 的一部分,通常在 C:/Program Files/dotnet/shared
文件夹中。
当你执行 dotnet.exe WebApp.dll
的时候,.NET Core 宿主 必须:
-
发现你的应用所依赖的名称和版本
-
在公共位置找到这些依赖内容
这些依赖可以在多个位置发现,包括,但是不限于,这些共享框架。在上一篇文章中,我已经总结了 deps.json
和 runtimeconfig.json
文件是如何配置宿主的行为。请查看它来得到更详细的说明。
.NET Core 宿主读取 *.runtimeconfig.json
文件来得到需要加载哪个共享框架。其内容可能类似于如下:
1
2
3
4
5
6
7
8
|
{ "runtimeOptions" : { "framework" : { "name" : "Microsoft.AspNetCore.App" , "version" : "2.1.1" } } } |
共享框架名称 只是一个名称。根据约定,该名称以 App
结束,但可以是任何名称,比如 "FooBananaShark"。
共享框架版本
2.1 我已经安装的共享框架是哪些?
执行 dotnet --list-runtimes
。它将会显示名称、版本和共享框架的位置。对于 .NET Core 3.1,共享框架的列表如下所示。
1
2
3
4
|
dotnet --list-runtimes Microsoft.AspNetCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 3.1.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] |
2.2 比较 Microsoft.NETCore.App,AspNetCore.App 和 AspNetCore.All
在 .NET Core 2.2 中,有如下三种共享框架:
框架名称 | 说明 |
---|---|
Microsoft.NETCore.App | 基础运行时. 它支持类似 System.Object , List , string , 内存管理,文件和网络 I/O,线程等等 |
Microsoft.AspNetCore.App | 默认的 Web 运行时. 它导入了 Microsoft.NETCore.App, 并添加了使用 Kestrel Mvc、SingalR、Razor 和部分 EF Core 构建 HTTP 服务的 API |
Microsoft.AspNetCore.All | 集成了第三方内容。它导入了 Microsoft.AspNetCore.App. 加入了对 EF Core + SQLite 支持, 使用 Redis 的扩展, 从 Azure Key Vault 进行配置, 以及更多内容. (在 .NET Core 3.0 中将被退役 deprecated in 3.0.) |
.NET Core 3.0 增加了 Microsoft.WindowsDesktop.App,并删除了
Microsoft.AspNetCore.All
。
2.3 与 NuGet package 的关系
.NET Core SDK 生成 runtimeconfig.json
文件。在 .NET Core 1 和 2 中,它使用项目文件中的两个片段来决定该文件中框架部分的内容:
-
MicrosoftNETPlatformLibrary
属性。默认对于所有的 .NET Core 项目设置为Microsoft.NETCore.App
-
NuGet 恢复的结果,它必须包含一个同名的包
对于所有的项目,.NET Core SDK 对 Microsoft.NETCore.App
添加隐式的包引用。ASP.NET Core 默认设置 MicrosoftNETPlatformLibrary
为 Microsoft.AspNetCore.App
此 NuGet 包,实际上,并不提供共享框架。重复一遍,这个 NuGet 包 不提供共享框架(后面我还会再次重复)。该 NuGet 包仅仅为编译器提供 API 集和很少的其它 SDK 部分。共享框架文件来自于安装的运行时,或者在 Visual Studio 中打包,Docker 映像,以及一些 Azure 服务。
2.4 版本前滚
如前所述,runtimeconfig.json 是最小版本号。实际使用的版本基于版本前滚策略。常见的方式:
-
如果某个应用程序的最小版本是 2.1.0,那么 2.1.* 的最高版本将会被应用
我将会在下一篇详细说明。
2.5 层化的共享框架
此特性在 .NET Core 2.1 被加入。
共享框架可以依赖于其它的共享框架。它被引入来支持 ASP.NET Core,它被从包的运行时存储转换成了共享框架。
例如,如果你进入并查看 $DOTNET_ROOT/shared/Microsoft.AspNetCore.All/$version/
文件夹,你将会看到一个 Microsoft.AspNetCore.All.runtimeconfig.json
文件。
1
2
3
4
5
6
7
8
9
|
{ "runtimeOptions" : { "tfm" : "netcoreapp2.1" , "framework" : { "name" : "Microsoft.AspNetCore.App" , "version" : "2.1.2" } } } |
在 .NET Core 3.1 中,内容如下:
1
2
3
4
5
6
7
8
9
10
|
{ "runtimeOptions" : { "tfm" : "netcoreapp3.1" , "framework" : { "name" : "Microsoft.NETCore.App" , "version" : "3.1.0" }, "rollForward" : "LatestPatch" } } |
2.6 多层查找
此特性在 .NET Core 2.0 加入。
宿主会探测多个位置来寻找合适的共享框架。查找从 dotnet root 开始,这是包含 dotnet
可执行程序的文件夹。它可以被环境变量 DOTNET_ROOT
所指定的文件夹覆盖。第一个探测的位置是
$DOTNET_ROOT/shared/$name/$version
如果没有合适的文件夹存在,将会使用 多层查找 试图查看预定义的全局位置。此特性可以通过环境变量 DOTNET_MULTILEVEL_LOOKUP=0
来关闭。默认的全局位置是:
OS | Location |
---|---|
Windows | C:\Program Files\dotnet (64-bit processes) C:\Program Files (x86)\dotnet (32-bit processes) (See in the source code) |
macOS | /usr/local/share/dotnet (source code) |
Unix | /usr/share/dotnet (source code) |
宿主将检测的目录位于:
$GLOBAL_DOTNET_ROOT/shared/$name/$version
2.7 ReadyToRun
共享框架中的程序集语境使用名为 crossgen
的工具进行了预优化。该过程生成了 ReadyToRun
版本的程序集,其对特定版本的操作系统和 CPU 架构进行了优化。主要的性能收益在于缩短了 JIT 花费在启动阶段的代码准备时间。
3. 陷阱
我想每个 .NET Core 开发者都可能在某个时候落入某个陷阱中。我将试图说明它是如何发生的。
3.1 HTTP Error 502.5 Process Failure
当在 IIS 上寄宿 ASP.NET Core 或者在 Azure 的 Web Services 上寄宿的时候,这是最常见的问题。典型发生在开发者升级项目之后,或者部署到一台最近没有更新的机器上。实际的错误来自于共享框架没有被找到,导致 .NET Core 应用程序不能启动。当 .NET Core 不能运行应用程序,IIS 输出 502.5
错误,但是并没有暴露内部的错误信息。
3.2 “The specified framework was not found”
1
2
3
4
5
6
7
8
9
10
11
|
It was not possible to find any compatible framework version The specified framework 'Microsoft.AspNetCore.App' , version '2.1.3' was not found. - Check application dependencies and target a framework version installed at: /usr/local/share/dotnet/ - Installing .NET Core prerequisites might help resolve this problem: http: //go .microsoft.com /fwlink/ ?LinkID=798306&clcid=0x409 - The .NET Core framework and SDK can be installed from: https: //aka .ms /dotnet-download - The following versions are installed: 2.1.1 at [ /usr/local/share/dotnet/shared/Microsoft .AspNetCore.App] 2.1.2 at [ /usr/local/share/dotnet/shared/Microsoft .AspNetCore.App] |
此错误通常潜伏在 HTTP 502.5 之后,或者 Visual Studio 测试管理器的错误中。
它发生在 runtimeconfig.json 文件中指定了特定的框架名称和版本,而宿主不能使用多级查找和前向错略找到对应的版本。如前所述。
3.3 为 Microsoft.AspNetCore.App 更新 NuGet 包
NuGet 中的 Microsoft.AspNetCore.App
包不提供共享框架。仅仅提供用于 C#/ F# 编译器的 API 和一些 SDK 支持。你必须单独下载并安装共享框架。
另外,基于前滚策略,你也不必升级 NuGet 包的版本来使得你的应用程序运行在更新的共享框架版本上。
将共享框架表现为项目文件中的一个 NuGet 包可能是 ASP.NET Core 团队的一个设计错误。表现共享框架的包并不是一个正常的包。不像多数的其它包,它不是自满足的。我们有理由期待在项目使用 <PackageReference>
引用某个包的时候,NuGet 能够安装任何所需要的内容,令人沮丧的是,这里的包偏离了该模式。有多个提案建议修复该问题。我期望某个提案很快落地。
3.4 <PackageReference Include="Microsoft.AspNetCore.App" />
所有其它的 <PackageReference> 都必须包含 Version
属性。缺失版本的包引用仅仅工作于项目开始部分使用 <Project Sdk="Microsoft.NET.Sdk.Web">。且仅仅工作于 Microsoft.AspNetCore.{App, All}
包。Web SDK 将基于项目中的其它值自动提取这些包的版本,例如 <TargetFramework> 和 <RuntimeIdentifier>
如果你为包引用元素指定了版本的话,或者你没有使用 Web SDK,该魔法将不会工作。很难建议一个好的解决方案,因为最佳的方式依赖于你理解的水平和项目的类型。
3.5 发布修剪
当你使用 dotnet publish
创建一个框架依赖的应用程序时,SDK 使用 NuGet 恢复结果来决定哪个程序集将会包含到发布文件夹中。有些从 NuGet 包中复制过来,有些不会,因为它们被期望存在于共享框架中。
这很容易导致错误,因为 ASP.NET Core 作为共享框架存在,也作为 NuGet 包存在。修剪使用进行某些图匹配来决定传递依赖、升级等等,来选取正确的文件。
例如,对于下面的项目
1
2
|
<PackageReference Include= "Microsoft.AspNetCore.App" Version= "2.1.1" /> <PackageReference Include= "Microsoft.AspNetCore.Mvc" Version= "2.1.9" /> |
MVC 实际上是 Microsoft.AspNetCore.App 的一部分,但是,当执行 dotnet publish
的时候,它发现你的项目决定 升级 Microsoft.AspNetCore.Mvc.dll
到比 Microsoft.AspNetCore.App
的版本 2.1.1 更高的版本,所以,它将会把 Mvc.dll 放到发布目录中。
这样是不理想的,因为你的应用程序尺寸变得更大了,并且你没有得到 ReadyToRun
优化之后的 Microsoft.AspNetCore.Mvc.dll
。如果你通过 ProjectReference 传递升级或者通过第三方依赖升级,就会无意中发生。
3.6 困惑于共享框架的目标框架名称
很容易认为:"netcoreapp2.0" == "Microsoft.NETCore.App, v2.0.0"
。但这不是真的。目标框架名称 (也称为 TFM) 在项目文件中使用 <TargetFramework> 指定。"netcoreapp2.0" 是一个友好的,你所使用的 .NET Core 版本名称。
这个 TFM 的缺陷在于它太短了。它不能说明像多个共享框架这样的问题,特定版本的补丁,版本前滚,输出类型,以及自包含和框架依赖的发布等等。SDK 将试图从 TFM 来推断这些设置,但它不能推断所有的事情。
所以,精确地说,“netcoreapp2.0” 表示至少 V2.0.0 的 "Microsoft.NETCore.App“
3.7 困惑的项目设置
最后一个提醒的陷阱是项目设置。许多术语和设置的名称并不确切。使用令人困惑的术语,所以,如果你搞混了它们,这并不是你的过错。
下面,我列出常见的项目设置,以及实际的含义。
<PropertyGroup> <TargetFramework>netcoreapp2.1</TargetFramework> <!-- Actual meaning: * The API set version to use when resolving compilation references from NuGet packages. --> <TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks> <!-- Actual meaning: * Compile for two different API version sets. This does not represent multi-layered shared frameworks. --> <MicrosoftNETPlatformLibrary>Microsoft.AspNetCore.App</MicrosoftNETPlatformLibrary> <!-- Actual meaning: * The name of the top-most shared framework --> <RuntimeFrameworkVersion>2.1.2</RuntimeFrameworkVersion> <!-- Actual meaning: * version of the implicit package reference to Microsoft.NETCore.App which then becomes the _minimum_ shared framework version. --> <RuntimeIdentifier>win-x64</RuntimeIdentifier> <!-- Actual meaning: * Operating system kind + CPU architecture --> <RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers> <!-- Actual meaning: * A list of operating systems and CPU architectures which this project _might_ run on. You still have to select one by setting RuntimeIdentifier. --> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.2" /> <!-- Actual meaning: * Use the Microsoft.AspNetCore.App shared framework. * Minimum version = 2.1.2 --> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.2" /> <!-- Actual meaning: * Use the Microsoft.AspNetCore.Mvc package. * Exact version = 2.1.2 --> <FrameworkReference Include="Microsoft.AspNetCore.App" /> <!-- Actual meaning: * Use the Microsoft.AspNetCore.App shared framework. (This is new and unreleased...see https://github.com/dotnet/sdk/pull/2486) --> </ItemGroup>
4. 总结
共享框架是 .NET Core 一个可选特性,我可以合理地认为多数的用户憎恨陷阱。我仍然认为对于 .NET Core 开发者来说,理解背后发生了什么是有用的。并期望这是关于共享框架的有用的总结。我还给出了官方的链接和指南,以便帮助你找到更多信息。如果有任何问题,欢迎留言。
More
-
Packages, metapackages and frameworks: https://docs.microsoft.com/en-us/dotnet/core/packages
-
.NET Core application deployment: https://docs.microsoft.com/en-us/dotnet/core/deploying/. Especially read the part about “Framework-dependent deployments (FDD)”
-
Specs on runtimeconfig.json and deps.json:
https://github.com/dotnet/cli/blob/v2.1.400/Documentation/specs/runtime-configuration-file.md
-
The implementation of the shared framework lookup: https://github.com/dotnet/core-setup/blob/v2.1.3/src/corehost/cli/fxr/fx_muxer.cpp#L464