.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) 的條件值有:

 

 

二、引用本地程序集

在下圖中可以看出由於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)》

六、自動生成內部版本號
七、文件複製

NuGet包相關