1. 程式人生 > >深入 .NET Core 基礎 - 2:共享框架

深入 .NET Core 基礎 - 2:共享框架

 

深入 .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 個檔案。

可以通過下面的命令來生成兩種應用程式

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 宿主 必須:

  1. 發現你的應用所依賴的名稱和版本

  2. 在公共位置找到這些依賴內容

這些依賴可以在多個位置發現,包括,但是不限於,這些共享框架。在上一篇文章中,我已經總結了 deps.json

runtimeconfig.json 檔案是如何配置宿主的行為。請檢視它來得到更詳細的說明。

.NET Core 宿主讀取 *.runtimeconfig.json 檔案來得到需要載入哪個共享框架。其內容可能類似於如下:

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

共享框架名稱 只是一個名稱。根據約定,該名稱以 App 結束,但可以是任何名稱,比如 "FooBananaShark"。

共享框架版本 表示最小版本。.NET Core 宿主從不執行在最小版本上,而是試圖執行在更高的版本上。

2.1 我已經安裝的共享框架是哪些?

執行 dotnet --list-runtimes 。它將會顯示名稱、版本和共享框架的位置。對於 .NET Core 3.1,共享框架的列表如下所示。

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 中,它使用專案檔案中的兩個片段來決定該檔案中框架部分的內容:

  1. MicrosoftNETPlatformLibrary 屬性。預設對於所有的 .NET Core 專案設定為 Microsoft.NETCore.App

  2. NuGet 恢復的結果,它必須包含一個同名的包

對於所有的專案,.NET Core SDK 對 Microsoft.NETCore.App新增隱式的包引用。ASP.NET Core 預設設定 MicrosoftNETPlatformLibraryMicrosoft.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 檔案。

{
  "runtimeOptions": {
    "tfm": "netcoreapp2.1",
    "framework": {
      "name": "Microsoft.AspNetCore.App",
      "version": "2.1.2"
    }
  }
}

在 .NET Core 3.1 中,內容如下:

{
  "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 來關閉。預設的全域性位置是:

OSLocation
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”

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 包存在。修剪使用進行某些圖匹配來決定傳遞依賴、升級等等,來選取正確的檔案。

例如,對於下面的專案

<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