.NET Standard中配置TargetFrameworks輸出多版本類庫
- 2020 年 10 月 10 日
- 筆記
- .net standard
在.NET Standard/.NET Core技術出現之前,編寫一個類庫項目(暫且稱為基礎通用類庫PA)且需要支持不同 .NET Framework 版本,那麼可行的辦法就是創建多個不同版本的項目(暫且稱為PB1、PB2、PB3 … PBn)。PB1、PB2、PB3 … PBn項目分別執行下面操作:【添加】–【現有項】–【添加為鏈接的方式】,將PA項目代碼文件添加到各自項目中,如果代碼不同,則需要使用#if #else #endif 等標籤來判斷 .NET Framework 版本。而在.NET Standard/.NET Core技術出現之後,可以通過配置SDK 樣式項目中的目標框架來支持一套代碼同時輸出多版本類庫。
下面以Visual Studio 2019 來演示整個操作過程。
1、新建一個 .NET Standard 類庫。
2、填寫項目名稱
3、創建完成後,查看「解決方案資源管理器」,項目下面多了一個「依賴項」節點,子節點是SDK,孫子節點是 NETStandard.Library(2.0.3)。
項目組織方式與傳統類庫項目的組織方式不同
4、項目,右鍵【屬性】–>【應用程序】–> 「目標框架」默認是 .NET Standard 2.0。
也可以修改為其他版本
5、編譯項目,查看bin –> debug。生成了 netstandard2.0目錄
目錄裏面生成的DLL,這與傳統.NET Framework 類型的類庫項目生成結果相同。
6、項目,右鍵 –> 「編輯項目文件」
可以看到當前類庫默認為 netstandard2.0,而此時其xml標籤為 TargetFramework。
如果要支持多版本,則需要做調整,將 TargetFramework 節點修改為 TargetFrameworks,再添加目標版本。
7、配置多目標框架
關於如何指定多目標框架,請參考博客《.NET Standard SDK 樣式項目中的目標框架》
我做的BIMFACE二次開發的接口的目標是支持 .NET Framework4.0、.NET Framework4.5 以及 .NET Core3.1。所以配置了選下3個目標版本
<PropertyGroup>
<TargetFrameworks>net40;net45;netstandard2.0;</TargetFrameworks> <!--輸出多版本類庫-->
</PropertyGroup>
修改後並保存,Visual Studio 會彈出黃色背景的提示信息。
這裡一定要點擊【重新加載項目】按鈕。重新加載後,依賴項中出現了如下圖所示的3個項
展開每個項查看, 每個版本的程序集對應一個單獨的依賴項節點。
8、項目,右鍵【屬性】–>【應用程序】–> 「目標框架」被禁用,因為單個項目支持多版本類庫,無法一次呈現多個,這是正確的。
9、重新編譯項目,查看bin –> debug,生成了3種不同版本的目標程序集。
通過上面的步驟我們已經實現了多版本輸出,但是在實際的企業級業務系統開發時情況比較複雜,還需要解決以下幾個問題:
1、條件編譯
2、引用本地程序集
3、NuGet方式引用程序集
4、XML文檔輸出
5、編碼與DEBUG 調試
6、自動生成內部版本號
7、文件複製
下面逐步講解如何解決以上問題。

這是VS中默認的編譯輸出目錄。
如果需要配置不同的類庫輸出到不同的位置,也可以自定義配置輸出路徑實現。
查看項目屬性,【生成】–>「輸出」–>「輸出路徑」中輸入自定義目錄或者點擊【瀏覽】按鈕選擇一個目錄。
填寫後,保存項目。項目右鍵,【編輯項目文件】,csproj文件中自動增加了如下配置,其中 Condition 後面的表達式即是編譯條件。OutputPath即是自定義輸出目錄。
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net40|AnyCPU'">
<OutputPath>bin\Debug\</OutputPath>
</PropertyGroup>
按照以上方式再複製2份,分別配置 net45 與 netstandard2.0版。完整配置如下:
<!--條件編譯-->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|Release|net40|AnyCPU'">
<OutputPath>bin\Debug\</OutputPath><!--編譯後的文件輸出目錄-->
</PropertyGroup>
<!--條件編譯-->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|Release|net45|AnyCPU'">
<OutputPath>bin\Debug\</OutputPath>
</PropertyGroup>
<!--條件編譯-->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|Release|netstandard2.0|AnyCPU'">
<OutputPath>bin\Debug\</OutputPath>
</PropertyGroup>
bin\Debug\ 是我自己定義的輸出目錄,大家可以根據實際需求填寫其他目錄。
$(Configuration) 的條件值有:Debug、Release。
$(TargetFramework)的條件為 <TargetFrameworks>節點中配置的值。
$(Platform) 的條件值有:
在下圖中可以看出由於3個不同的輸出類庫中所引用的程序集是不同的,那麼當編譯時,一定是每個類庫進行單獨編譯,這時就就需要通過某種方式告訴編譯器當前編譯的類庫版本是什麼,然後添加針對具體版本的第三方程序集引用。
.NET Standard 指定多個目標框架時,可有條件地為每個目標框架引用程序集。
以下庫項目面向 .NET Standard (netstandard1.4
) 和 .NET Framework(net40
和 net45
)的 API。 將複數形式的 TargetFrameworks 元素與多個目標框架一起使用。 為兩個 .NET Framework TFM 編譯庫時,Condition
屬性包括特定於實現的包:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net40;net45</TargetFrameworks>
</PropertyGroup>
<!-- 有條件地獲取.NET Framework 4.0 目標的引用 -->
<ItemGroup Condition=" '$(TargetFramework)' == 'net40' ">
<Reference Include="System.Net" />
</ItemGroup>
<!-- 有條件地獲取.NET Framework 4.5 目標引用 -->
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
<Reference Include="System.Net.Http" />
<Reference Include="System.Threading.Tasks" />
</ItemGroup>
</Project>
下面開始添加引用,點擊項目子節點【依賴項】–>【添加程序集引用】
打開如下界面。默認加載的目標框架顯示為 .NET Framework 4。
如何才能添加 net45 或者 netstandard2.1 的引用呢?正常來說應該在VS的「引用管理器」界面上提供目標框架的下拉選擇框,可以自由切換選擇不同的目標框架,但是到目前為止VS沒有此功能,我的VS版本信息如下
希望微軟在後續VS版本中能增加此功能。
回到csproj編輯界面,可以看到 TargetFrameworks 值第一個為 net40,估計與這個有關係。
通過取巧的方式調整 TargetFrameworks 里的版本先後順序,保存後,重啟VS(我的VS2019是這種情況,需要重啟才生效。不知道其他小夥伴們的VS是不是保存後可以自動切換呢?)
再次添加程序集引用,此時加載了 .NET Framework 4.5
添加一個「System.Net.dll」引用來測試一下
添加後,如下圖所示
.NET Framework 4.5 項目中多了「System.Net.dll」引用。但是 .NET Standard 2.0 前面顯示黃色警告符合。展開所有依賴項,.NET Framework 4.0 與 .NET Framework 4.5 都已經正確引用。
.NET Standard 2.0 程序及引用有警告。這表示 netstandard2.0 並不知道 System.Net.dll 是什麼。
查看.csproj文件
紅色框內的配置,表示net40、.net45 和 netstand2.0 都需要「System.Net」引用(即統一配置),而實際只有 net40、.net45 才需要該引用,所以這裡我們要使用 Condition 條件,修改如下:
這樣只有 .net40 與 .net45 條件下才引用「System.Net.dll」。保存後,發現 netstand2.0 下面的警告標示消失了。
下面演示添加一個多版本都支持的第三方類庫,NLog 日誌組件,目前最新版本為4.7.5。通過 NuGet 方式添加引用
下圖可以看出該組件同時支持 .NET4.0、.NET4.5 以及 .NET Standard 2.0
點擊【安裝】
點擊【確定】,安裝完成後,每一個類庫均添加了引用
查看.csproj文件,添加了如下配置
注意這裡是 PackageReference,而之前程序集的是 Reference,而且我們也會發現在VS解決方案管理器中並沒有出現 packages.config 文件。默認在 sln 文件的同級也沒有創建一個 packages 文件夾。
而是將dll下載到了C:\Users\當前登錄用戶\.nuget目錄下,這與java的Maven管理方式類似。我的本地路徑為:C:\Users\Savion\.nuget\packages
下面再添加一個 netstandard 專有的 nuget 引用 Microsoft.Extensions.DependencyInjection.dll
點擊【安裝】
點擊【確定】
點擊【我接受】。
添加完後解決方案中僅有 .NET Standard2.0 中增加了引用。.net40 與 .net45 中沒有引用。
添加完後 csproj文件 會多出如下配置
NuGet 很智能,自動把 Condition 給加好了。
選擇項目,點擊 屬性–>生成,勾選 「XML 文檔文件」。默認生成的xml文件名稱包含絕對路徑,這個名稱不是很友好,一般修改為程序集的名稱即可
點擊菜單欄上的【保存】按鈕。查看.csproj文件新增了如下配置:
這表示 net40 會生成 xml 文件,將該配置信息複製兩份,然後修改 Platform 以及輸出路徑為 net45 與 netstandard2.0。完整配置如下:
<!--條件編譯-->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|Release|net40|AnyCPU'">
<DocumentationFile>ZCN.NET.BIMFace.SDK.xml</DocumentationFile><!--xml文檔,輸出類庫中方法與參數的注釋等信息-->
<OutputPath>bin\Debug\</OutputPath><!--編譯後的文件輸出目錄-->
</PropertyGroup>
<!--條件編譯-->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|Release|net45|AnyCPU'">
<DocumentationFile>ZCN.NET.BIMFace.SDK.xml</DocumentationFile>
<OutputPath>bin\Debug\</OutputPath>
</PropertyGroup>
<!--條件編譯-->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|Release|netstandard2.0|AnyCPU'">
<DocumentationFile>ZCN.NET.BIMFace.SDK.xml</DocumentationFile>
<OutputPath>bin\Debug\</OutputPath>
</PropertyGroup>
重新編譯項目,查看輸出目錄裏面的內容
其中ZCN.NET.BIMFace.SDK.xml 內容如下
.netstandard2.0 中多了一個 ZCN.NET.BIMFace.SDK.deps.josn 文件,裏面包含了運行時環境以及依賴項等信息
/// <summary>
/// 判斷字符串是否為null、空或者空白
/// </summary>
/// <param name="str">待判斷的字符串</param>
/// <returns></returns>
public static bool IsNullOrWhiteSpace(this string str)
{
return string.IsNullOrEmpty(str.Trim());
}
在.NET4.0及以上框架下使用下面的方式實現
/// <summary>
/// 判斷字符串是否為null、空或者空白
/// </summary>
/// <param name="str">待判斷的字符串</param>
/// <returns></returns>
public static bool IsNullOrWhiteSpace(this string str)
{
return string.IsNullOrWhiteSpace(str);
}
2種框架下實現的邏輯方式不同,為了只編寫一套代碼(該情況為一個方法),此時就需要使用預處理指令編寫條件指令。
在庫或應用中,使用預處理器指令編寫條件代碼,針對每個目標框架進行編譯。關於預處理指令請參考《C# 預處理器指令》
使用預處理指令編寫條件代碼的實現方式如下:
/// <summary>
/// 判斷字符串是否為null、空或者空白
/// </summary>
/// <param name="str">待判斷的字符串</param>
/// <returns></returns>
public static bool IsNullOrWhiteSpace(this string str)
{
#if NET35
return string.IsNullOrEmpty(str.Trim());
#else
return string.IsNullOrWhiteSpace(str);
#endif
}
上面的實現方式是在一個方法內進行條件區分,下面介紹在同一個類中(方法之外),使用條件區分不同邏輯的實現方式
#if NET35 || NET40 || NET45
/// <summary>
/// 對URL字符串進行編碼
/// <para>注意:.NET Core 轉義後字母為大寫</para>
/// </summary>
/// <param name="url">有效的url字符串</param>
/// <param name="encoding">編碼,默認為 UTF8</param>
/// <returns></returns>
public static string UrlEncode(this string url, Encoding encoding = null)
{
encoding = encoding ?? Encoding.UTF8;
return System.Web.HttpUtility.UrlEncode(url, encoding);
}
#else
/// <summary>
/// 對URL字符串進行編碼
/// <para>注意:.NET Core 轉義後字母為大寫</para>
/// </summary>
/// <param name="url">有效的url字符串</param>
/// <returns></returns>
public static string UrlEncode(this string url)
{
return WebUtility.UrlEncode(url);//轉義後字母為大寫
}
#endif
上面兩段代碼中的預處理符號 NET35、NET40、NET45 是.NET目標框架中預定義的預處理符號。
使用 SDK 樣式項目時,生成系統可識別預處理器符號,這些符號表示支持的目標框架版本表中所示的目標框架。 使用表示 .NET Standard、.NET Core 或 .NET 5 TFM 的符號時,請用下劃線替換點和連字符,並將小寫字母更改為大寫字母(例如,netstandard1.4
的符號為 NETSTANDARD1_4
)。
.NET 目標框架的預處理器符號的完整列表如下:
除此之外,開發者可以通過配置自定義常量的方式達到與.NET目標框架中預定義的預處理符號相同的功能。
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <DefineConstants>TRACE;RELEASE</DefineConstants> <!--統一定義的常量--> </PropertyGroup>
上述代碼片段通過 <DefineConstants> 節點 定義了2個常量(多個常量之間使用分號分隔)TRACE 與 RELEASE。
在編寫C#代碼時能夠自動智能感知到自定義的常量
上面是定義的統一的全局變量,也可以在每個條件編譯分組中自定義常量
<!--條件編譯--> <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net40|AnyCPU'"> <DocumentationFile>ZCN.NET.BIMFace.SDK.xml</DocumentationFile><!--xml文檔,輸出類庫中方法與參數的注釋等信息--> <OutputPath>bin\Debug\</OutputPath><!--編譯後的文件輸出目錄--> <DefineConstants>NET_FULL</DefineConstants><!--獨立定義的常量--> </PropertyGroup>
PropertyGroup,是包含一組用戶定義的 Property 元素。 MSBuild 項目中使用的每個 Property 元素必須是 PropertyGroup 元素的子元素。其包含如下的子元素
更加完整詳細的信息請參考微軟官方文檔《PropertyGroup 元素 (MSBuild)》
- 以前的寫法是在/Properties/AssemblyInfo.cs里通過
[assembly: AssemblyVersion("2.3.*")]
這樣的形式生成,但是現在默認關閉這個功能了,如果我們直接指定<AssemblyVersion>9.8.*</AssemblyVersion>
會警告錯誤,加上<Deterministic>False</Deterministic>
即可 -
為什麼默認關閉?請了解下Roslyn中的確定性構建
-
其它生成方式、彙編內部版本號後面兩位的生成規則,請看使用Visual Studio時是否可以自動增加文件構建版本、Visual Studio 2017中的自動版本控制(.NET Core)、如何有一個自動遞增版本號(Visual Studio)
-
msbuildtasks也了解一下,如果要兼容以前的內部版本號生成規則,可自己動手
NuGet包相關
- 靜態文件如何指定複製行為等,或許會發現安裝NuGet之後希望能編輯的文件僅僅只是一個鏈接而已,如何讓它包含在項目裏面呢,請參考微軟官方文檔 NuGet ContentFiles揭秘,帶回解決方案級包的討論
- PackageReference 方式作為包管理格式,安裝時不支持執行install.ps1等powershell相關腳本,init.ps1在解決方案第一次安裝時可用。vs2017中,已不支持此功能,NuGet 3 – 什麼和為什麼-Powershell安裝和卸載腳本
- 關於nuget包安裝的相關行為估計都可以通過msbuild屬性或者任務來搞定,這一切都是可以通過命令行來執行的,方便跨平台使用吧
- msbuildtasks也了解一下,可以代替ps1腳本完成想做的事