LINQ之路21:LINQ to XML之生成X-DOM(Projecting)
到目前為止,我們已經討論了如何使用LINQ從一個X-DOM中獲取數據。其實,我們同樣可以使用LINQ查詢來生成一個X-DOM。數據源可以是支持LINQ查詢的任何數據,比如:
- LINQ to SQL或 Entity Framework查詢
- 本地集合
- 另外一個X-DOM
不管是何種數據源,使用LINQ來產生X-DOM的策略都是一樣的:首先寫出產生目標X-DOM的函數式構造表達式,然后針對該表達式創建LINQ查詢。
比如,假設我們想從數據庫中獲取customers并產生如下XML:
<customers>
<customer id="1">
<name>Sue</name>
<buys>3</buys>
</customer>
...
</customers>
我們首先為X-DOM寫出函數式構造表達式(這里用簡單的字符串值):
var customers =
new XElement("customers",
new XElement("customer", new XAttribute("id", 1),
new XElement("name", "Sue"),
new XElement("buys", 3)
)
);
然后我們我們把它放入數據轉換之中,創建出針對該表達式的LINQ查詢:
var customers =
new XElement("customers",
from c in dataContext.Customers
select
new XElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count)
)
);
這里是其結果XML:
<customers>
<customer id="1">
<name>Tom</name>
<buys>3</buys>
</customer>
<customer id="2">
<name>Harry</name>
<buys>2</buys>
</customer>
...
</customers>
通過把上面查詢的創建分成兩步,我們可以更清楚地理解它的工作方式:
第一步:
// 創建一個普通的LINQ to SQL查詢,產生一個XElement sequence
IEnumerable<XElement> sqlQuery =
from c in dataContext.Customers
select
new XElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count)
);
第二步:
// 創建根元素customers
var customers = new XElement("customers", sqlQuery);
唯一不同尋常的事情是內容參數sqlQuery,它不是一個簡單的XElement,而是IQueryable<XElement>(實現了IEnumerable<XElement>)。還記得X-DOM處理XML內容的過程嗎?記住,它會自動遍歷實現了IEnumerable的集合。所以,每個XElement都被作為一個子節點被添加到customers元素下。
排除空元素
假如在上面的例子中,我們還想把customer最新的大額purchase包含進來,那么我們可以創建如下查詢:
var customers =
new XElement("customers",
from c in dataContext.Customers
let lastBigBuy = (from p in c.Purchases // 子查詢獲取最新的大額訂單
where p.Price > 1000
orderby p.Date descending
select p).FirstOrDefault()
select
new XElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count),
new XElement("lastBigBuy",
new XElement("description",
lastBigBuy == null ? null : lastBigBuy.Description),
new XElement("price",
lastBigBuy == null ? 0m : lastBigBuy.Price)
)
)
);
當customer沒有大額訂單時,上面的查詢會產生空的lastBigBuy元素。其實在這種情況下,更好的解決方案是完全忽略lastBigBuy節點。我們可以通過把lastBigBuy元素的構造函數包裝在條件運算符中來實現這一目的:
select
new XElement ("customer", new XAttribute ("id", c.ID),
new XElement ("name", c.Name),
new XElement ("buys", c.Purchases.Count),
lastBigBuy == null ? null :
new XElement ("lastBigBuy",
new XElement ("description", lastBigBuy.Description),
new XElement ("price", lastBigBuy.Price)
)
)
對于沒有lastBigBuy的customers,查詢會產生null而不是空的XElement。這正是我們想要的,因為null會被X-DOM簡單的忽略掉。
Stream類型元素
如果我們僅是為了保存(或調用ToString)的目的來產生X-DOM,那么我們可以通過XStreamingElement來改善內存的使用效率。一個XStreamingElement相當于XElement的簡化版本,并且它對子節點應用延遲加載語義。
使用它時,簡單的把外層XElements替換成XStreamingElements即可:
var customers =
new XStreamingElement("customers",
from c in dataContext.Customers
select
new XStreamingElement("customer", new XAttribute("id", c.ID),
new XElement("name", c.Name),
new XElement("buys", c.Purchases.Count)
)
);
customers.Save("data.xml");
直到我們調用Save、ToString或WriteTo方法時,傳入XStreamingElement構造函數的查詢才會真正被執行。這樣就避免了立即把整個X-DOM裝載到內存之中。當然另一方面,如果我們多次調用Save,查詢也會被重復執行。還有,我們不能遍歷XStreamingElement的子節點,它沒有提供諸如Elements或Attributes之類的成員。
XStreamingElement不是從XObject(或是其它類)繼承而來,它僅有的成員除了Save、ToString和 WriteTo,就是Add方法了,Add方法用于向它添加子節點等內容。
轉換X-DOM
我們可以對現有的X-DOM進行轉換。比如,C#編譯器和Visual Studio為了描述一個項目,會創建相應的XML文件,該文件會類似以下格式:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/dev...>"
<PropertyGroup>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.11209</ProductVersion>
...
</PropertyGroup>
<ItemGroup>
<Compile Include="ObjectGraph.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Tests\Aggregation.cs" />
<Compile Include="Tests\Advanced\RecursiveXml.cs" />
</ItemGroup>
<ItemGroup>
...
</ItemGroup>
...
</Project>
假如我們現在相對該XML文件進行簡化,創建一個只包含相關文件的Report,如下所示:
<ProjectReport>
<File>ObjectGraph.cs</File>
<File>Program.cs</File>
<File>Properties\AssemblyInfo.cs</File>
<File>Tests\Aggregation.cs</File>
<File>Tests\Advanced\RecursiveXml.cs</File>
</ProjectReport>
下面的查詢可以完成該項轉換:
XElement project = XElement.Load("myProjectFile.csproj");
XNamespace ns = project.Name.Namespace; // 獲取Namespace
var query =
new XElement("ProjectReport",
from compileItem in
project.Elements(ns + "ItemGroup").Elements(ns + "Compile") // 獲取所有的Compile元素
let include = compileItem.Attribute("Include") // 獲取Include屬性
where include != null
select new XElement("File", include.Value) // 轉換X-DOM,創建新的File元素
);
查詢首先獲取所有的ItemGroup元素,然后使用Elements擴展方法獲取所有的Compile子元素(結果位于一個平展的sequence之中)。需要注意的是,我們必須指定一個XML命名空間,因為源文件中的所有節點都繼承了定義在Project元素上的命名空間,所以單憑一個本地的元素名稱如ItemGroup是不夠的。之后,我們根據Include屬性創建新的元素。
高級轉換
但我們對一個本地集合如X-DOM進行查詢時,我們可以通過自定義的查詢運算符來創建更加復雜的查詢,完成更加高級的功能。
假如在前面的例子中,我們想要的是一個按目錄分組的層次輸出,如下:
<Project>
<File>ObjectGraph.cs</File>
<File>Program.cs</File>
<Folder name="Properties">
<File>AssemblyInfo.cs</File>
</Folder>
<Folder name="Tests">
<File>Aggregation.cs</File>
<Folder name="Advanced">
<File>RecursiveXml.cs</File>
</Folder>
</Folder>
</Project>
產生這樣的結果,我們需要遞歸的處理路徑字符串如: Tests\Advanced\RecursiveXml.cs。下面的方法可以完成這項工作:它接收一個路徑sequence,產生我們期望的X-DOM層次結果級:
static IEnumerable<XElement> ExpandPaths(IEnumerable<string> paths)
{
var brokenUp = from path in paths
let split = path.Split(new char[] { '\\' }, 2)
orderby split[0]
select new
{
name = split[0],
remainder = split.ElementAtOrDefault(1)
};
IEnumerable<XElement> files = from b in brokenUp
where b.remainder == null
select new XElement("file", b.name);
IEnumerable<XElement> folders = from b in brokenUp
where b.remainder != null
group b.remainder by b.name into grp
select new XElement("folder",
new XAttribute("name", grp.Key),
ExpandPaths(grp)
);
return files.Concat(folders);
}
上面的方法中,第一個查詢以第一個反斜線符號把路徑分為兩部分:name + remainder。比如:Tests\Advanced\RecursiveXml.cs到Tests + Advanced\RecursiveXml.cs。如果remainder為null,則name就是一個文件,files查詢會把它裝載到files sequence。如果remainder不為null,name就是一個folder,folders查詢會處理這種情況。因為其它文件可能也位于相同的目錄中,我們必須按目錄名來進行分組。對每一個分組,會遞歸調用該方法來處理子元素。
最終的結果是包含了所有文件和目錄的sequence。Concat運算符會保持元素的順序,所以所有的文件按字母順序排在前面,然后是按字母順序排列的所有目錄。
有了這個方法,我們可以通過兩步來完成我們的查詢。首先,獲取所有的文件路徑:
IEnumerable<string> paths =
from compileItem in
project.Elements(ns + "ItemGroup").Elements(ns + "Compile")
let include = compileItem.Attribute("Include")
where include != null
select include.Value;
然后,在查詢中使用ExpandPaths方法來獲得最終結果:
var query = new XElement("Project", ExpandPaths(paths));
至此,LINQ之路系列博客已經接近完成了。從開始的LINQ介紹到C#3.0的語言特性;從LINQ方法語法到查詢表達式語法;從延遲執行到子查詢;從解釋查詢到LINQ to SQL/Entity Framework;從各個查詢運算符到LINQ to XML。共計21篇博客,前后歷時將近2個月,好漫長的過程,真心感謝各位園友的大力支持和陪伴!稍后,會為大家奉上系列博客后記和全篇博客導航。

浙公網安備 33010602011771號