理解 C# 項目 csproj 文件格式的本質(zhì)和編譯流程

寫了這么多個 C# 項目,是否對項目文件 csproj 有一些了解呢犀被?Visual Studio 是怎么讓 csproj 中的內(nèi)容正確顯示出來的呢橄仆?更深入的,我能夠自己擴展 csproj 的功能嗎耳奕?

本文將直接從 csproj 文件格式的本質(zhì)來看以上這些問題绑青。

閱讀本文,你將:

可以通讀 csproj 文件屋群,并說出其中每一行的含義

可以手工修改 csproj 文件闸婴,以實現(xiàn)你希望達到的高級功能(更高級的,可以開始寫個工具自動完成這樣的工作了)

理解新舊 csproj 文件的差異芍躏,不至于寫工具解析和修改 csproj 文件的時候出現(xiàn)不兼容的錯誤

csproj 里面是什么邪乍?

總覽 csproj 文件

相信你一定見過傳統(tǒng)的 csproj 文件格式。就算你幾乎從來沒主動去看過里面的內(nèi)容对竣,在版本管理工具中解沖突時也在里面修改過內(nèi)容庇楞。

不管你是新手還是老手,一定都會覺得這么長這么復(fù)雜的文件一定不是給人類閱讀的否纬。你說的是對的吕晌!傳統(tǒng) csproj 文件中有大量的重復(fù)或者相似內(nèi)容,只為 msbuild 和 Visual Studio 能夠識別整個項目的屬性和結(jié)構(gòu)临燃,以便正確編譯項目睛驳。

不過烙心,既然這篇文章的目標是理解 csproj 文件格式的本質(zhì),那我當然不會把這么復(fù)雜的文件內(nèi)容直接給你去閱讀乏沸。

我已經(jīng)將整個文件結(jié)構(gòu)進行了極度簡化淫茵,然后用思維導(dǎo)圖進行了分割〉旁荆總結(jié)成了下圖匙瘪,如果先不關(guān)注文件的細節(jié),是不是更容易看懂了呢蝶缀?

如果你此前也閱讀過我的其他博客丹喻,會發(fā)現(xiàn)我一直在試圖推薦使用新的 csproj 格式:

將 WPF、UWP 以及其他各種類型的舊樣式的 csproj 文件遷移成新樣式的 csproj 文件

讓一個 csproj 項目指定多個開發(fā)框架

那么新格式和舊格式究竟有哪些不同使得新的格式如此簡潔扼劈?

于是驻啤,我將新的 csproj 文件結(jié)構(gòu)也進行簡化菲驴,用思維導(dǎo)圖進行了分割荐吵。總結(jié)成了下圖:

比較兩個思維導(dǎo)圖之后赊瞬,是不是發(fā)現(xiàn)其實兩者本是相同的格式先煎。如果忽略我在文字顏色上做的標記,其實兩者的差異幾乎只在文件開頭是否有一個 xml 文件標記()巧涧。我在文字顏色上的標記代表著這部分的部件是否是可選的薯蝎,白色代表必須,灰色代表可選谤绳;而更接近背景色的灰色代表一般情況下都是不需要的占锯。

我把兩個思維導(dǎo)圖放到一起方便比較:

會發(fā)現(xiàn),傳統(tǒng)格式中?xml 聲明缩筛、Project 節(jié)點消略、Import (props)、PropertyGroup瞎抛、ItemGroup艺演、Import (targets)?都是必要的,而新格式中只有?Project 節(jié)點?和?PropertyGroup?是必要的桐臊。

是什么導(dǎo)致了這樣的差異胎撤?在了解 csproj 文件中各個部件的作用之前,這似乎很難回答断凶。

了解 csproj 中的各個部件的作用

xml 聲明部分完全沒有在此解釋的必要了伤提,為兼容性提供了方便,詳見:XML - Wikipedia认烁。

接下來飘弧,我們不會依照部件出現(xiàn)的順序安排描述的順序识藤,而是按照關(guān)注程度排序。

PropertyGroup

PropertyGroup?是用來存放屬性的地方次伶,這與它的名字非常契合痴昧。那么里面放什么屬性呢?答案是——什么都能放冠王!

在這里寫屬性就像在代碼中定義屬性或變量一樣赶撰,只要寫了,就會生成一個指定名稱的屬性柱彻。

比如豪娜,我們寫:

walterlv is a 逗比

那么,就會生成一個?Foo?屬性哟楷,值為字符串?walterlv is a 逗比瘤载。至于這個屬性有什么用,那就不歸這里管了卖擅。

這些屬性的含義完全是由外部來決定的鸣奔,例如在舊的 csproj 格式中,編譯過程中會使用?TargetFrameworkVersion?屬性惩阶,以確定編譯應(yīng)該使用的 .NET Framework 目標框架的版本(是 v4.5 還是 v4.7)挎狸。在新的 csproj 格式中,編譯過程會使用?TargetFrameworks?屬性來決定編譯應(yīng)該使用的目標框架(是 net47 還是 netstandard2.0)断楷。具體是編譯過程中的哪個環(huán)節(jié)哪個組件使用了此屬性锨匆,我們后面會說。

從這個角度來說冬筒,如果你沒有任何地方用到了你定義的屬性恐锣,那為什么還要定義它呢?是的——這只是浪費舞痰。

PropertyGroup?可以定義很多個土榴,里面都可以同等地放屬性。至于為什么會定義多個匀奏,原因無外乎兩個:

為了可讀性——將一組相關(guān)的屬性放在一起鞭衩,便于閱讀和理解意圖(舊的 csproj 談不上什么可讀性)

為了加條件——有的屬性在 Debug 和 Release 下不一樣(例如條件編譯符?DefineConstants)

額外說一下,Debug?和?Release?這兩個值其實是在某處一個名為?Configuration?的屬性定義的娃善,它們其實只是普通的字符串而已论衍,沒什么特殊的意義,只是有很多的?PropertyGroup?加上了?Debug?Release?的判斷條件才使得不同的?Configuration?具有不同的其他屬性聚磺,最終表現(xiàn)為編譯后的巨大差異坯台。由于?Configuration?屬性可以放任意字符串,所以甚至可以定義一個非?Debug?和?Release?的配置(例如用于性能專項測試)也是可以的瘫寝。

ItemGroup

ItemGroup?是用來指定集合的地方蜒蕾,這與它的名字非常契合稠炬。那么這集合里面放什么項呢?答案是——什么都能放咪啡!

是不是覺得這句話跟前面的?PropertyGroup?句式一模一樣首启?是的——就是一模一樣!csproj 中的兩個大頭都這樣不帶語義撤摸,幾乎可以說明 csproj 文件是不包含語義的毅桃,它能夠用來做什么事情純屬由其他模塊來指定;這為 csproj 文件強大的擴展性提供了格式基礎(chǔ)准夷。

既然什么都能放钥飞,那我們放這些吧:

walterlv is a 逗比walterlv is a 天才天才向左,逗比向右逗比屬性額外加成

于是我們就有 4 個類型為?Foo?的項了衫嵌,至于這 4 個?Foo?項有什么作用读宙,那就不歸這里管了。

這些項的含義與?PropertyGroup?一樣也是由外部來決定楔绞。具體是哪個外部结闸,我們稍后會說。但是我們依然有一些常見的項可以先介紹介紹:

Reference?引用某個程序集

PackageReference?引用某個 NuGet 包

ProjectReference?引用某個項目

Compile?常規(guī)的 C# 編譯

None?沒啥特別的編譯選項墓律,就為了執(zhí)行一些通用的操作(或者是只是為了在 Visual Studio 列表中能夠有一個顯示)

Folder?一個空的文件夾膀估,也沒啥用(不過標了這個文件夾幔亥,Visual Studio 中就能有一個文件夾的顯式耻讽,即便實際上這個文件夾可能不存在)

ItemGroup?也可以放很多組,一樣是為了提升可讀性或者增加條件帕棉。

Import

你應(yīng)該注意到在前面的思維導(dǎo)圖中针肥,無論是新 csproj 還是舊 csproj 文件,我都寫了兩個?Import?節(jié)點香伴。其實它們本質(zhì)上是完全一樣的慰枕,只不過在含義上有不同。前面我們了解到 csproj 文件致力于脫離語義即纲,所以分開兩個地方寫幾乎只是為了可讀性考慮具帮。

那么前面那個?Import?和后面的?Import?在含義上有何區(qū)別?思維導(dǎo)圖的括號中我已說明了含義低斋。前面是為了導(dǎo)入屬性(props)蜂厅,后面是為了導(dǎo)入?Targets。屬性就是前面?PropertyGroup?中說的那些屬性和?ItemGroup?里說的那些項膊畴;而?Targets?是新東西掘猿,這才是真正用來定義編譯流程的關(guān)鍵,由于?Targets?是所有節(jié)點里面最復(fù)雜的部分唇跨,所以我們放到最后再說稠通。

那么衬衬,被我們?Import?進來的那些文件是什么呢?用兩種擴展名改橘,定義屬性的那一種是?.props滋尉,定義行為的那一種是?.targets。

這兩種文件除了含義不同以外飞主,內(nèi)容的格式都是完全一樣的——而且——就是 csproj 文件的那種格式兼砖!沒錯,也包含?Project既棺、Import讽挟、PropertyGroup、ItemGroup丸冕、Targets耽梅。只不過,相比于對完整性有要求的 csproj 文件來說胖烛,這里可以省略更多的節(jié)點眼姐。由于有?Import?的存在,所以一層一層地嵌套?props?或者?targets?都是可能的佩番。

說了這么多众旗,讓我們來看其中兩個 .props 文件吧。

先看看舊格式 csproj 文件中第一行一定會?Import?的那個?Microsoft.Common.props趟畏。

truetruetruetruetrue

文件太長贡歧,做了大量刪減,但也可以看到文件格式與 csproj 幾乎是一樣的赋秀。此文件中利朵,根據(jù)其他屬性的值有條件地定義了另一些屬性。

再看看另一個 MSTest 單元測試項目中被隱式?Import?進 csproj 文件中的 .props 文件猎莲。(所謂隱式地?Import绍弟,只不過是被間接地引入,在 csproj 文件中看不到這個文件名而已著洼。至于如何間接引入樟遣,因為涉及到?Targets,所以后面一起說明身笤。

Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.dllPreserveNewestFalseMicrosoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.dllPreserveNewestFalseMicrosoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.dllPreserveNewestFalse

此文件中將三個 dll 文件從 MSTest 的 NuGet 包中以鏈接的形式包含到項目中豹悬,并且此文件在 Visual Studio 的解決方案列表中不可見。

可以看出展鸡,引入的 props 文件可以實現(xiàn)幾乎與 csproj 文件中一樣的功能屿衅。

那么,既然 csproj 文件中可以完全實現(xiàn)這樣的功能莹弊,為何還要單獨用?props?文件來存放呢涤久?原因顯而易見了——為了在多個項目中使用涡尘,一處更新,到處生效响迂。所以有沒有覺得很好玩——如果把版本號單獨放到 props 文件中考抄,就能做到一處更新版本號,到處更新版本號啦蔗彤!

Target

終于開始說 Target 了川梅。為什么會這么期待呢?因為前面埋下的各種伏筆幾乎都要在這一節(jié)點得到解釋了然遏。

一般來說贫途,Target?節(jié)點寫在 csproj 文件的末尾,但這個并不是強制的待侵。Targets 是一種非常強大的功能擴展方式丢早,支持 msbuild 預(yù)定義的一些指令,支持命令行秧倾,甚至支持使用 C# 直接編寫(當然編譯成 dll 會更方便些)怨酝,還支持這些的排列組合和順序安排。而我們實質(zhì)上的編譯過程便全部由這些 Targets 來完成那先。我們甚至可以直接說——編譯過程就是靠這些?Target?的組合來完成的农猬。

如果你希望全面了解 Targets,推薦直接閱讀微軟的官方文檔?MSBuild Targets售淡,而本文只會對其進行一些簡單的概述(我即將用另一篇博客來詳細講解斤葱,不然這篇就太長了)。

不過勋又,為了簡單地理解?Target苦掘,我依然需要借用官方文檔的例子作為開頭换帜。

這份代碼定義了一個名為?Construct?的?Target楔壤,這是隨意取的一個名字,并不重要——但是編譯過程中會執(zhí)行這個?Target惯驼。在這個?Target?內(nèi)部蹲嚣,使用了一個 msbuild 自帶的名為?Csc?的?Task。這里我們再次引入了一個新的概念?Task祟牲。而?Task?是?Target內(nèi)部真正完成邏輯性任務(wù)的核心隙畜;或者說?Target?其實只是一種容器,本身并不包含編譯邏輯说贝,但它的內(nèi)部可以存放?Task?來實現(xiàn)編譯邏輯议惰。一個?Target?內(nèi)可以放多個?Task,不止如此乡恕,還能放?PropertyGroup?和?ItemGroup言询,不過這是僅在編譯期生效的屬性和項了俯萎。

@(Compile)?是?ItemGroup?中所有?Compile?類型節(jié)點的集合。還記得我們在?ItemGroup?小節(jié)時說到每一種?Item?的含義由外部定義嗎运杭?是的夫啊,就是在這里定義的!本身并沒有什么含義辆憔,但它們作為參數(shù)傳入到了具體的?Task?之后便有了此?Task?指定的含義撇眯。

于是??的含義便是調(diào)用 msbuild 內(nèi)置的 C# 編譯器編譯所有?Compile?類型的項。

如果后面定義了一個跟此名稱一樣的?Target虱咧,那么后一個?Target?就會覆蓋前一個?Target熊榛,導(dǎo)致前一個?Target?失效。

再次回到傳統(tǒng)的 csproj 文件上來腕巡,每一個傳統(tǒng)格式的 csproj 都有這樣一行:

而引入的這份?.targets?文件便包含了 msbuild 定義的各種核心編譯任務(wù)来候。只要引入了這個?.targets?文件,便能使用 msbuild 自帶的編譯任務(wù)完成絕大多數(shù)項目的編譯逸雹。你可以自己去查看此文件中的內(nèi)容营搅,相信有以上?Target?的簡單介紹,應(yīng)該能大致理解其完成編譯的流程梆砸。這是我的地址:C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\Microsoft.CSharp.targets转质。

Project

所有的 csproj 文件都是以?Project?節(jié)點為根節(jié)點。既然是根節(jié)點為何我會在最后才說?Project?呢帖世?因為這可是一個大懸念靶菪贰!本文一開始就描述了新舊兩款 csproj 文件格式的差異日矫,你也能從我的多篇博客中感受到新格式帶來的各種好處赂弓;而簡潔便是新格式中最大的好處之一。它是怎么做到簡潔的呢哪轿?

就靠?Project?節(jié)點了盈魁。

注意到新格式中?Project?節(jié)點有?Sdk?屬性嗎?因為有此屬性的存在窃诉,csproj 文件才能如此簡潔杨耙。因為——所謂 Sdk,其實是一大波?.targets?文件的集合飘痛。它幫我們導(dǎo)入了公共的屬性珊膜、公共的編譯任務(wù),還幫我們自動將項目文件夾下所有的?**\*.cs?文件都作為?ItemGroup?的項引入進來宣脉。

如果你希望看看?Microsoft.NET.Sdk?都引入了哪些文件车柠,可以去本機安裝的 msbuild 或 dotnet 的目錄下查看。當我使用 msbuild 編譯時,我的地址:C:\Program Files\dotnet\sdk\2.1.200\Sdks\Microsoft.NET.Sdk\build\竹祷。比如你可以從此文件夾里的?Microsoft.NET.GenerateAssemblyInfo.targets?文件中發(fā)現(xiàn)?AssemblyInfo.cs?文件是如何自動生成及生效的介蛉。

編譯器是如何將這些零散的部件組織起來的?

這里說的編譯器幾乎只指 msbuild 和 Roslyn溶褪,前者基于 .NET Framework币旧,后者基于 .NET Core。不過猿妈,它們在處理我們的項目文件時的行為大多是一致的——至少對于通常項目來說如此吹菱。

我們前一部分介紹每個部件的時候,已經(jīng)簡單說了其組織方式彭则,這里我們進行一個回顧和總結(jié)鳍刷。

當 Visual Studio 打開項目時,它會解析里面所有的?Import?節(jié)點俯抖,確認應(yīng)該引入的 .props 和 .targets 文件都引入了输瓜。隨后根據(jù)?PropertyGroup?里面設(shè)置的屬性正確顯示屬性面板中的狀態(tài),根據(jù)?ItemGroup?中的項正確顯示解決方案管理器中的引用列表芬萍、文件列表尤揣。——這只是 Visual Studio 做的事情柬祠。

在編譯時北戏,msbuild 或 Roslyn 還會重新做一遍上面的事情——畢竟這兩個才是真正的編譯器,可不是 Visual Studio 的一部分啊漫蛔。隨后嗜愈,執(zhí)行編譯過程。它們會按照?Target?指定的先后順序來安排不同?Target?的執(zhí)行莽龟,當執(zhí)行完所有的?Target蠕嫁,便完成了編譯過程。

新舊 csproj 在編譯過程上有什么差異毯盈?

相信讀完前面兩個部分之后剃毒,你應(yīng)該已經(jīng)了解到在格式本身上,新舊格式之間其實并沒有什么差異奶镶〕僭撸或者更嚴格來說,差異只有一條——新格式在 Project 上指定了?Sdk厂镇。真正造成新舊格式在行為上的差別來源于默認為我們項目?Import?進來的那些 .props 和 .targets 不同。新格式通過?Microsoft.NET.Sdk?為我們導(dǎo)入了更現(xiàn)代化的 .props 和 .targets左刽,而舊格式需要考慮到兼容性壓力捺信,只能引入舊的那些 .targets。

新的?Microsoft.NET.Sdk?以不兼容的方式支持了各種新屬性,例如新的?TargetFrameworks?代替舊的?TargetFrameworkVersion迄靠,使得我們的 C# 項目可以脫離 .NET Framework秒咨,引入其他各種各樣的目標框架,例如 netstandard2.0掌挚、net472雨席、uap10.0 等(可以參考?從以前的項目格式遷移到 VS2017 新項目格式 - 林德熙)了解可以使用那些目標框架。

新的?Microsoft.NET.Sdk?以不兼容的方式原生支持了 NuGet 包管理吠式。也就是說我們可以在不修改 csproj 的情況之下通過 NuGet 包來擴展 csproj 的功能陡厘。而舊的格式需要在 csproj 文件的末尾添加如下代碼才可以獲得其中一個 NuGet 包功能的支持:

不過好在 NuGet 4.x 以上版本在安裝 NuGet 包時自動為我們在 csproj 中插入了以上代碼。

原文地址:https://walterlv.github.io/post/understand-the-csproj.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末特占,一起剝皮案震驚了整個濱河市糙置,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌是目,老刑警劉巖谤饭,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異懊纳,居然都是意外死亡揉抵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門嗤疯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來功舀,“玉大人,你說我怎么就攤上這事身弊”偬” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵阱佛,是天一觀的道長帖汞。 經(jīng)常有香客問我,道長凑术,這世上最難降的妖魔是什么翩蘸? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮淮逊,結(jié)果婚禮上催首,老公的妹妹穿的比我還像新娘。我一直安慰自己泄鹏,他們只是感情好郎任,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著备籽,像睡著了一般舶治。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天霉猛,我揣著相機與錄音尺锚,去河邊找鬼。 笑死惜浅,一個胖子當著我的面吹牛瘫辩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播坛悉,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼伐厌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吹散?” 一聲冷哼從身側(cè)響起弧械,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎空民,沒想到半個月后刃唐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡界轩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年画饥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浊猾。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡抖甘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出葫慎,到底是詐尸還是另有隱情衔彻,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布偷办,位于F島的核電站艰额,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏椒涯。R本人自食惡果不足惜柄沮,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望废岂。 院中可真熱鬧祖搓,春花似錦、人聲如沸湖苞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袒啼。三九已至哈扮,卻和暖如春纬纪,著一層夾襖步出監(jiān)牢的瞬間蚓再,已是汗流浹背滑肉。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留摘仅,地道東北人靶庙。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像娃属,于是被迫代替她去往敵國和親六荒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容