فصل ۱۰: LINQ to XML؛ معماری X-DOM، ساخت، بارگذاری و پیمایش
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
فصل ۱۰ — LINQ to XML
تصویر آغاز فصل ۱۰ — LINQ to XML
.NET چند API برای کار با دادههای XML فراهم میکند. انتخاب اصلی برای پردازش عمومی اسناد XML، LINQ to XML است. LINQ to XML از یک مدل شیء سند XML سبکوزن و سازگار با LINQ (DOM) بهعلاوهٔ مجموعهای از اپراتورهای پرسوجوی تکمیلی تشکیل شده است.
در این فصل تمرکز کاملاً روی LINQ to XML است. فصل ۱۱ خواننده/نویسندهٔ XML با حرکت یکطرفه را پوشش میدهد و مکمل آنلاین کتاب نوعهای مربوط به schema و stylesheet را بررسی میکند. .NET همچنین DOM قدیمی مبتنی بر XmlDocument را دارد که در این کتاب پوشش داده نمیشود.
تمام نوعهای LINQ to XML در فضای نام System.Xml.Linq تعریف شدهاند.
مرور معماری
این بخش ابتدا مفهوم DOM را بسیار کوتاه معرفی میکند و سپس منطق طراحی DOM در LINQ to XML را توضیح میدهد.
DOM چیست؟
فایل XML زیر را در نظر بگیرید:
<?xml version="1.0" encoding="utf-8"?>
<customer id="123" status="archived">
<firstname>Joe</firstname>
<lastname>Bloggs</lastname>
</customer>
مانند همهٔ فایلهای XML، با declaration شروع میکنیم و سپس root element با نام customer میآید. عنصر customer دو attribute دارد که هرکدام نام (id و status) و مقدار ("123" و "archived") دارند. درون customer دو child element با نامهای firstname و lastname قرار گرفتهاند که محتوای متنی سادهٔ "Joe" و "Bloggs" دارند.
هر یک از این سازهها—declaration، element، attribute، value و text content—را میتوان با یک کلاس نمایش داد. اگر این کلاسها propertyهای collection برای نگهداری child content داشته باشند، میتوان درختی از objectها ساخت که کل سند را توصیف کند. به این ساختار Document Object Model (DOM) میگویند.
X-DOM در LINQ to XML
LINQ to XML دو جزء دارد:
- یک XML DOM که آن را X-DOM مینامیم؛
- حدود ۱۰ اپراتور پرسوجوی تکمیلی.
همانطور که انتظار میرود، X-DOM شامل نوعهایی مانند XDocument، XElement و XAttribute است. نکتهٔ جالب این است که نوعهای X-DOM وابسته به LINQ نیستند: میتوانید X-DOM را بدون نوشتن هیچ query LINQ بارگذاری، نمونهسازی، بهروزرسانی و ذخیره کنید. برعکس، از LINQ میتوان برای query روی DOM ساختهشده با نوعهای قدیمی سازگار با W3C هم استفاده کرد، ولی این کار محدود و دشوار است.
ویژگی متمایز X-DOM این است که LINQ-friendly است:
- متدهایی دارد که دنبالههای مفید
IEnumerable تولید میکنند و میتوان روی آنها query نوشت؛ - constructorهایش طوری طراحی شدهاند که بتوان درخت X-DOM را درون یک projection از LINQ ساخت.
مرور X-DOM
شکل ۱۰-۱ نوعهای اصلی X-DOM را نشان میدهد. پرکاربردترین آنها XElement است. XObject ریشهٔ سلسلهمراتب inheritance است و XElement و XDocument ریشههای سلسلهمراتب containership هستند.
شکلهای 10-1 و 10-2 — X-DOM و ساختار XMLبرچسبها شامل XDocument، XElement، XAttribute و رابطهٔ Parent/Child در درخت XML هستند.
شکل ۱۰-۲ درخت X-DOM حاصل از کد زیر را نشان میدهد:
string xml = @"<customer id='123' status='archived'>
<firstname>Joe</firstname>
<lastname>Bloggs<!--nice name--></lastname>
</customer>";
XElement customer = XElement.Parse (xml);
XObject کلاس پایهٔ abstract برای تمام محتوای XML است. این کلاس پیوندی به عنصر Parent در درخت containership و یک XDocument اختیاری تعریف میکند.
XNode کلاس پایه برای بیشتر محتوای XML بهجز attributeهاست. ویژگی متمایز XNode این است که میتواند در یک collection مرتبشده از XNodeهای با typeهای مختلف قرار گیرد. XML زیر را در نظر بگیرید:
<data>
Hello world
<subelement1/>
<!--comment-->
<subelement2/>
</data>
درون <data> ابتدا یک XText، سپس XElement، بعد XComment و سپس یک XElement دیگر داریم. در مقابل، XAttribute فقط XAttributeهای دیگر را بهعنوان peer میپذیرد.
با اینکه XNode به parent XElement دسترسی دارد، مفهومی از child node ندارد؛ این مسئولیت subclass آن یعنی XContainer است. XContainer اعضای کار با childها را تعریف میکند و base class انتزاعی برای XElement و XDocument است.
XElement اعضای مدیریت attributeها را همراه Name و Value اضافه میکند. در حالت رایج عنصر با یک child از نوع XText، property Value محتوای child را برای get و set کپسوله میکند و navigation اضافی را حذف میکند؛ بنابراین معمولاً لازم نیست مستقیماً با XText کار کنید.
XDocument ریشهٔ درخت XML را نمایش میدهد؛ دقیقتر، root XElement را wrap میکند و XDeclaration، processing instruction و سایر محتوای سطح root را میافزاید. برخلاف W3C DOM، استفاده از XDocument اختیاری است؛ میتوانید بدون ساخت آن X-DOM را load، دستکاری و save کنید. این استقلال همچنین انتقال ساده و کارآمد یک subtree به سلسلهمراتب X-DOM دیگر را ممکن میکند.
Loading و Parsing
XElement و XDocument متدهای static Load و Parse دارند:
Load از file، URI، Stream، TextReader یا XmlReader درخت X-DOM میسازد؛Parse از string درخت X-DOM میسازد.
XDocument fromWeb = XDocument.Load ("[نشانی نمونهٔ کتاب]");
XElement fromFile = XElement.Load (@"e:\media\somefile.xml");
XElement config = XElement.Parse (
@"<configuration>
<client enabled='true'>
<timeout>30</timeout>
</client>
</configuration>");
در بخشهای بعد پیمایش و update X-DOM را توضیح میدهیم. پیشنمایش کوتاه:
foreach (XElement child in config.Elements())
Console.WriteLine (child.Name); // client
XElement client = config.Element ("client");
bool enabled = (bool) client.Attribute ("enabled"); // Read attribute
Console.WriteLine (enabled); // True
client.Attribute ("enabled").SetValue (!enabled); // Update attribute
int timeout = (int) client.Element ("timeout"); // Read element
Console.WriteLine (timeout); // 30
client.Element ("timeout").SetValue (timeout * 2); // Update element
client.Add (new XElement ("retries", 3)); // Add new element
Console.WriteLine (config);
نتیجهٔ آخرین Console.WriteLine:
<configuration>
<client enabled="false">
<timeout>60</timeout>
<retries>3</retries>
</client>
</configuration>
Saving و Serializing
ToString روی هر node محتوای آن را به رشتهٔ XML با line break و indentation تبدیل میکند. با SaveOptions.DisableFormatting میتوان formatting را غیرفعال کرد. XElement و XDocument متد Save برای file، Stream، TextWriter یا XmlWriter دارند. اگر file بدهید، XML declaration خودکار نوشته میشود.
XNode.WriteTo نیز وجود دارد و فقط XmlWriter میپذیرد. جزئیات declaration در بخش «Documents and Declarations» بررسی میشود.
نمونهسازی X-DOM
بهجای Load یا Parse میتوانید objectها را دستی بسازید و با XContainer.Add به parent اضافه کنید.
XElement lastName = new XElement ("lastname", "Bloggs");
lastName.Add (new XComment ("nice name"));
XElement customer = new XElement ("customer");
customer.Add (new XAttribute ("id", 123));
customer.Add (new XElement ("firstname", "Joe"));
customer.Add (lastName);
Console.WriteLine (customer.ToString());
نتیجه:
<customer id="123">
<firstname>Joe</firstname>
<lastname>Bloggs<!--nice name--></lastname>
</customer>
هنگام ساخت XElement مقدار اختیاری است؛ میتوانید فقط نام عنصر را بدهید و بعد محتوا اضافه کنید. وقتی value دادیم، string ساده کافی بود و لازم نبود XText child را صریحاً بسازیم؛ X-DOM این کار را خودکار انجام میدهد.
Functional Construction
در مثال قبلی فهم ساختار XML از کد دشوار بود. X-DOM شیوهای به نام functional construction دارد که کل درخت را در یک expression میسازد:
XElement customer =
new XElement ("customer", new XAttribute ("id", 123),
new XElement ("firstname", "joe"),
new XElement ("lastname", "bloggs",
new XComment ("nice name")
)
);
Functional construction دو مزیت دارد: کد شبیه شکل XML است و میتواند مستقیماً در بند select یک LINQ query قرار گیرد. مثال projection از EF Core entity به X-DOM:
XElement query =
new XElement ("customers",
from c in dbContext.Customers.AsEnumerable()
select
new XElement ("customer", new XAttribute ("id", c.ID),
new XElement ("firstname", c.FirstName),
new XElement ("lastname", c.LastName,
new XComment ("nice name")
)
)
);
مشخصکردن Content
functional construction به این دلیل ممکن است که constructorهای XElement و XDocument یک آرایهٔ params object[] میپذیرند:
public XElement (XName name, params object[] content)
همین قاعده برای Add در XContainer برقرار است:
public void Add (params object[] content)
بنابراین هنگام ساخت یا append کردن X-DOM میتوان هر تعداد child object از هر type داد، چون هر چیزی در نهایت content قانونی محسوب میشود. XContainer هر content object را به ترتیب این قواعد پردازش میکند:
- اگر object برابر null باشد نادیده گرفته میشود.
- اگر از
XNode یا XStreamingElement باشد همانطور به collection Nodes اضافه میشود. - اگر
XAttribute باشد به collection Attributes اضافه میشود. - اگر string باشد در XText wrap و به Nodes افزوده میشود.
- اگر
IEnumerable را پیادهسازی کند، enumerate شده و همین قواعد برای هر عنصر اجرا میشود. - در غیر این صورت object به string تبدیل، در XText wrap و به Nodes اضافه میشود.
در نتیجه همهچیز در یکی از دو bucket یعنی Nodes یا Attributes قرار میگیرد. هر object میتواند content باشد چون در نهایت میتوان ToString آن را فراخواند و به XText تبدیل کرد.
Automatic Deep Cloning
وقتی node یا attribute به element اضافه میشود، property Parent آن روی همان element تنظیم میشود. یک node فقط یک parent element میتواند داشته باشد؛ اگر node دارای parent را به parent دوم اضافه کنید، node بهطور خودکار deep-clone میشود:
var address = new XElement ("address",
new XElement ("street", "Lawley St"),
new XElement ("town", "North Beach")
);
var customer1 = new XElement ("customer1", address);
var customer2 = new XElement ("customer2", address);
customer1.Element ("address").Element ("street").Value = "Another St";
Console.WriteLine (
customer2.Element ("address").Element ("street").Value); // Lawley St
این تکثیر خودکار باعث میشود نمونهسازی objectهای X-DOM بدون side effect بماند؛ یکی دیگر از ویژگیهای برنامهنویسی تابعی.
پیمایش و Query
کلاسهای XNode و XContainer متدها و propertyهایی برای پیمایش درخت X-DOM تعریف میکنند. برخلاف DOM سنتی، این توابع collection پیادهکنندهٔ IList<T> نمیدهند؛ یا یک مقدار منفرد یا دنبالهای از IEnumerable<T> میدهند که انتظار میرود روی آن LINQ query یا foreach اجرا کنید. بنابراین هم navigation ساده و هم queryهای پیشرفته با syntax آشنای LINQ ممکن میشود.
Child Node Navigation
| نوع خروجی | عضو | روی چه نوعی |
| XNode | FirstNode { get; } | XContainer |
| XNode | LastNode { get; } | XContainer |
| IEnumerable<XNode> | Nodes() | XContainer* |
| IEnumerable<XNode> | DescendantNodes() | XContainer* |
| IEnumerable<XNode> | DescendantNodesAndSelf() | XElement* |
| XElement | Element(XName) | XContainer |
| IEnumerable<XElement> | Elements() | XContainer* |
| IEnumerable<XElement> | Elements(XName) | XContainer* |
| IEnumerable<XElement> | Descendants() | XContainer* |
| IEnumerable<XElement> | Descendants(XName) | XContainer* |
| IEnumerable<XElement> | DescendantsAndSelf() | XElement* |
| IEnumerable<XElement> | DescendantsAndSelf(XName) | XElement* |
| bool | HasElements { get; } | XElement |
FirstNode، LastNode و Nodes
دو property نخست دسترسی مستقیم به اولین/آخرین child node و Nodes همهٔ childها را بهصورت دنباله میدهد. هر سه فقط direct descendantها را در نظر میگیرند:
var bench = new XElement ("bench",
new XElement ("toolbox",
new XElement ("handtool", "Hammer"),
new XElement ("handtool", "Rasp")
),
new XElement ("toolbox",
new XElement ("handtool", "Saw"),
new XElement ("powertool", "Nailgun")
),
new XComment ("Be careful with the nailgun")
);
foreach (XNode node in bench.Nodes())
Console.WriteLine (node.ToString (SaveOptions.DisableFormatting) + ".");
// <toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.
// <toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.
// <!--Be careful with the nailgun-->.
بازیابی elementها
Elements فقط child nodeهای نوع XElement را برمیگرداند:
foreach (XElement e in bench.Elements())
Console.WriteLine (e.Name + "=" + e.Value);
// toolbox=HammerRasp
// toolbox=SawNailgun
toolbox دارای nail gun:
IEnumerable<string> query =
from toolbox in bench.Elements()
where toolbox.Elements().Any (tool => tool.Value == "Nailgun")
select toolbox.Value;
// { "SawNailgun" }
تمام hand toolها با SelectMany:
IEnumerable<string> query =
from toolbox in bench.Elements()
from tool in toolbox.Elements()
where tool.Name == "handtool"
select tool.Value;
// { "Hammer", "Rasp", "Saw" }
فقط elementهای یک نام مشخص:
int x = bench.Elements ("toolbox").Count(); // 2
int x2 = bench.Elements().Where (e => e.Name == "toolbox").Count(); // 2
Elements بهصورت extension method برای IEnumerable<T> where T:XContainer هم تعریف شده است؛ بنابراین روی sequence از elementها نیز کار میکند:
from tool in bench.Elements ("toolbox").Elements ("handtool")
select tool.Value;
فراخوانی اول به instance method در XContainer و دومی به extension method bind میشود.
بازیابی یک element منفرد
Element (مفرد) اولین عنصر مطابق نام را برمیگرداند:
XElement settings = XElement.Load ("databaseSettings.xml");
string cx = settings.Element ("database").Element ("connectString").Value;
Element معادل Elements() سپس FirstOrDefault با predicate نام است و اگر عنصر نباشد null میدهد.
بازیابی descendantها
XContainer.Descendants و DescendantNodes child element/nodeها و تمام فرزندان آنها را تا انتهای درخت میدهند. Descendants نام element اختیاری میپذیرد:
Console.WriteLine (bench.Descendants ("handtool").Count()); // 3
هم parent و هم leaf nodeها در DescendantNodes میآیند:
foreach (XNode node in bench.DescendantNodes())
Console.WriteLine (node.ToString (SaveOptions.DisableFormatting));
// <toolbox>...</toolbox>
// <handtool>Hammer</handtool>
// Hammer
// <handtool>Rasp</handtool>
// Rasp
// <toolbox>...</toolbox>
// <handtool>Saw</handtool>
// Saw
// <powertool>Nailgun</powertool>
// Nailgun
// <!--Be careful with the nailgun-->
query زیر همهٔ commentهای هر جای X-DOM را که واژهٔ careful دارند استخراج میکند:
IEnumerable<string> query =
from c in bench.DescendantNodes().OfType<XComment>()
where c.Value.Contains ("careful")
orderby c.Value
select c.Value;
Parent Navigation
تمام XNodeها property Parent و متدهای AncestorXXX برای حرکت به بالا دارند. parent همیشه XElement است.
| نوع خروجی | عضو | روی چه نوعی |
|---|
| XElement | Parent { get; } | XNode |
| IEnumerable<XElement> | Ancestors() | XNode |
| IEnumerable<XElement> | Ancestors(XName) | XNode |
| IEnumerable<XElement> | AncestorsAndSelf() | XElement |
| IEnumerable<XElement> | AncestorsAndSelf(XName) | XElement |
اگر x یک XElement باشد، این همیشه true چاپ میکند:
foreach (XNode child in x.Nodes())
Console.WriteLine (child.Parent == x);
برای XDocument چنین نیست: XDocument میتواند child داشته باشد ولی parent هیچکس نمیشود. برای دسترسی به XDocument از property Document استفاده کنید که روی هر object در درخت X-DOM کار میکند.
Ancestors دنبالهای میدهد که عنصر اول Parent، بعد Parent.Parent و ... تا root است.
Peer Node Navigation
| نوع خروجی | عضو | تعریفشده در |
|---|
| bool | IsBefore(XNode node) | XNode |
| bool | IsAfter(XNode node) | XNode |
| XNode | PreviousNode { get; } | XNode |
| XNode | NextNode { get; } | XNode |
| IEnumerable<XNode> | NodesBeforeSelf() | XNode |
| IEnumerable<XNode> | NodesAfterSelf() | XNode |
| IEnumerable<XElement> | ElementsBeforeSelf() / overload نامدار | XNode |
| IEnumerable<XElement> | ElementsAfterSelf() / overload نامدار | XNode |
با PreviousNode و NextNode (و FirstNode/LastNode) میتوان nodeها را با حس linked list پیمایش کرد؛ اتفاقی نیست، چون nodeها درون X-DOM در linked list ذخیره میشوند.
Attribute Navigation
| نوع خروجی | عضو | تعریفشده در |
|---|
| bool | HasAttributes { get; } | XElement |
| XAttribute | Attribute(XName name) | XElement |
| XAttribute | FirstAttribute { get; } | XElement |
| XAttribute | LastAttribute { get; } | XElement |
| IEnumerable<XAttribute> | Attributes() | XElement |
| IEnumerable<XAttribute> | Attributes(XName name) | XElement |
علاوه بر این، XAttribute propertyهای PreviousAttribute و NextAttribute و نیز Parent دارد. Attributes(name) دنبالهای با صفر یا یک عنصر برمیگرداند، چون در XML یک element نمیتواند attributeهای با نام تکراری داشته باشد.