深刻理解.NET Core的基元(二) - 共享框架

原文:Deep-dive into .NET Core primitives, part 2: the shared framework
做者:Nate McMaster
译文:深刻理解.NET Core的基元(二) - 共享框架
做者: Lamond Luhtml

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

前言

自.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。github

你能够以下命令行生成基于两种不一样模式的应用。web

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

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

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

这篇官方文档(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开发人员来讲,了解背后的原理是一件好事,但愿本文是对共享框架功能的良好概述。我尽量的关联了一些官网文档和指南,以便你能够找到更多的信息。若是还有其余问题,请在下面发表评论。

相关文章
相关标签/搜索