在.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) 的條件值有:
檢視專案屬性,【生成】-->“常規”-->“條件編譯和符號”中輸入自定義內容。選擇 “定義DEGUG常數” 與 “定義TRACE常量”,儲存專案。
檢視csproj檔案,在第一個目標版本對應的 <PropertyGroup> 配置節點下增加了
<DefineConstants>TRACE;DEBUG;NET_FULL;TEST;</DefineConstants>
為了做統一配置,將其提取出來
<PropertyGroup Condition=" '$(Configuration)' == 'Debug|Release' "> <!--統一定義的常量--> <DefineConstants>TRACE;DEBUG;RELEASE;NET_FULL;TEST;</DefineConstants> </PropertyGroup>
在下圖中可以看出由於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指令碼完成想做的事