寫(xiě)了這么多個(gè) C# 項(xiàng)目,是否對(duì)項(xiàng)目文件 csproj 有一些了解呢?Visual Studio 是怎么讓 csproj 中的內(nèi)容正確顯示出來(lái)的呢?更深入的,我能夠自己擴(kuò)展 csproj 的功能嗎?
本文將直接從 csproj 文件格式的本質(zhì)來(lái)看以上這些問(wèn)題。
閱讀本文,你將:
可以通讀 csproj 文件,并說(shuō)出其中每一行的含義
可以手工修改 csproj 文件,以實(shí)現(xiàn)你希望達(dá)到的高級(jí)功能(更高級(jí)的,可以開(kāi)始寫(xiě)個(gè)工具自動(dòng)完成這樣的工作了)
理解新舊 csproj 文件的差異,不至于寫(xiě)工具解析和修改 csproj 文件的時(shí)候出現(xiàn)不兼容的錯(cuò)誤
csproj 里面是什么?
總覽 csproj 文件
相信你一定見(jiàn)過(guò)傳統(tǒng)的 csproj 文件格式。就算你幾乎從來(lái)沒(méi)主動(dòng)去看過(guò)里面的內(nèi)容,在版本管理工具中解沖突時(shí)也在里面修改過(guò)內(nèi)容。
不管你是新手還是老手,一定都會(huì)覺(jué)得這么長(zhǎng)這么復(fù)雜的文件一定不是給人類(lèi)閱讀的。你說(shuō)的是對(duì)的!傳統(tǒng) csproj 文件中有大量的重復(fù)或者相似內(nèi)容,只為 msbuild 和 Visual Studio 能夠識(shí)別整個(gè)項(xiàng)目的屬性和結(jié)構(gòu),以便正確編譯項(xiàng)目。
不過(guò),既然這篇文章的目標(biāo)是理解 csproj 文件格式的本質(zhì),那我當(dāng)然不會(huì)把這么復(fù)雜的文件內(nèi)容直接給你去閱讀。
我已經(jīng)將整個(gè)文件結(jié)構(gòu)進(jìn)行了極度簡(jiǎn)化,然后用思維導(dǎo)圖進(jìn)行了分割??偨Y(jié)成了下圖,如果先不關(guān)注文件的細(xì)節(jié),是不是更容易看懂了呢?

如果你此前也閱讀過(guò)我的其他博客,會(huì)發(fā)現(xiàn)我一直在試圖推薦使用新的 csproj 格式:
將 WPF、UWP 以及其他各種類(lèi)型的舊樣式的 csproj 文件遷移成新樣式的 csproj 文件
讓一個(gè) csproj 項(xiàng)目指定多個(gè)開(kāi)發(fā)框架
那么新格式和舊格式究竟有哪些不同使得新的格式如此簡(jiǎn)潔?
于是,我將新的 csproj 文件結(jié)構(gòu)也進(jìn)行簡(jiǎn)化,用思維導(dǎo)圖進(jìn)行了分割??偨Y(jié)成了下圖:

比較兩個(gè)思維導(dǎo)圖之后,是不是發(fā)現(xiàn)其實(shí)兩者本是相同的格式。如果忽略我在文字顏色上做的標(biāo)記,其實(shí)兩者的差異幾乎只在文件開(kāi)頭是否有一個(gè) xml 文件標(biāo)記()。我在文字顏色上的標(biāo)記代表著這部分的部件是否是可選的,白色代表必須,灰色代表可選;而更接近背景色的灰色代表一般情況下都是不需要的。
我把兩個(gè)思維導(dǎo)圖放到一起方便比較:

會(huì)發(fā)現(xiàn),傳統(tǒng)格式中?xml 聲明、Project 節(jié)點(diǎn)、Import (props)、PropertyGroup、ItemGroup、Import (targets)?都是必要的,而新格式中只有?Project 節(jié)點(diǎn)?和?PropertyGroup?是必要的。
是什么導(dǎo)致了這樣的差異?在了解 csproj 文件中各個(gè)部件的作用之前,這似乎很難回答。
了解 csproj 中的各個(gè)部件的作用
xml 聲明部分完全沒(méi)有在此解釋的必要了,為兼容性提供了方便,詳見(jiàn):XML - Wikipedia。
接下來(lái),我們不會(huì)依照部件出現(xiàn)的順序安排描述的順序,而是按照關(guān)注程度排序。
PropertyGroup
PropertyGroup?是用來(lái)存放屬性的地方,這與它的名字非常契合。那么里面放什么屬性呢?答案是——什么都能放!
在這里寫(xiě)屬性就像在代碼中定義屬性或變量一樣,只要寫(xiě)了,就會(huì)生成一個(gè)指定名稱(chēng)的屬性。
比如,我們寫(xiě):
walterlv is a 逗比
那么,就會(huì)生成一個(gè)?Foo?屬性,值為字符串?walterlv is a 逗比。至于這個(gè)屬性有什么用,那就不歸這里管了。
這些屬性的含義完全是由外部來(lái)決定的,例如在舊的 csproj 格式中,編譯過(guò)程中會(huì)使用?TargetFrameworkVersion?屬性,以確定編譯應(yīng)該使用的 .NET Framework 目標(biāo)框架的版本(是 v4.5 還是 v4.7)。在新的 csproj 格式中,編譯過(guò)程會(huì)使用?TargetFrameworks?屬性來(lái)決定編譯應(yīng)該使用的目標(biāo)框架(是 net47 還是 netstandard2.0)。具體是編譯過(guò)程中的哪個(gè)環(huán)節(jié)哪個(gè)組件使用了此屬性,我們后面會(huì)說(shuō)。
從這個(gè)角度來(lái)說(shuō),如果你沒(méi)有任何地方用到了你定義的屬性,那為什么還要定義它呢?是的——這只是浪費(fèi)。
PropertyGroup?可以定義很多個(gè),里面都可以同等地放屬性。至于為什么會(huì)定義多個(gè),原因無(wú)外乎兩個(gè):
為了可讀性——將一組相關(guān)的屬性放在一起,便于閱讀和理解意圖(舊的 csproj 談不上什么可讀性)
為了加條件——有的屬性在 Debug 和 Release 下不一樣(例如條件編譯符?DefineConstants)
額外說(shuō)一下,Debug?和?Release?這兩個(gè)值其實(shí)是在某處一個(gè)名為?Configuration?的屬性定義的,它們其實(shí)只是普通的字符串而已,沒(méi)什么特殊的意義,只是有很多的?PropertyGroup?加上了?Debug?Release?的判斷條件才使得不同的?Configuration?具有不同的其他屬性,最終表現(xiàn)為編譯后的巨大差異。由于?Configuration?屬性可以放任意字符串,所以甚至可以定義一個(gè)非?Debug?和?Release?的配置(例如用于性能專(zhuān)項(xiàng)測(cè)試)也是可以的。
ItemGroup
ItemGroup?是用來(lái)指定集合的地方,這與它的名字非常契合。那么這集合里面放什么項(xiàng)呢?答案是——什么都能放!
是不是覺(jué)得這句話跟前面的?PropertyGroup?句式一模一樣?是的——就是一模一樣!csproj 中的兩個(gè)大頭都這樣不帶語(yǔ)義,幾乎可以說(shuō)明 csproj 文件是不包含語(yǔ)義的,它能夠用來(lái)做什么事情純屬由其他模塊來(lái)指定;這為 csproj 文件強(qiáng)大的擴(kuò)展性提供了格式基礎(chǔ)。
既然什么都能放,那我們放這些吧:
walterlv is a 逗比walterlv is a 天才天才向左,逗比向右逗比屬性額外加成
于是我們就有 4 個(gè)類(lèi)型為?Foo?的項(xiàng)了,至于這 4 個(gè)?Foo?項(xiàng)有什么作用,那就不歸這里管了。
這些項(xiàng)的含義與?PropertyGroup?一樣也是由外部來(lái)決定。具體是哪個(gè)外部,我們稍后會(huì)說(shuō)。但是我們依然有一些常見(jiàn)的項(xiàng)可以先介紹介紹:
Reference?引用某個(gè)程序集
PackageReference?引用某個(gè) NuGet 包
ProjectReference?引用某個(gè)項(xiàng)目
Compile?常規(guī)的 C# 編譯
None?沒(méi)啥特別的編譯選項(xiàng),就為了執(zhí)行一些通用的操作(或者是只是為了在 Visual Studio 列表中能夠有一個(gè)顯示)
Folder?一個(gè)空的文件夾,也沒(méi)啥用(不過(guò)標(biāo)了這個(gè)文件夾,Visual Studio 中就能有一個(gè)文件夾的顯式,即便實(shí)際上這個(gè)文件夾可能不存在)
ItemGroup?也可以放很多組,一樣是為了提升可讀性或者增加條件。
Import
你應(yīng)該注意到在前面的思維導(dǎo)圖中,無(wú)論是新 csproj 還是舊 csproj 文件,我都寫(xiě)了兩個(gè)?Import?節(jié)點(diǎn)。其實(shí)它們本質(zhì)上是完全一樣的,只不過(guò)在含義上有不同。前面我們了解到 csproj 文件致力于脫離語(yǔ)義,所以分開(kāi)兩個(gè)地方寫(xiě)幾乎只是為了可讀性考慮。
那么前面那個(gè)?Import?和后面的?Import?在含義上有何區(qū)別?思維導(dǎo)圖的括號(hào)中我已說(shuō)明了含義。前面是為了導(dǎo)入屬性(props),后面是為了導(dǎo)入?Targets。屬性就是前面?PropertyGroup?中說(shuō)的那些屬性和?ItemGroup?里說(shuō)的那些項(xiàng);而?Targets?是新東西,這才是真正用來(lái)定義編譯流程的關(guān)鍵,由于?Targets?是所有節(jié)點(diǎn)里面最復(fù)雜的部分,所以我們放到最后再說(shuō)。
那么,被我們?Import?進(jìn)來(lái)的那些文件是什么呢?用兩種擴(kuò)展名,定義屬性的那一種是?.props,定義行為的那一種是?.targets。
這兩種文件除了含義不同以外,內(nèi)容的格式都是完全一樣的——而且——就是 csproj 文件的那種格式!沒(méi)錯(cuò),也包含?Project、Import、PropertyGroup、ItemGroup、Targets。只不過(guò),相比于對(duì)完整性有要求的 csproj 文件來(lái)說(shuō),這里可以省略更多的節(jié)點(diǎn)。由于有?Import?的存在,所以一層一層地嵌套?props?或者?targets?都是可能的。
說(shuō)了這么多,讓我們來(lái)看其中兩個(gè) .props 文件吧。
先看看舊格式 csproj 文件中第一行一定會(huì)?Import?的那個(gè)?Microsoft.Common.props。
truetruetruetruetrue
文件太長(zhǎng),做了大量刪減,但也可以看到文件格式與 csproj 幾乎是一樣的。此文件中,根據(jù)其他屬性的值有條件地定義了另一些屬性。
再看看另一個(gè) MSTest 單元測(cè)試項(xiàng)目中被隱式?Import?進(jìn) csproj 文件中的 .props 文件。(所謂隱式地?Import,只不過(guò)是被間接地引入,在 csproj 文件中看不到這個(gè)文件名而已。至于如何間接引入,因?yàn)樯婕暗?Targets,所以后面一起說(shuō)明。)
Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.dllPreserveNewestFalseMicrosoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.dllPreserveNewestFalseMicrosoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.dllPreserveNewestFalse
此文件中將三個(gè) dll 文件從 MSTest 的 NuGet 包中以鏈接的形式包含到項(xiàng)目中,并且此文件在 Visual Studio 的解決方案列表中不可見(jiàn)。
可以看出,引入的 props 文件可以實(shí)現(xiàn)幾乎與 csproj 文件中一樣的功能。
那么,既然 csproj 文件中可以完全實(shí)現(xiàn)這樣的功能,為何還要單獨(dú)用?props?文件來(lái)存放呢?原因顯而易見(jiàn)了——為了在多個(gè)項(xiàng)目中使用,一處更新,到處生效。所以有沒(méi)有覺(jué)得很好玩——如果把版本號(hào)單獨(dú)放到 props 文件中,就能做到一處更新版本號(hào),到處更新版本號(hào)啦!
Target
終于開(kāi)始說(shuō) Target 了。為什么會(huì)這么期待呢?因?yàn)榍懊媛裣碌母鞣N伏筆幾乎都要在這一節(jié)點(diǎn)得到解釋了。

一般來(lái)說(shuō),Target?節(jié)點(diǎn)寫(xiě)在 csproj 文件的末尾,但這個(gè)并不是強(qiáng)制的。Targets 是一種非常強(qiáng)大的功能擴(kuò)展方式,支持 msbuild 預(yù)定義的一些指令,支持命令行,甚至支持使用 C# 直接編寫(xiě)(當(dāng)然編譯成 dll 會(huì)更方便些),還支持這些的排列組合和順序安排。而我們實(shí)質(zhì)上的編譯過(guò)程便全部由這些 Targets 來(lái)完成。我們甚至可以直接說(shuō)——編譯過(guò)程就是靠這些?Target?的組合來(lái)完成的。
如果你希望全面了解 Targets,推薦直接閱讀微軟的官方文檔?MSBuild Targets,而本文只會(huì)對(duì)其進(jìn)行一些簡(jiǎn)單的概述(我即將用另一篇博客來(lái)詳細(xì)講解,不然這篇就太長(zhǎng)了)。
不過(guò),為了簡(jiǎn)單地理解?Target,我依然需要借用官方文檔的例子作為開(kāi)頭。
這份代碼定義了一個(gè)名為?Construct?的?Target,這是隨意取的一個(gè)名字,并不重要——但是編譯過(guò)程中會(huì)執(zhí)行這個(gè)?Target。在這個(gè)?Target?內(nèi)部,使用了一個(gè) msbuild 自帶的名為?Csc?的?Task。這里我們?cè)俅我肓艘粋€(gè)新的概念?Task。而?Task?是?Target內(nèi)部真正完成邏輯性任務(wù)的核心;或者說(shuō)?Target?其實(shí)只是一種容器,本身并不包含編譯邏輯,但它的內(nèi)部可以存放?Task?來(lái)實(shí)現(xiàn)編譯邏輯。一個(gè)?Target?內(nèi)可以放多個(gè)?Task,不止如此,還能放?PropertyGroup?和?ItemGroup,不過(guò)這是僅在編譯期生效的屬性和項(xiàng)了。
@(Compile)?是?ItemGroup?中所有?Compile?類(lèi)型節(jié)點(diǎn)的集合。還記得我們?cè)?ItemGroup?小節(jié)時(shí)說(shuō)到每一種?Item?的含義由外部定義嗎?是的,就是在這里定義的!本身并沒(méi)有什么含義,但它們作為參數(shù)傳入到了具體的?Task?之后便有了此?Task?指定的含義。
于是??的含義便是調(diào)用 msbuild 內(nèi)置的 C# 編譯器編譯所有?Compile?類(lèi)型的項(xiàng)。
如果后面定義了一個(gè)跟此名稱(chēng)一樣的?Target,那么后一個(gè)?Target?就會(huì)覆蓋前一個(gè)?Target,導(dǎo)致前一個(gè)?Target?失效。
再次回到傳統(tǒng)的 csproj 文件上來(lái),每一個(gè)傳統(tǒng)格式的 csproj 都有這樣一行:
而引入的這份?.targets?文件便包含了 msbuild 定義的各種核心編譯任務(wù)。只要引入了這個(gè)?.targets?文件,便能使用 msbuild 自帶的編譯任務(wù)完成絕大多數(shù)項(xiàng)目的編譯。你可以自己去查看此文件中的內(nèi)容,相信有以上?Target?的簡(jiǎn)單介紹,應(yīng)該能大致理解其完成編譯的流程。這是我的地址:C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\Microsoft.CSharp.targets。
Project
所有的 csproj 文件都是以?Project?節(jié)點(diǎn)為根節(jié)點(diǎn)。既然是根節(jié)點(diǎn)為何我會(huì)在最后才說(shuō)?Project?呢?因?yàn)檫@可是一個(gè)大懸念??!本文一開(kāi)始就描述了新舊兩款 csproj 文件格式的差異,你也能從我的多篇博客中感受到新格式帶來(lái)的各種好處;而簡(jiǎn)潔便是新格式中最大的好處之一。它是怎么做到簡(jiǎn)潔的呢?
就靠?Project?節(jié)點(diǎn)了。
注意到新格式中?Project?節(jié)點(diǎn)有?Sdk?屬性嗎?因?yàn)橛写藢傩缘拇嬖?,csproj 文件才能如此簡(jiǎn)潔。因?yàn)椤^ Sdk,其實(shí)是一大波?.targets?文件的集合。它幫我們導(dǎo)入了公共的屬性、公共的編譯任務(wù),還幫我們自動(dòng)將項(xiàng)目文件夾下所有的?**\*.cs?文件都作為?ItemGroup?的項(xiàng)引入進(jìn)來(lái)。
如果你希望看看?Microsoft.NET.Sdk?都引入了哪些文件,可以去本機(jī)安裝的 msbuild 或 dotnet 的目錄下查看。當(dāng)我使用 msbuild 編譯時(shí),我的地址:C:\Program Files\dotnet\sdk\2.1.200\Sdks\Microsoft.NET.Sdk\build\。比如你可以從此文件夾里的?Microsoft.NET.GenerateAssemblyInfo.targets?文件中發(fā)現(xiàn)?AssemblyInfo.cs?文件是如何自動(dòng)生成及生效的。
編譯器是如何將這些零散的部件組織起來(lái)的?
這里說(shuō)的編譯器幾乎只指 msbuild 和 Roslyn,前者基于 .NET Framework,后者基于 .NET Core。不過(guò),它們?cè)谔幚砦覀兊捻?xiàng)目文件時(shí)的行為大多是一致的——至少對(duì)于通常項(xiàng)目來(lái)說(shuō)如此。
我們前一部分介紹每個(gè)部件的時(shí)候,已經(jīng)簡(jiǎn)單說(shuō)了其組織方式,這里我們進(jìn)行一個(gè)回顧和總結(jié)。
當(dāng) Visual Studio 打開(kāi)項(xiàng)目時(shí),它會(huì)解析里面所有的?Import?節(jié)點(diǎn),確認(rèn)應(yīng)該引入的 .props 和 .targets 文件都引入了。隨后根據(jù)?PropertyGroup?里面設(shè)置的屬性正確顯示屬性面板中的狀態(tài),根據(jù)?ItemGroup?中的項(xiàng)正確顯示解決方案管理器中的引用列表、文件列表?!@只是 Visual Studio 做的事情。
在編譯時(shí),msbuild 或 Roslyn 還會(huì)重新做一遍上面的事情——畢竟這兩個(gè)才是真正的編譯器,可不是 Visual Studio 的一部分啊。隨后,執(zhí)行編譯過(guò)程。它們會(huì)按照?Target?指定的先后順序來(lái)安排不同?Target?的執(zhí)行,當(dāng)執(zhí)行完所有的?Target,便完成了編譯過(guò)程。
新舊 csproj 在編譯過(guò)程上有什么差異?
相信讀完前面兩個(gè)部分之后,你應(yīng)該已經(jīng)了解到在格式本身上,新舊格式之間其實(shí)并沒(méi)有什么差異。或者更嚴(yán)格來(lái)說(shuō),差異只有一條——新格式在 Project 上指定了?Sdk。真正造成新舊格式在行為上的差別來(lái)源于默認(rèn)為我們項(xiàng)目?Import?進(jìn)來(lái)的那些 .props 和 .targets 不同。新格式通過(guò)?Microsoft.NET.Sdk?為我們導(dǎo)入了更現(xiàn)代化的 .props 和 .targets,而舊格式需要考慮到兼容性壓力,只能引入舊的那些 .targets。
新的?Microsoft.NET.Sdk?以不兼容的方式支持了各種新屬性,例如新的?TargetFrameworks?代替舊的?TargetFrameworkVersion,使得我們的 C# 項(xiàng)目可以脫離 .NET Framework,引入其他各種各樣的目標(biāo)框架,例如 netstandard2.0、net472、uap10.0 等(可以參考?從以前的項(xiàng)目格式遷移到 VS2017 新項(xiàng)目格式 - 林德熙)了解可以使用那些目標(biāo)框架。
新的?Microsoft.NET.Sdk?以不兼容的方式原生支持了 NuGet 包管理。也就是說(shuō)我們可以在不修改 csproj 的情況之下通過(guò) NuGet 包來(lái)擴(kuò)展 csproj 的功能。而舊的格式需要在 csproj 文件的末尾添加如下代碼才可以獲得其中一個(gè) NuGet 包功能的支持:
不過(guò)好在 NuGet 4.x 以上版本在安裝 NuGet 包時(shí)自動(dòng)為我們?cè)?csproj 中插入了以上代碼。
原文地址:https://walterlv.github.io/post/understand-the-csproj.html