寫了這么多個 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 文件結(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