原文:Deep-dive into .NET Core primitives, part 2: the shared framework

作者:Nate McMaster

译文:深入理解.NET Core的基元(二) - 共享框架

作者: Lamond Lu

本篇是之前翻译过的《深入理解.NET Core的基元: deps.json, runtimeconfig.json, dll文件》的后续,这个系列作者暂时只写了3篇,虽然有一些内容和.NET Core 3.0已经不兼容了,但是大部分的原理还都是相通的,所以后面的第三篇我也会翻译。

前言

自.NET Core 1.0起,共享框架(Shared Framework)就已经成为了.NET Core的重要组成部分。自.NET Core 2.1起,ASP.NET Core就已经作为共享框架的第一次出现。你可能从来注意过这一点,但是在设计它的时候,我们经历了许多反复和持续的讨论。在本篇文章中,我们将深入共享框架并讨论一些开发人员经常遇到的一些陷阱。

基础部分

.NET Core应用可以在两种模式下运行, 分别是框架依赖模式(Framework - Dependent) 和独立运行模式(Self Contained) 。在我的Macbook上,一个最小的可独立运行的ASP.NET Core网站应用,大约拥有350个文件,文件大小总共是93MB。相对的,一个最小的框架依赖应用,大约5个文件,文件大小总共239KB。

你可以如下命令行生成基于两种不同模式的应用。

dotnet new web
dotnet publish --runtime osx-x64 --output bin/self_contained_app/
dotnet publish --output bin/framework_dependent_app/

当程序运行的时候,他们的功能是一样的。那么这两种模式有什么区别么?其实正如官网文档中的解释:

框架依赖部署(framework-dependant deployment) 依赖目标中安装的.NET Core共享组件。独立部署(self-contained deployment)不依赖目标系统中安装的共享组件,程序所需的所有组件都已经包含在当前应用程序中。

这篇官方文档(https://docs.microsoft.com/en-us/dotnet/core/deploying/)中很好的解释了不同模式的优势。

PS: 作者当时写这边文章的时候, 没有引入Framework-dependent executables (FDE),有兴趣的同学可以自行查看。

共享框架

这里,简单的说,.NET Core的共享框架就是一个程序集(*.dll文件)集合的目录,这些程序集不需要出现在你的.NET Core的应用目录中。这个目录是.NET Core的共享系统范围版本的一部分,通常你可以在C:\Program Filres\dotnet\shared中发现它。

当你运行dotnet.exe WebApi1.dll命令时,.NET Core宿主程序会

  • 尝试发现你的应用依赖的程序集名称和版本
  • 在某些固定位置中尝试查找该程序集

这些程序集可以在许多不同的位置被发现了,包含且不限于共享框架。在我之前的文章中,我主要解释了如果通过deps.jsonruntimeconfig.json文件配置宿主程序的行为。希望了解更多的同学,可以查看那篇文章。

.NET Core宿主程序会读取*.runtimeconfig.json文件来确定加载哪个版本的共享框架。这个文件的内容类似:

{
"runtimeOptions": {
"framework": {
"name": "Microsoft.AspNetCore.App",
"version": "2.1.1"
}
}
}

这里,共享框架名称只是一个名字。按照约定,这个名字应该是以.App结尾的,但是实际上它可以是任何字符串,例如"FooBananaShark"。

对于共享框架的版本,这里只是配置了一个最低的版本。.NET Core宿主程序会根据配置,加载对应版本的共享框架,或者更高版本的共享框架,但是它永远不会加载比指定版本低的共享框架。

那么,我到底安装了哪些共享框架呢?

运行dotnet --list-runtimes, 你就可以看到你电脑中安装了哪些共享框架,以及它们的版本和文件位置。

对比Microsoft.NETCore.App, AspNetCore.App以及AspNetCore.All

这里,以.NET Core 2.2为例。

框架名称 描述
Microsoft.NETCore.App 基础运行时。它主要提供了System.ObjectList<T>string类,以及内存管理,文件管理,网络I/O, 线程管理等功能
Microsoft.AspNetCore.App 默认Web运行时。它主要提供了使用API创建Web服务器的功能,这里主要包含Kestral, Mvc, SignalR, Razor, 以及EF Core的部分功能。
Microsoft.AspNetCore.All 与第三方的集成库。它追加了EF Core + Sqlite的支持,以及一些扩展功能, 例如Redis, Azure Key Valut等。(在.NET Core 3.0中已经不再使用)

共享框架与Nuget包的关系

.NET Core SDK生成了runtimeconfig.json文件。在.NET Core 1和2中,SDK使用了项目配置中的两部分来确定runtimeconfig.json文件中框架部分内容。

  • MicrosoftNETPlatformLibrary属性。对于所有.NET Core项目,它默认是Microsoft.NETCore.App
  • Nuget包管理工具的还原结果集,结果集中可能包含了相同名称的包

这里针对所有的.NET Core项目, .NET Core SDK都会添加一个隐式的包来引用Microsoft.NETCore.App。ASP.NET Core通过修改默认配置MicrosoftNETPlatformLibrary, 将其改为Microsoft.AspNetCore.App

但是这里需要注意,Nuget包管理工具不提供任何共享框架!不提供任何共享框架! 不提供任何共享框架! 重要的事情说三遍_。Nuget包管理工具只提供编译器使用的一些API,以及少量SDK。共享框架的获取来源可以是运行时安装器 https://aka.ms/dotnet-download, 或者捆绑在Visual Studio中,Docker镜像中,以及一些Azure服务器中。

版本前滚策略

正如我上面提到的,runtimeconfig.json只是指定了一个最小版本。实际使用的版本会依赖于一个版本前滚策略(详细内容可以参阅官方文档。例如

  • 如果应用使用的共享框架最小版本是2.1.0, 那么程序最高会加载的共享框架版本是2.1.*。

针对这一部分,可以参见《深入理解.NET Core的基元(三):深入理解runtimeconfig.json》

作者:《深入理解.NET Core的基元(三):深入理解runtimeconfig.json》后续会补上

分层的共享框架

在.NET Core 2.1版本中引入了分层共享框架的特性。

共享框架可以依赖于其他共享框架。引入此特性是为了支持ASP.NET Core, 这个特性可以将程序包的运行时存储转换为一个共享框架。

如果你查看一下$DOTNET_ROOT/shared/Microsoft.AspNetCore.All/$version/文件夹,你会发现一个名为Microsoft.AspNetCore.All.runtimeconfig.json的文件,其内容如下

$ cat /usr/local/share/dotnet/shared/Microsoft.AspNetCore.All/2.1.2/Microsoft.AspNetCore.All.runtimeconfig.json
{
"runtimeOptions": {
"tfm": "netcoreapp2.1",
"framework": {
"name": "Microsoft.AspNetCore.App",
"version": "2.1.2"
}
}
}

多级检索

在.NET Core 2.0中引入了多级检索特性。

宿主程序在启动时会探查多个位置,以寻找合适的共享框架。程序首先会查找dotnet根目录,即包含一个dotnet.exe可执行文件的目录。这里我们可以通过配置DOTNET_ROOT的环境变量来覆盖此配置。根据此配置,程序检索的第一个目录是:

$DOTNET_ROOT/shared/$name/$version

如果这个目录不存在,宿主程序会尝试使用多级检索机制,检索预定的全局路径列表。这个机制可以通过设置全局变量DOTNET_MULTILEVEL_LOOKUP=0来关闭。默认情况下,预定的全局路径列表如下:

OS Location
Windows C:\Program Files\dotnet (64位进程) C:\Program Files (x86)\dotnet (32位进程) (查看源代码)
macOS /usr/local/share/dotnet (查看源代码)
Unix /usr/share/dotnet (查看源代码)

最终宿主程序会在找到的全局目录中检索以下目录

$GLOBAL_DOTNET_ROOT/shared/$name/$version

ReadyToRun特性

共享框架中的程序集,都是经过crossgen工具预优化过的。使用这个工具可以生成"ReadyToRun"版本的程序集,这些程序集都是针对指定操作系统和CPU架构优化过的。这里主要的性能提升是,减少了JIT在启动时准备代码所花费的时间。

Crossgen相关文档:https://github.com/dotnet/coreclr/blob/v2.1.3/Documentation/building/crossgen.md

一些陷阱

我相信每个.NET Core程序员都会遇到以下陷阱中的一部分。我将尽力解释这些问题是如何产生的。

Http Error 502.5 Process Failure

到目前为止,开发人员,最常遇到的陷阱是在IIS中或者Azure Web Services中托管ASP.NET Core应用程序。这个问题通常发生在开发人员升级了一个项目,或者当应用部署的时候,目标机器没有更新。这个错误的真正原因通常是应用所需版本的共享框架找不到,导致.NET Core应用程序无法正常启动。当dotnet无法启动应用程序时,IIS会返回HTTP 502.5的错误,但是不会显示内部的错误消息。

"The specified framework was not found"

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 Test Explorer故障。

如上所述,当runtimeconfig.json文件指定了一个框架名称和版本,但是经过多级检索特性和前滚策略之后,主机依然无法找到一个合适的框架版本的时候,就会出现以上错误。

升级Microsoft.AspNetCore.App程序集的Nuget包

Microsoft.AspNetCore.App程序集的Nuget包,并不提供共享框架。它只是提供了C#/F#编译器使用的一些API以及少量SDK. 所以你必须要单独下载并安装共享框架。

同时,由于前滚策略,你并不需要更新Nuget包的版本,来让你的程序运行在新版本的共享框架中。

这可能是ASP.NET Core团队的一个设计失误,我们无法将共享框架作为Nuget包出现在项目文件中。共享框架所提供的程序包并不是通常意义上的程序包。与大部分程序包不同,它们不是自给自足的。我们希望当项目使用<PackageReference>时,Nuget能够安装所需的所有引用,但是令人沮丧的是,这些特殊程序包的设计偏离我们期望的模式。当然,现在我们已经得到了各种建议来解决这个问题。我希望它能早日实现。

在ASP.NET Core 2.1的新项目模板和文档中,微软向开发人员展示了,他们只需要在项目文件中添加如下的一行代码。

<PackageReference Include="Microsoft.AspNetCore.App" />

其他的<PackageReference>引用代码都必须要包含Version属性。只有当项目文件是以<Project Sdk="Microsoft.NET.Sdk.Web">开头的,那么以上这句与版本无关的程序包引用才会起作用,并且这里只对Microsoft.AspNetCore.{App, All}程序集包起作用。Web SDK将根据项中的其他配置, 例如:<TargetFramework><RuntimeIdentifier>, 来自动选择一个合适的程序包版本。

如果你在包引用的部分加入的Version属性,并指定了版本,或者你没有使用Web SDK作为项目文件的开头,则无法使用此功能。这里我很难推荐一个好的解决方案,因为最好的实现方式是基于你对此的理解水平和项目类型的。

发布修剪(Publish Trimming)

当你使用dotnet publish命令发布一个框架依赖的应用时,SDK会使用Nuget的还原结果(restore result)来决定哪些程序集应该出现在发布目录中。有一些程序集是通过Nuget程序集包拷贝的,而有一些就不是,因为他们已经出现在共享框架中。

这很容易产生一些错误,因为ASP.NET Core作为共享框架和Nuget程序包都是可用的。项目发布修剪特性会尝试通过图形数学来检查依赖传递,以及升级等,并以此选择正确的程序包文件。

下面我们以如下的项目引用为例:

<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版本要高,所以最终Microsoft.AspNetCore.Mvc.dll会被拷贝到发布目录中。

这就不太理想了,因为随着你的应用程序大小不断增长,你永远得不到ReadyToRun优化版本的Microsoft.AspNetCore.Mvc.dll

PS: 这个问题以前很少注意到,不过真的很常见。

混淆目标框架的别称与共享框架

如果简单认为"netcoreapp2.0" == "Microsoft.NETCore.App, v2.0.0", 你就大错特错了。目标框架别称(Target Framework Moniker简称TFM)只通过项目文件中<TargetFramework>节点指定的。"netcoreapp2.0"只是希望以人类友好的方式来表达你要使用哪个版本的.NET Core。

TFM的陷阱在于它的名称太短了。它不能表达出多种共享框架,特定补丁的版本控制,版本前滚,输出类型以及是独立发布还是框架依赖发布等内容。SDK会尝试从TFM推断许多设置,但是无法推断所有内容。

所以,更准确的说“netcoreapp2.0”意味着"Microsoft.NETCore.App v2.0.0及以上版本"。

混淆项目配置

最后一个陷阱和项目配置有关。在这里有很多术语和配置名称,它们不总是一致的。这些术语非常令人困惑,因此,如果混淆了这些术语,也没有关系,那不是你的错。

下面,我就列出一些常见的项目设置及其实际含义。

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<!--
实际意义:
* 从Nuget包解析编译引用时使用的API集合的版本
--> <TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks>
<!--
实际意义:
* 使用两个不同的API集合版本进行编译。但这并不代表多层共享框架
--> <MicrosoftNETPlatformLibrary>Microsoft.AspNetCore.App</MicrosoftNETPlatformLibrary>
<!--
实际意义:
* 最顶层的共享框架名称
--> <RuntimeFrameworkVersion>2.1.2</RuntimeFrameworkVersion>
<!--
实际意义:
* 指定了Microsoft.AspNetCore.App程序包的版本,这个版本就是最小的共享框架版本
--> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<!--
实际意义:
* 操作系统种类 + CPU架构
--> <RuntimeIdentifiers>win-x64;win-x86</RuntimeIdentifiers>
<!--
实际意义:
* 运行此项目可能使用的操作系统种类和CPU架构列表,你必须要通过RuntimeIdentifier配置选择其中一个
--> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.2" />
<!--
实际意义:
* 使用Microsoft.AspNetCore.App作为共享框架
* 最低版本2.1.2
--> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.2" />
<!--
实际意义:
* 引用Microsoft.AspNetCore.Mvc程序包
* 实际版本2.1.2
--> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<!--
实际意义:
* 使用Microsoft.AspNetCore.App作为共享框架.
--> </ItemGroup>

总结

共享框架作为.NET Core的可选功能,尽管存在缺陷,但是我认为对于绝大部分用户来说,这是一个合理的默认设置。我依然认为对于.NET Core开发人员来说,了解背后的原理是一件好事,希望本文是对共享框架功能的良好概述。我尽可能的关联了一些官网文档和指南,以便你可以找到更多的信息。如果还有其他问题,请在下面发表评论。

深入理解.NET Core的基元(二) - 共享框架的更多相关文章

  1. 深入理解.NET Core的基元(二)

    原文:Deep-dive into .NET Core primitives, part 2: the shared framework作者:Nate McMaster译文:深入理解.NET Core ...

  2. 深入理解.NET Core的基元(三) - 深入理解runtimeconfig.json

    原文:Deep-dive into .NET Core primitives, part 3: runtimeconfig.json in depth 作者:Nate McMaster 译文:深入理解 ...

  3. 深入理解.NET Core的基元: deps.json, runtimeconfig.json, dll文件

    原文链接: Deep-dive into .NET Core primitives: deps.json, runtimeconfig.json, and dll's 作者: Nate McMaste ...

  4. 深入理解net core中的依赖注入、Singleton、Scoped、Transient(二)

    相关文章: 深入理解net core中的依赖注入.Singleton.Scoped.Transient(一) 深入理解net core中的依赖注入.Singleton.Scoped.Transient ...

  5. 读经典——《CLR via C#》(Jeffrey Richter著) 笔记_基元类型(二)

    [基元类型推荐] 推荐直接使用 FCL 类型. [理由] 编码时不至于困惑string与String的使用.由于C#的stirng(一个关键字)直接映射到System.String(一个 FCL 类型 ...

  6. 谈谈C#基元类型

    首先看一下.NET 中的基元类型,如下表: C# Type | .NET Framework Type -------------| ---------------------- bool | Sys ...

  7. .Net Core 中的包、元包与框架(Packages, Metapackages and Frameworks)

    包,元包与框架 本文翻译自 Packages, Metapackages and Frameworks. .Net Core 是一种由 NuGet 包组成的平台.一些产品体验受益于代码包的细粒度定义, ...

  8. .NET Core中的包、元包与框架

    本文为翻译文章,原文:Packages, Metapackages and Frameworks .NET Core是一个由NuGet包组成的平台.一些产品受益于细粒度包的定义,也有一些受益于粗粒度包 ...

  9. 深入理解net core中的依赖注入、Singleton、Scoped、Transient(三)

    相关文章: 深入理解net core中的依赖注入.Singleton.Scoped.Transient(一) 深入理解net core中的依赖注入.Singleton.Scoped.Transient ...

随机推荐

  1. 03-body标签中的部分标签

    一.字体标签 标题标签h1-h6 h1定义最大的标题,h6定义最小的标题,一般一个页面中h1只能出现一次,尽量标题不要超过三级.h标签具有align属性,属性值分别是:left.center.righ ...

  2. Centeos7部署Flask+Gunicorn+nginx

    一.环境安装 pip3 install flask pip3 install gunicorn pip3 install nginx 二.模块介绍 1.Flask是一个使用 Python 编写的轻量级 ...

  3. 004-python面向对象,错误,调试和测试

    ---恢复内容开始--- 1.面向对象 面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想.OOP把对象作为程序的基本单元,一个对象包含了数据和操作 ...

  4. 你竟然不装油猴插件-Chrome神器TamperMonkey

    油猴插件是一款可以在chrome浏览器中使用油猴脚本的插件.理解为脚本运行的平台 脚本 是一段代码,安装之后,有些脚本能为网站添加新的功能,有些能使网站的界面更加易用,有些则能隐藏网站上烦人的部分内容 ...

  5. git clone 解决Permission Denied (publickey)问题

    本地git bash 使用git clone git@github.com:***.git方式下载github代码至本地时需要依赖ssh key,遇到权限不足问题时一般都是SSH key失效或者SSH ...

  6. ThinkPHP5 支付宝 电脑与手机支付扩展库

    ThinkPHP5 电脑与手机支付扩展库(2017年9月18日) 使用说明 在默认配置情况下,将文件夹拷贝到根目录即可, 其中extend目录为支付扩展目录, application\extra\al ...

  7. HTML5 相关扩展

    一.与类相关的扩展 class属性的应用极其广泛,与class的相关的操作也越来越简化,HTML5增加了 getElementsByClassName来查找元素,通过也增加了classList属性,方 ...

  8. 【Java基础】让编码不再让你困惑

    目录 1. ASCII编码 2. Unicode编码 3. UTF-8编码 4. UTF8.UTF16和UTF32之间的区别 5. GBK.GB2312和GB18030之间的区别 6. Java中的编 ...

  9. Flask学习之旅--Flask项目部署

    一.写在前面 Flask 作为一个轻量级的 Web 框架,具有诸多优点,灵活方便,扩展性强,开发文档也很丰富.在开发调试的过程中,我们往往会使用 Flask 自带的 Web 服务器,但如果要投入到生产 ...

  10. Spring Boot2 系列教程(十一)Spring Boot 中的静态资源配置

    当我们使用 SpringMVC 框架时,静态资源会被拦截,需要添加额外配置,之前老有小伙伴在微信上问松哥 Spring Boot 中的静态资源加载问题:"松哥,我的 HTML 页面好像没有样 ...