1. 程式人生 > >.NET Standard中配置TargetFrameworks輸出多版本類庫

.NET Standard中配置TargetFrameworks輸出多版本類庫

系列目錄     【已更新最新開發文章,點選檢視詳細】

   在.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、檔案複製

 

下面逐步講解如何解決以上問題。

一、條件編譯 在下圖中可以看出,編譯成功後,在專案的預設位置 bin\Debug 下生成了3個不同目錄,分別對應3個目標版本。

這是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 下面的警告標示消失了。

三、NuGet 方式引用程式集

下面演示新增一個多版本都支援的第三方類庫,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 文件檔案”。預設生成的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 檔案,裡面包含了執行時環境以及依賴項等資訊

 

五、編碼與DEBUG除錯    雖然 .NET Standard 支援編寫一套程式碼編譯輸出支援多平臺,但是實際編碼中會遇到很多特殊情況需要使用條件指令進行區分邏輯,比如編寫一個擴充套件方法判斷字串是否為空或者為null。 在.NET3.5框架下使用下面的方式實現
 /// <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指令碼完成想做的事
系列目錄     【已更新最新開發文章,點選檢視詳細】