فصل ۱۰: بهروزرسانی X-DOM، Values، Namespaceها و Projection در LINQ to XML
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
بهروزرسانی X-DOM
elementها و attributeها را میتوان به روشهای زیر بهروزرسانی کرد:
- فراخوانی
SetValue یا مقداردهی مجدد Value؛ SetElementValue یا SetAttributeValue؛- یکی از متدهای
RemoveXXX؛ - یکی از متدهای
AddXXX یا ReplaceXXX با محتوای جدید.
همچنین میتوان property Name روی XElement را دوباره مقداردهی کرد.
بهروزرسانی سادهٔ Value
| عضو | روی چه نوعی |
|---|
SetValue(object value) | XElement, XAttribute |
Value { get; set; } | XElement, XAttribute |
SetValue محتوای element یا attribute را با یک مقدار ساده جایگزین میکند. مقداردهی Value نیز همین کار را میکند، اما فقط string میپذیرد. اثر SetValue یا assign کردن Value این است که تمام child nodeها جایگزین میشوند:
XElement settings = new XElement ("settings",
new XElement ("timeout", 30)
);
settings.SetValue ("blah");
Console.WriteLine (settings.ToString()); // <settings>blah</settings>
بهروزرسانی Child Nodeها و Attributeها
| دسته | عضو | روی چه نوعی |
|---|
| Add | Add(params object[] content) | XContainer |
| Add | AddFirst(params object[] content) | XContainer |
| Remove | RemoveNodes() | XContainer |
| Remove | RemoveAttributes() | XElement |
| Remove | RemoveAll() | XElement |
| Update | ReplaceNodes(params object[] content) | XContainer |
| Update | ReplaceAttributes(params object[] content) | XElement |
| Update | ReplaceAll(params object[] content) | XElement |
| Update | SetElementValue(XName name, object value) | XElement |
| Update | SetAttributeValue(XName name, object value) | XElement |
راحتترین متدها در این گروه دو مورد آخرند. SetElementValue و SetAttributeValue میانبری برای ساخت XElement/XAttribute و سپس افزودن آن به parent هستند و element/attribute موجود با همان نام را جایگزین میکنند:
XElement settings = new XElement ("settings");
settings.SetElementValue ("timeout", 30); // Adds child node
settings.SetElementValue ("timeout", 60); // Update it to 60
Add child node را در انتهای element/document اضافه میکند؛ AddFirst در ابتدای collection درج میکند. با RemoveNodes یا RemoveAttributes میتوان یکباره همهٔ child nodeها یا attributeها را حذف کرد. RemoveAll معادل هر دو است. متدهای ReplaceXXX عملاً Remove سپس Add هستند و snapshot ورودی میگیرند؛ بنابراین e.ReplaceNodes(e.Nodes()) مطابق انتظار کار میکند.
بهروزرسانی از طریق Parent
| عضو | روی چه نوعی |
|---|
AddBeforeSelf(params object[] content) | XNode |
AddAfterSelf(params object[] content) | XNode |
Remove() | XNode, XAttribute |
ReplaceWith(params object[] content) | XNode |
این متدها روی childهای node کار نمیکنند، بلکه روی collectionای کار میکنند که خود node در آن قرار دارد؛ پس node باید parent داشته باشد وگرنه exception. AddBeforeSelf/AddAfterSelf برای درج در موقعیت دلخواه مفیدند:
XElement items = new XElement ("items",
new XElement ("one"),
new XElement ("three")
);
items.FirstNode.AddAfterSelf (new XElement ("two"));
// <items><one /><two /><three /></items>
درج در موقعیت دلخواه در دنبالهٔ طولانی کارآمد است، چون nodeها در linked list نگهداری میشوند. Remove node فعلی را از parent حذف میکند؛ ReplaceWith همین کار را کرده و content دیگری را در همان موقعیت درج میکند:
XElement items = XElement.Parse ("<items><one/><two/><three/></items>");
items.FirstNode.ReplaceWith (new XComment ("One was here"));
نتیجه:
<items><!--one was here--><two /><three /></items>
حذف دنبالهای از Nodeها یا Attributeها
به کمک extension methodهای System.Xml.Linq میتوانید Remove را روی sequence از node یا attribute هم صدا بزنید. X-DOM زیر را در نظر بگیرید:
XElement contacts = XElement.Parse (
@"<contacts>
<customer name='Mary'/>
<customer name='Chris' archived='true'/>
<supplier name='Susan'>
<phone archived='true'>012345678<!--confidential--></phone>
</supplier>
</contacts>");
حذف همهٔ customerها:
contacts.Elements ("customer").Remove();
حذف همهٔ contactهای archived (در نتیجه Chris حذف میشود):
contacts.Elements().Where (e => (bool?) e.Attribute ("archived") == true)
.Remove();
اگر Elements() را با Descendants() عوض کنیم، همهٔ elementهای archived در کل DOM حذف میشوند و نتیجه چنین است:
<contacts>
<customer name="Mary" />
<supplier name="Susan" />
</contacts>
حذف تمام contactهایی که در هر جای درختشان comment با متن confidential دارند:
contacts.Elements().Where (e => e.DescendantNodes()
.OfType<XComment>()
.Any (c => c.Value == "confidential")
).Remove();
نتیجه:
<contacts>
<customer name="Mary" />
<customer name="Chris" archived="true" />
</contacts>
در مقابل، query سادهٔ زیر همهٔ comment nodeها را از درخت پاک میکند:
contacts.DescendantNodes().OfType<XComment>().Remove();
کار با Valueها
XElement و XAttribute هر دو property Value از نوع string دارند. اگر element فقط یک XText child داشته باشد، Value در XElement میانبر محتوای آن node است؛ در XAttribute نیز Value همان مقدار attribute است. با وجود تفاوت ذخیرهسازی، X-DOM عملیات یکسانی برای valueهای element و attribute ارائه میکند.
Setting Values
دو راه برای assign کردن value وجود دارد: SetValue یا property Value. SetValue منعطفتر است و علاوه بر string نوعهای سادهٔ دیگر را میپذیرد:
var e = new XElement ("date", DateTime.Now);
e.SetValue (DateTime.Now.AddDays(1));
Console.Write (e.Value); // 2019-10-02T16:39:10.734375+09:00
میتوانستیم Value را مستقیم set کنیم، اما باید DateTime را دستی و با XmlConvert به format سازگار XML تبدیل میکردیم. constructorهای XElement/XAttribute نیز برای valueهای nonstring همین تبدیل خودکار را انجام میدهند؛ به این ترتیب DateTime درست format میشود، true با حروف کوچک نوشته میشود و double.NegativeInfinity به -INF تبدیل میشود.
Getting Values
برای parse کردن Value به base type، خود XElement/XAttribute را به type مطلوب cast میکنید:
XElement e = new XElement ("now", DateTime.Now);
DateTime dt = (DateTime) e;
XAttribute a = new XAttribute ("resolution", 1.234);
double res = (double) a;
element/attribute DateTime یا number را ذاتاً نگه نمیدارد؛ همیشه text ذخیره میشود و هنگام نیاز parse میشود. type اصلی نیز «به خاطر سپرده» نمیشود، پس cast باید درست باشد وگرنه runtime error. برای robustness میتوانید cast را در try/catch و FormatException قرار دهید.
explicit castهای XElement/XAttribute میتوانند به این typeها parse کنند: همهٔ numeric typeهای استاندارد؛ string، bool، DateTime، DateTimeOffset، TimeSpan، Guid؛ و نسخههای Nullable<> value typeهای گفتهشده.
cast به nullable همراه Element/Attribute مفید است؛ اگر نام خواستهشده نباشد cast همچنان کار میکند:
int timeout = (int) x.Element ("timeout"); // Error
int? timeout = (int?) x.Element ("timeout"); // OK; null
با ?? nullable را در نتیجهٔ نهایی حذف میکنیم؛ اگر attribute resolution نباشد مقدار 1.0:
double resolution = (double?) x.Attribute ("resolution") ?? 1.0;
nullable cast وقتی element/attribute وجود دارد اما empty یا بدفرمت است کمکی نمیکند؛ آنجا باید FormatException را catch کنید.
cast در LINQ query نیز قابل استفاده است. query زیر «John» را میدهد:
var data = XElement.Parse (
@"<data>
<customer id='1' name='Mary' credit='100' />
<customer id='2' name='John' credit='150' />
<customer id='3' name='Anne' />
</data>");
IEnumerable<string> query = from cust in data.Elements()
where (int?) cust.Attribute ("credit") > 100
select cust.Attribute ("name").Value;
nullable int از NullReferenceException برای Anne که credit ندارد جلوگیری میکند. راه دیگر، افزودن predicate وجود attribute است:
where cust.Attributes ("credit").Any() && (int) cust.Attribute...
همین اصول برای query روی element valueها نیز برقرار است.
Values و Mixed Content Nodeها
چرا اصلاً مستقیم با XText کار کنیم؟ وقتی mixed content داریم:
<summary>An XAttribute is <bold>not</bold> an XNode</summary>
یک property سادهٔ Value برای capture کردن content این summary کافی نیست؛ سه child دارد: XText، سپس XElement و سپس XText دیگر. ساخت:
XElement summary = new XElement ("summary",
new XText ("An XAttribute is "),
new XElement ("bold", "not"),
new XText (" an XNode")
);
با این حال query کردن summary.Value exception نمیدهد، بلکه value childها را به هم متصل میکند: An XAttribute is not an XNode. مقداردهی دوبارهٔ Value قانونی است، اما همهٔ childهای قبلی را با یک XText تازه جایگزین میکند.
Automatic XText Concatenation
وقتی simple content به XElement اضافه کنید، X-DOM آن را به XText موجود append میکند، نه اینکه XText جدید بسازد. e1 و e2 هر دو در پایان یک child XText با HelloWorld دارند:
var e1 = new XElement ("test", "Hello"); e1.Add ("World");
var e2 = new XElement ("test", "Hello", "World");
اما اگر XText را صریح بسازید، چند child حفظ میشود:
var e = new XElement ("test", new XText ("Hello"), new XText ("World"));
Console.WriteLine (e.Value); // HelloWorld
Console.WriteLine (e.Nodes().Count()); // 2
XElement این دو XText را concatenate نمیکند تا object identity nodeها حفظ شود.
Documents و Declarations
XDocument
XDocument یک root XElement را wrap میکند و اجازه میدهد XDeclaration، processing instruction، document type و commentهای سطح root افزوده شوند. XDocument اختیاری است و مانند W3C DOM glue اجباریِ کل ساختار نیست.
XDocument همان functional constructorهای XElement را دارد و چون از XContainer مشتق شده، AddXXX، RemoveXXX و ReplaceXXX را پشتیبانی میکند. اما content مجاز محدود است: یک root XElement، یک XDeclaration، یک XDocumentType برای DTD، هر تعداد XProcessingInstruction و هر تعداد XComment.
سادهترین XDocument معتبر:
var doc = new XDocument (
new XElement ("test", "data")
);
با اینکه XDeclaration نگذاشتهایم، doc.Save بهطور پیشفرض declaration تولید میکند.
مثال کاملتر XHTML با تمام سازههای قابلقبول XDocument:
var styleInstruction = new XProcessingInstruction (
"xml-stylesheet", "href='styles.css' type='text/css'");
var docType = new XDocumentType ("html",
"-//W3C//DTD XHTML 1.0 Strict//EN",
"[شناسهٔ DTD استاندارد XHTML]", null);
XNamespace ns = "[namespace استاندارد XHTML]";
var root =
new XElement (ns + "html",
new XElement (ns + "head",
new XElement (ns + "title", "An XHTML page")),
new XElement (ns + "body",
new XElement (ns + "p", "This is the content"))
);
var doc =
new XDocument (
new XDeclaration ("1.0", "utf-8", "no"),
new XComment ("Reference a stylesheet"),
styleInstruction,
docType,
root);
doc.Save ("test.html");
فایل حاصل با declaration، comment، processing instruction و DOCTYPE آغاز میشود و سپس XHTML شامل head/title و body/p قرار میگیرد. نشانیهای namespace/DTD موجود در منبع برای رعایت قاعدهٔ عدم لینک خارجی بهصورت متن توصیفی غیرقابل کلیک نشان داده شدهاند.
XDocument.Root میانبری برای دسترسی به تنها XElement سند است و لینک برگشتی با XObject.Document روی همهٔ objectهای درخت وجود دارد:
Console.WriteLine (doc.Root.Name.LocalName); // html
XElement bodyNode = doc.Root.Element (ns + "body");
Console.WriteLine (bodyNode.Document == doc); // True
childهای document هیچ Parent ندارند:
Console.WriteLine (doc.Root.Parent == null); // True
foreach (XNode node in doc.Nodes())
Console.Write (node.Parent == null); // TrueTrueTrueTrue
XML Declarations
یک فایل XML استاندارد میتواند با declaration مثل <?xml version="1.0" encoding="utf-8" standalone="yes"?> شروع شود. XElement/XDocument این قواعد را رعایت میکنند:
Save با filename همیشه declaration مینویسد؛Save با XmlWriter declaration مینویسد مگر XmlWriter خلافش تنظیم شده باشد؛ToString هرگز declaration نمینویسد.
وجود یا عدم وجود object از نوع XDeclaration روی اصل نوشتهشدن declaration اثری ندارد؛ XDeclaration فقط serialization را از دو جهت hint میکند: encoding متن و مقادیری که در attributeهای encoding و standalone declaration نوشته میشوند. constructor سه آرگومان version، encoding و standalone میگیرد.
var doc = new XDocument (
new XDeclaration ("1.0", "utf-16", "yes"),
new XElement ("test", "data")
);
doc.Save ("test.xml");
نوشتن Declaration در String
اگر XDocument را با declaration در یک string serialize کنیم، چون ToString declaration نمینویسد باید از XmlWriter استفاده کنیم:
var doc = new XDocument (
new XDeclaration ("1.0", "utf-8", "yes"),
new XElement ("test", "data")
);
var output = new StringBuilder();
var settings = new XmlWriterSettings { Indent = true };
using (XmlWriter xw = XmlWriter.Create (output, settings))
doc.Save (xw);
Console.WriteLine (output.ToString());
نتیجه declaration با encoding="utf-16" است، با اینکه در XDeclaration از UTF-8 خواسته بودیم. این bug نیست: چون مقصد string است و string درون .NET به UTF-16 ذخیره میشود، encoding دیگری قابل اعمال نیست؛ XmlWriter برای دروغ نگفتن utf-16 مینویسد.
همین موضوع توضیح میدهد چرا ToString declaration نمیدهد. اگر بنویسیم:
File.WriteAllText ("data.xml", doc.ToString());
فایل declaration ندارد، اما همچنان parsable است. اگر ToString declaration با UTF-16 میداد، WriteAllText که فایل را UTF-8 مینویسد declaration اشتباه تولید میکرد و شاید خواندن فایل را مختل میکرد.
Names و Namespaces
همانطور که typeهای .NET namespace دارند، element و attributeهای XML هم میتوانند namespace داشته باشند. XML namespace دو هدف دارد: جلوگیری از برخورد نامها هنگام ترکیب دادههای XML، و دادن معنای مطلق به نام. مثلاً nil بهتنهایی میتواند هر معنایی داشته باشد، اما در namespace استاندارد XML Schema Instance معنایی متناظر با null دارد و قواعد مشخصی برای کاربردش وجود دارد.
Namespaces در XML
فرض کنید element customer را در namespace OReilly.Nutshell.CSharp میخواهیم. راه اول استفاده از attribute رزروشدهٔ xmlns است:
<customer xmlns="OReilly.Nutshell.CSharp"/>
xmlns هم namespace element جاری و هم default namespace تمام descendant elementها را تعیین میکند:
<customer xmlns="OReilly.Nutshell.CSharp">
<address>
<postcode>02138</postcode>
</address>
</customer>
در این مثال address و postcode بهطور ضمنی در همان namespace هستند.
اگر address و postcode بدون namespace باشند:
<customer xmlns="OReilly.Nutshell.CSharp">
<address xmlns="">
<postcode>02138</postcode>
</address>
</customer>
Prefixها
راه دیگر namespace، prefix است؛ aliasی برای کاهش تایپ. تعریف و استفاده را میتوان همزمان انجام داد:
<nut:customer xmlns:nut="OReilly.Nutshell.CSharp"/>
سمت راست prefix nut را تعریف و برای element و descendantها در دسترس میکند؛ سمت چپ prefix را به customer اختصاص میدهد. element prefixed برخلاف default namespace، namespace پیشفرض descendantها را تعیین نمیکند:
<nut:customer xmlns:nut="OReilly.Nutshell.CSharp">
<firstname>Joe</firstname>
</nut:customer>
برای namespace دادن به firstname:
<nut:customer xmlns:nut="OReilly.Nutshell.CSharp">
<nut:firstname>Joe</nut:firstname>
</nut:customer>
میتوان prefixها را فقط برای convenience descendantها تعریف کرد بدون اینکه parent namespace بگیرد. مثال کتاب دو prefix استاندارد schema/serialization را روی customer تعریف میکند. اگر root باشد، کل سند آن prefixها را در دسترس دارد. Prefix وقتی elementها از چند namespace استفاده میکنند مفید است. استفاده از URIهای تحت مالکیت شما برای uniqueness namespace روش رایج است؛ نشانیهای نمونهٔ منبع در این خروجی بهصورت متن غیرقابل کلیک در نظر گرفته شدهاند.
Attributeها
میتوان به attribute هم namespace داد؛ تفاوت اصلی این است که attribute همیشه prefix میخواهد:
<customer xmlns:nut="OReilly.Nutshell.CSharp" nut:id="123" />
attribute بدون qualifier همیشه namespace خالی دارد و default namespace parent را به ارث نمیبرد. attributeها معمولاً namespace لازم ندارند چون معنا غالباً محلی به element است؛ استثنا metadata عمومی مانند xsi:nil است:
<customer xmlns:xsi="[XML Schema Instance namespace]">
<firstname>Joe</firstname>
<lastname xsi:nil="true"/>
</customer>
این بهطور شفاف میگوید lastname مقدار nil/null دارد، نه رشتهٔ خالی.
تعیین Namespace در X-DOM
تا اینجا نامهای XElement/XAttribute را string ساده داده بودیم؛ string ساده یعنی نام XML در namespace خالی. راه اول برای namespace، نوشتن آن داخل brace پیش از local name است:
var e = new XElement ("{[namespace]}customer", "Bloggs");
Console.WriteLine (e.ToString());
راه دوم و کارآمدتر XNamespace و XName است:
public sealed class XNamespace
{
public string NamespaceName { get; }
}
public sealed class XName
{
public string LocalName { get; }
public XNamespace Namespace { get; }
}
هر دو implicit cast از string دارند.
XNamespace ns = "[namespace]";
XName localName = "customer";
XName fullName = "{[namespace]}customer";
XNamespace اپراتور + را overload میکند تا namespace و name بدون brace به XName ترکیب شوند:
XNamespace ns = "[namespace]";
XName fullName = ns + "customer";
Console.WriteLine (fullName); // {[namespace]}customer
تمام constructorها و methodهایی که در X-DOM نام element/attribute میگیرند در واقع XName میپذیرند؛ string به خاطر implicit cast کار میکند. namespace برای element و attribute به یک شکل تعیین میشود:
XNamespace ns = "[namespace]";
var data = new XElement (ns + "data",
new XAttribute (ns + "id", 123)
);
X-DOM و Default Namespace
X-DOM مفهوم default namespace را تا زمان output کردن XML نادیده میگیرد؛ هنگام ساخت child XElement باید namespace را صریح بدهید و از parent ارث نمیگیرد:
XNamespace ns = "[namespace]";
var data = new XElement (ns + "data",
new XElement (ns + "customer", "Bloggs"),
new XElement (ns + "purchase", "Bicycle")
);
با این حال هنگام read/output، default namespace درست اعمال میشود و XML بهشکل root با xmlns و childهای بدون تکرار namespace serialize میشود. data.Element(ns + "customer") نیز همان namespace را حفظ میکند.
اگر childها را بدون namespace بسازید، هنگام serialization روی آنها xmlns="" میآید تا namespace خالیشان حفظ شود.
var data = new XElement (ns + "data",
new XElement ("customer", "Bloggs"),
new XElement ("purchase", "Bicycle")
);
// <data xmlns="[namespace]">
// <customer xmlns="">Bloggs</customer>
// <purchase xmlns="">Bicycle</purchase>
// </data>
trap دیگر، حذف namespace هنگام navigation است:
XElement x = data.Element (ns + "customer"); // ok
XElement y = data.Element ("customer"); // null
اگر درخت را بدون namespace ساخته باشید، بعداً میتوان همهٔ elementهای بدون namespace را به یک namespace واحد منتقل کرد:
foreach (XElement e in data.DescendantsAndSelf())
if (e.Name.Namespace == "")
e.Name = ns + e.Name.LocalName;
Prefixها در X-DOM
X-DOM prefix را صرفاً یک قابلیت serialization میداند؛ در construction/query/update میتوان عملاً prefixها را نادیده گرفت و full name بهکار برد. prefix عمدتاً برای کاهش تکرار در output XML مفید است.
XNamespace ns1 = "[namespace-1]";
XNamespace ns2 = "[namespace-2]";
var mix = new XElement (ns1 + "data",
new XElement (ns2 + "element", "value"),
new XElement (ns2 + "element", "value"),
new XElement (ns2 + "element", "value")
);
بدون prefix، namespace دوم روی هر child تکرار میشود. راهحل این نیست که ساخت X-DOM را عوض کنید، بلکه پیش از serialization روی root attributeهای تعریف prefix اضافه کنید.
mix.SetAttributeValue (XNamespace.Xmlns + "ns1", ns1);
mix.SetAttributeValue (XNamespace.Xmlns + "ns2", ns2);
این کار prefixهای ns1 و ns2 را به namespaceها نسبت میدهد و X-DOM هنگام serialize برای فشردهتر شدن XML از آنها استفاده میکند:
<ns1:data xmlns:ns1="[namespace-1]"
xmlns:ns2="[namespace-2]">
<ns2:element>value</ns2:element>
<ns2:element>value</ns2:element>
<ns2:element>value</ns2:element>
</ns1:data>
prefixها نحوهٔ ساخت، query یا update X-DOM را تغییر نمیدهند؛ فقط هنگام تبدیل به/از file یا stream XML وارد عمل میشوند.
prefix برای attributeها نیز رعایت میشود. مثال ثبت date of birth و credit بهعنوان nil با attribute استاندارد W3C:
XNamespace xsi = "[XML Schema Instance namespace]";
var nil = new XAttribute (xsi + "nil", true);
var cust = new XElement ("customers",
new XAttribute (XNamespace.Xmlns + "xsi", xsi),
new XElement ("customer",
new XElement ("lastname", "Bloggs"),
new XElement ("dob", nil),
new XElement ("credit", nil)
)
);
XML خروجی prefix xsi را یکبار روی root تعریف و روی dob/credit بهصورت xsi:nil="true" استفاده میکند. attribute nil را یکبار ساختهایم اما دوبار استفاده کردهایم؛ X-DOM بهدلیل automatic duplication آن را بهدرستی duplicate میکند.
Annotations
میتوانید با annotation دادهٔ سفارشی به هر XObject متصل کنید. annotation برای مصرف خصوصی خودتان است و X-DOM آن را black box میداند. مفهوم شبیه property Tag در Windows Forms/WPF است، با این تفاوت که چند annotation و scope خصوصی ممکن است.
متدهای افزودن/حذف annotation:
public void AddAnnotation (object annotation)
public void RemoveAnnotations<T>() where T : class
بازیابی:
public T Annotation<T>() where T : class
public IEnumerable<T> Annotations<T>() where T : class
کلید هر annotation، type آن است و باید reference type باشد. افزودن/گرفتن string annotation:
XElement e = new XElement ("test");
e.AddAnnotation ("Hello");
Console.WriteLine (e.Annotation<string>()); // Hello
میتوان چند annotation همنوع افزود و با Annotations sequence همه را گرفت. اما type عمومی مثل string کلید خوبی نیست چون کد دیگر ممکن است دخالت کند. بهتر است class internal یا private nested بسازید:
class X
{
class CustomData { internal string Message; }
static void Test()
{
XElement e = new XElement ("test");
e.AddAnnotation (new CustomData { Message = "Hello" } );
Console.Write (e.Annotations<CustomData>().First().Message); // Hello
}
}
برای حذف نیز باید به type کلید دسترسی داشته باشید:
e.RemoveAnnotations<CustomData>();
Projecting به X-DOM
تا اینجا از LINQ برای بیرونکشیدن داده از X-DOM استفاده کردیم؛ LINQ میتواند به X-DOM نیز project کند. source میتواند EF Core entity، collection محلی یا X-DOM دیگری باشد.
راهبرد مستقل از source است: ابتدا functional construction expression برای شکل X-DOM مطلوب بنویسید و سپس LINQ query را دور آن بسازید. فرض کنید XML هدف این باشد:
<customers>
<customer id="1">
<name>Sue</name>
<buys>3</buys>
</customer>
...
</customers>
ابتدا functional construction با literal:
var customers =
new XElement ("customers",
new XElement ("customer", new XAttribute ("id", 1),
new XElement ("name", "Sue"),
new XElement ("buys", 3)
)
);
سپس آن را projection میکنیم:
var customers =
new XElement ("customers",
// We must call AsEnumerable() due to a bug in EF Core.
from c in dbContext.Customers.AsEnumerable()
select
new XElement ("customer", new XAttribute ("id", c.ID),
new XElement ("name", c.Name),
new XElement ("buys", c.Purchases.Count)
)
);
خروجی نمونه:
<customers>
<customer id="1"><name>Tom</name><buys>3</buys></customer>
<customer id="2"><name>Harry</name><buys>2</buys></customer>
...
</customers>
همین query را دو مرحلهای بهتر میتوان فهمید. مرحلهٔ اول یک LINQ query عادی است که به XElement project میکند:
IEnumerable<XElement> sqlQuery =
from c in dbContext.Customers.AsEnumerable()
select
new XElement ("customer", new XAttribute ("id", c.ID),
new XElement ("name", c.Name),
new XElement ("buys", c.Purchases.Count)
);
مرحلهٔ دوم root XElement را میسازد:
var customers = new XElement ("customers", sqlQuery);
نکتهٔ غیرعادی این است که content یک XElement منفرد نیست، بلکه sequence از XElementهاست. در پردازش XML content، collectionها خودکار enumerate میشوند و هر XElement بهعنوان child node افزوده میشود.
حذف Elementهای خالی
اگر بخواهیم جزئیات آخرین خرید باارزش مشتری را نیز اضافه کنیم:
var customers =
new XElement ("customers",
from c in dbContext.Customers.AsEnumerable()
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?.Description),
new XElement ("price", lastBigBuy?.Price ?? 0m)
)
)
);
برای مشتری بدون خرید باارزش، elementهای خالی تولید میشود (و اگر query محلی باشد ممکن است NullReferenceException رخ دهد). بهتر است کل node lastBigBuy حذف شود؛ constructor را در conditional operator قرار میدهیم.
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، null بهجای XElement خالی تولید میشود؛ و این مطلوب است چون null content خودکار نادیده گرفته میشود.
Streaming یک Projection
اگر فقط برای Save یا ToString به X-DOM project میکنید، با XStreamingElement میتوان مصرف حافظه را کاهش داد. این نوع نسخهٔ کوچکشدهٔ XElement است و برای child content، deferred loading دارد. کافی است outer XElementها را با XStreamingElement عوض کنید:
var customers =
new XStreamingElement ("customers",
from c in dbContext.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");
queryهای دادهشده به constructor تا زمان Save، ToString یا WriteTo enumerate نمیشوند، بنابراین کل X-DOM همزمان در حافظه لود نمیشود. نقطهٔ مقابل: با Save دوباره، queryها دوباره ارزیابی میشوند؛ و child content یک XStreamingElement قابل traversal نیست و متدهایی مانند Elements یا Attributes ندارد.
XStreamingElement بهدلیل مجموعهٔ بسیار محدود اعضا از XObject یا کلاس دیگری مشتق نیست. جز Save، ToString و WriteTo فقط Add و property Name را دارد.
XStreamingElement امکان خواندن streamشدهٔ content را نمیدهد؛ برای آن باید XmlReader را همراه X-DOM بهکار ببرید که در فصل ۱۱ توضیح داده میشود.