原文: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的基元(一):deps.json, runtimeconfig.json, dll文件
- 深入理解.NET Core的基元(二):共享框架
- 深入理解.NET Core的基元(三):深入理解runtimeconfig.json
前言
自.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.json
和runtimeconfig.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.Object 、List<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開發人員來說,了解背后的原理是一件好事,希望本文是對共享框架功能的良好概述。我盡可能的關聯了一些官網文檔和指南,以便你可以找到更多的信息。如果還有其他問題,請在下面發表評論。