天天看點

無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5

Visual Studio 2022 已正式釋出!着急更新的小夥伴興緻勃勃地更新并解除安裝了原來的 Visual Studio 2019 後,發現自己的幾個庫項目竟然無法編譯通過了。究其原因,是因為我的一些庫依舊在支援古老的 .NET Framework 4.5 架構,而 Visual Studio 2022 不再附帶如此古老的目标包了。

我之前在 另一篇文章 中告訴大家通過将 Visual Studio 2019 裝回來的方式解決這個問題,但是有小夥伴不想安裝 Visual Studio 2019;是以本文用另外一種方法,無需安裝 Visual Studio 2019,也無需單獨安裝 .NET Framework 目标包。

無法編譯 .NET Framework 4.5 項目

為了更廣泛的适用于各種項目,我的一些庫相容的架構版本是非常古老的(比如下圖截取的這張)。可是解除安裝掉 Visual Studio 2019 隻留下 Visual Studio 2022 之後這些項目就不再能編譯通過了。如果點開 Visual Studio 2022 的安裝程式,會發現已經删除掉了 .NET Framework 4.5 的目标包了,無法通過它安裝回來。

無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5
無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5
無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5

關鍵步驟

第一步:安裝 NuGet 包 Microsoft.NETFramework.ReferenceAssemblies

Microsoft.NETFramework.ReferenceAssemblies 這款 NuGet 包旨在解決沒有目标包的時候編譯 .NET Framework 架構的問題。是以,我們将通過安裝此 NuGet 包來解決 Visual Studio 2022 中目标包的缺失問題。

正常你隻需要在項目中安裝這個 NuGet 包即可。如果你整個解決方案裡所有項目都需要相容 .NET Framwework 4.5 或者更加古老的 .NET 架構,也可以用 Directory.Build.props 檔案,詳見:使用 Directory.Build.props 管理多個項目配置 - 林德熙

<Project>
  <ItemGroup>
    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" />
  </ItemGroup>
</Project>
           

請特别注意

如果你正在開發的是庫項目,那麼在引用此 NuGet 包之後,應該加上 PrivateAssets="all" 來标記此 NuGet 包不會成為你自己的庫的其中一個依賴。否則就會像下圖一樣有一個不期望的依賴。

無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5

▲ 不期望的依賴

無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5

▲ 正常的依賴

第二步:适配 Visual Studio 的特殊開發環境

如果你不用 VS2022,而隻是使用 dotnet build 或 msbuild 指令來編譯,那麼以上第一步完成後就夠了。不過考慮到大家基本上都是用 Visual Studio 來開發,是以上述操作在 VS 中的水土不服也需要特别處理一下。

在項目的 csproj 檔案中添加一個 Target:

<Target Name="WalterlvPackagesIncludeNetFrameworkReferences" BeforeTargets="GetReferenceAssemblyPaths" DependsOnTargets="Restore"
        Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' And '$(TargetFrameworkRootPath)' == '' ">
  <PropertyGroup>
    <TargetFrameworkRootPath Condition=" $(TargetFrameworkMoniker) == '.NETFramework,Version=v4.5' ">$(UserProfile)\.nuget\packages\microsoft.netframework.referenceassemblies.net45\1.0.2\build</TargetFrameworkRootPath>
    <TargetFrameworkRootPath Condition=" $(TargetFrameworkMoniker) == '.NETFramework,Version=v4.0' ">$(UserProfile)\.nuget\packages\microsoft.netframework.referenceassemblies.net40\1.0.2\build</TargetFrameworkRootPath>
  </PropertyGroup>
</Target>
           

或者如果前面你是在 Directory.Build.props 檔案中添加的引用,那麼就在對應的 Directory.Build.targets 檔案中添加這一段(沒有此檔案則建立)。

解釋一下這段代碼如何适配了 Visual Studio 的特殊開發環境:

  1. 猜測 VS 會緩存 TargetFrameworkRootPath 屬性,一旦擷取到其值将再也不會更新之,就算後面緊跟着還原 NuGet 包後值已被正常指派了也不會使用(即使重新開機 VS 也是如此);于是我們在 TargetFrameworkRootPath 屬性為 `` 時手工給其賦上正确的值。
  2. 猜測 VS 在發現 TargetFrameworkRootPath 屬性所對應的路徑不存在時視為與空同等處理;是以我們 DependsOnTargets="Restore" 以便在第一次還原 NuGet 包相關路徑還沒有建立時馬上完成 NuGet 包的還原以建立對應目錄。

在使用了以上代碼後,Visual Studio 2022 剛打開項目時會短暫提示缺少 .NET Framework 4.5 架構,但真正編譯時此提示會消失。這些問題都是單獨使用指令來編譯時不會遇到的問題。我也嘗試過其他的解決方法,但都不能完美消除此錯誤提示(如果你沒有 WPF 項目的話,也可以通過建立名為 GetReferenceAssemblyPaths 的空 Target 跳過檢查)。

寫完上面的代碼之後:

  1. 關閉 Visual Studio 2022
  2. 清理倉庫,執行 git clean -xdf 指令(這會删除所有未被版本管理的檔案,包括 Visual Studio 的各種緩存檔案)
  3. 重新啟動 Visual Studio 2022

一些注意事項

1. 需要覆寫整個解決方案中所有涉及到 .NET Framework 架構的項目

這個 NuGet 包的本質是在編譯的時候設定 TargetFrameworkRootPath 屬性到 NuGet 包裡安裝過來的目錄,并且通過 <Reference Include="mscorlib" Pack="false" /> 指定額外引用 mscorelib,是以不會産生額外的引用。于是這種方式安裝的 NuGet 包不像其他的 NuGet 包那樣可以傳遞到其他引用它的項目。

你需要做的:

  1. 給所有含 .NET Framework 架構的項目安裝 Microsoft.NETFramework.ReferenceAssemblies NuGet 包
  2. 如果不想直接給所有項目安裝,可以使用 Directory.Build.props 來一并安裝

2. 不支援同一個檔案夾下有兩個 csproj 項目的情況

有時候為了友善,當兩個項目幾乎所有檔案都相同,隻是項目配置不同時,我們會考慮将這兩個項目放到同一個檔案夾裡面以共用檔案。可惜這種方式組織的項目,跟本問所提供的方案不相容。

無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5

如果解決方案中存在這樣的項目組織方式,你會發現其他項目都能編譯通過,唯獨這兩個項目依舊死在缺少 .NET Framework 45 目标包上。解決方法就是把這兩個項目拆開成兩個檔案夾。

可是他們共用的檔案怎麼辦?答案是在每個項目的 csproj 檔案中添加下面幾行:

<ItemGroup>
    <Compile Include="..\SomeCommonFolder\**\*.cs" Link="%(RecursiveDir)%(Filename)%(Extension)" />
  </ItemGroup>
           

即他們都去共同的目錄下把檔案都拉進來編譯,并且以連結的方式顯示到 Visual Studio 解決方案管理器裡。詳見:使用連結共享 Visual Studio 中的代碼檔案

另外,這裡的 %(RecursiveDir) 是遞歸顯示檔案夾(否則所有檔案會拍平到項目裡),%(Filename) 是将連結顯示成檔案名,%(Extension) 是在檔案名後面顯示檔案擴充名。經此寫法,項目裡顯示的其他檔案夾的檔案看起來就像真的在這個項目裡一樣。

3. 對于經典 csproj 格式(而非 SDK 風格 csproj 格式)的情況

評論區 @afunc233 的回複 說經典 csproj 格式沒辦法使用本文所述的方法。

我個人建議還是遷移一下比較好,不難而且完全相容舊格式的所有功能。遷移教程:将 WPF、UWP 以及其他各種類型的舊 csproj 遷移成 Sdk 風格的 csproj。

如果不想遷移,也可以試試官方的方法。但我不想嘗試,是以就線上等 TA 在評論區的回複吧!

4. 不想折騰之一:還是裝回 VS2019 吧

有時候,你可能會遇到各種意料之外的問題,超出我上面列舉的坑。不想折騰的話,那就把 .NET Framework 4.5 目标包裝回來吧,可參見:Visual Studio 2022 更新不再附帶 .NET Framework 4.5 這種古老的目标包了,本文幫你裝回來。

5. 不想折騰之二:打死也不裝回 VS2019

有時候,你可能會遇到各種意料之外的問題,超出我上面列舉的坑。如果你跟我一樣,無論如何都不想裝回 VS2019,那麼還有解決方法:直接把 .NET Framework 的引用全拷到項目裡來。操作如下:

  1. 去 Microsoft.NETFramework.ReferenceAssemblies NuGet 包的下載下傳頁,找到 Dependencies 标簽,裡面有各個不同 .NET Framework 版本的 .NET Framework 引用包。
  2. 點開你項目需要的那個版本的 .NET Framework 包,然後在頁面右邊找到 Download package 連結,點它,下下來。
  3. 解壓下載下傳下來的 NuGet 包,取出其中的“/build/.NET Framework”檔案夾,複制到你的項目裡某個位置。
  4. 在你倉庫的根目錄添加或修改 Directory.Build.props 檔案,裡面添加下面的代碼。
無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5
無需安裝 VS2019,在 VS2022 中編譯 .NET Framework 4.5/4/3.5

Directory.Build.props 檔案的新增内容:

<Project>

++  <PropertyGroup>
++      <TargetFrameworkRootPath>$(MSBuildThisFileDirectory)Dependencies</TargetFrameworkRootPath>
++  </PropertyGroup>

++  <ItemGroup Condition=" ('$(TargetFrameworkIdentifier)' == '.NETFramework') And ('$(TargetFrameworkVersion)' == 'v4.5') ">
++      <Reference Include="mscorlib" Pack="false" />
++      <Reference Include="Microsoft.VisualBasic" Pack="false" Condition="'$(Language)' == 'VB' And '$(UsingMicrosoftNETSdk)' == 'true'" />
++  </ItemGroup>

</Project>
           

其中:

  1. 如果沒有此檔案,那麼建立一個。
  2. 那個 TargetFrameworkRootPath 的值是 .NETFramework 檔案夾的父級檔案夾。劃重點,你需要確定那個檔案夾裡面包含我們從 NuGet 包裡解壓出來的 .NETFramework 完整檔案夾。
  3. 後面的 ItemGroup 裡的内容,直接照抄上文即可,我也是照抄 Microsoft.NETFramework.ReferenceAssemblies 包裡的

用最後的這種方法,算就究級解決方案了。沒有這種方案解決不了的問題!如果有,那就是有某項目沒受此檔案影響,把這段代碼拷到那個項目的 csproj 檔案裡去。

本文會經常更新,請閱讀原文: https://blog.walterlv.com/post/support-old-netfx-on-vs2022-or-later.html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。

本作品采用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定 進行許可。歡迎轉載、使用、重新釋出,但務必保留文章署名 呂毅 (包含連結: https://blog.walterlv.com ),不得用于商業目的,基于本文修改後的作品務必以相同的許可釋出。如有任何疑問,請 與我聯系 ([email protected]) 。

繼續閱讀