LINQ to XML
DOM是什么?
如下是一个xml文件:
<?xml version="1.0"?>
johnyang
30
kehua
1234
就像其他所有的XML文件,开始有一个声明,然后根元素,根元素有两个属性,还有子元素。
所有的这些--声明,元素,属性,值,文字等都可以用一个类来表示。如果这样的类有很多属性来储存子级内容,我们就可以组装树状的一组数据来描述一个文档,即文档对象模型(Document Object Model---DOM)。
LINQ to XML
包含两个东西:
- XML DOM,即称X-DOM
- 大概10多个附加的询问操作符
X-DOM包含了XDocument,XElement,XAttribute,而X-DOM类型并没与LINQ绑定死,可以不用LINQ而完成X-DOM的装载,实例化,更新,保存等。
X-DOM是LINQ友好的,也就是说:它有可以返回IEnumerable序列的方法;可以通过LINQ的Projection来建立一个X-DOM。
X-DOM概述

猛一看上面的架构图,有点懵,但主要反映的是几点:XObject是所有XML对象的抽象类,从图上也可以看到,它定义了Parent的属性,和Document属性。而它的子类有XAttribute和XNode,这两个其实具有不同的行为,即在一个元素下的多个特性都是同级的,也只能特性与特性是同级的,而在一个元素可以包含很多不同类型的同级的节点。再往下,XContainer作为XElement,XDocuement的基类,与字面意义一样,它们都是容器类的对象,即都可以包裹其他节点,元素,特性等。而非容器的节点不具备这样的包裹能力。
最常用的类型是XElement,XObject是继承类的根部,XElement和XDocument是容器类的根部。
XObject是所有XML内容的抽象类,XNode是除了属性外的绝大多数XML内容的基类,其特点是它可以以一定的顺序来排列混合类型的XNode。
例如:
Hello world
在父元素中,第一个节点是XText,然后是XElement节点,再然后是XComment节点,最后是第二个XElement,与此相反的是,XAttribute没有这样的嵌套关系。
尽管XNode可以访问其父元素,但它没有子节点的概念,子节点是XContainer的子类所属的概念,XContainer是XElement和XDocument的基类。
XElement有Name和Value属性,在相当多的情况下,XElement只有一个XText子节点,在这种情况下,Value就可以实现XElement对子内容的设置和获取,避免了不必要的索引,避免了再与XText发生直接关系。而其他节点并无这两个属性。
XDocument代表是XML树的树根,它包裹了XElement,再加上XDeclaration。
创建X-DOM的两个方法:Loading 和Parsing
作为XContainer的两个子类:XElement和XDocument提供了两个静态方法:Load和Parse来从以下资源创建X-DOM:
- Load从文件,URL,Stream,TextReader,XmlReader来创建X-DOM
- Parse从字符串创建
X-DOM
比如:
public static void Main()
{
var config = XElement.Parse(
@"
30
"
);
foreach(var child in config.Elements())
{
Console.WriteLine(child.Name);
}
var client = config.Element("client");
bool enabled = (bool)client.Attribute("enabled");
Console.WriteLine(enabled);
client.Attribute("enabled").SetValue(!enabled);
int timeout = (int)client.Element("timeout");
Console.WriteLine(timeout);
client.Element("timeout").SetValue(timeout * 2);
client.Add(new XElement("retries", 2));
Console.WriteLine(client);
}
output:
client
True
30
60
2
保存与序列化
对任何节点调用ToString都将它的内容转变为XML字符串,有缩进格式(如果不想要缩进格式,在调用ToString时候,加上SaveOptions.DisableFormatting)
XElement和XDocument也提供了Save方法,因此可以将X-DOM写入到文件,Stream,TextWriter,XmlWriter中。如果是文件,那么XML声明将被自动的写入进去。
在XNode类中,还有一个WriteTo方法,该方法只接受XmlWriter。
实例化X-DOM
不用Load,Parse,也可以它通过XContainer的Add方法手动的实例化X-DOM。
为了创建XElement,XAttribute,仅仅需要提供name,value:
public static void Main()
{
var lastName = new XElement("lastname", "Bloggs");
lastName.Add(new XComment("nice name"));
var customer = new XElement("Customer");
customer.Add(new XAttribute("id", 123));
customer.Add(new XElement("firstname", "Joe"));
customer.Add(lastName);
Console.WriteLine(customer);
}
output
Joe
Bloggs
当创建XElement时候,Value是可选的,即可以先提供一个name,后面再赋值Value,比如上面例子中的customer,另外,注意到,简单的字符串就可以作为值赋予XElement,而不必显式地创建和添加XText子节点。
函数式地创建(Functional Construction)
在上一个例子,很难直接看出来XML的结构。XML支持另一种创建的模式:函数式创建。
public static void Main()
{
var customer = new XElement("customer",new XAttribute("id",123),
new XElement("firstname","Joe"),
new XElement("lastname","bloggs"),
new XComment("nice name")
);
Console.WriteLine(customer);
}
output:
Joe
bloggs
这有两个好处:其一,可以直接看出来XML的结构,其二,可以从LINQ直接投射为X-DOM
比如:
public class Person { public int ID; public string FirstName; public string LastName; } class Program { public static void Main() { var names = new List { new Person(){ID=123,FirstName="John",LastName="Yang"}, new Person(){ID=124,FirstName="Tom",LastName="Cruse"}, new Person(){ID=125,FirstName="Daviad",LastName="Trump"} }; var query = new XElement("customers", from c in names select new XElement("customer",new XAttribute("id",c.ID), new XElement("firstname",c.FirstName), new XElement("lastname",c.LastName), new XComment("nice name") ) ); Console.WriteLine(query); } }
output:
John Yang Tom Cruse Daviad Trump
自动深度复制
当一个节点或者特性被添加到一个元素时(不论是通过函数式方法,或者Add方法),该节点或特性的Parent属性就被赋值为那个元素,而一个节点只能有一个父元素,如果将一个已有父元素的节点添加到第二个父元素下,那么该节点将自动深度复制。
public static void Main() { var address = new XElement("Address", new XElement("street","TianFuSiJie"), new XElement("town","WenXin") ); var customer1 = new XElement("customer1", address); var customer2 = new XElement("customer2", address); customer1.Element("Address").Element("street").Value = "RenMinNanLu"; Console.WriteLine(customer2); }
output:
TianFuSiJie WenXin
可以发现变更customer2的street,并不会影响customer1的street,这正是因为自动深度复制的缘故。
Navigating and Querying
XNode和XContainer都定义了能够游历X-DOM树的方法,属性,这些方法,属性要么返回一个值,要么返回实现IEnumerable的序列。
子节点游历
FirstNode,LastNode,Nodes
public static void Main()
{
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("poweetool","Nailgun"))
);
foreach(var node in bench.Nodes())//bench下的所有节点
{
Console.WriteLine(node.ToString());
}
}
output:
Hammer
Rasp
Saw
Nailgun
得到elements
Elements方法返回XElement的所有子节点,
public static void Main()
{
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 XAttribute("test",123)
);
foreach (var c in bench.Elements())
Console.WriteLine(c.Name + "=" + c.Value);
var a = from tool in bench.Elements("toolbox").Elements("handtool")
select tool.Value.ToUpper();
var b = a.ToList();
foreach (var ele in a)
Console.WriteLine(ele);
}
output:
toolbox=HammerRasp
toolbox=SawNailgun
HAMMER
RASP
SAW
var a = from tool in bench.Elements("toolbox").Elements("handtool") select tool.Value.ToUpper();第一个Elements方法是XElement的方法,返回IEnumerable类型,第二个Elements方法是扩展方法,正好可被IEnumerable类调用,返回的仍然是IEnumerable类型。
得到单个Element
Element方法返回与给定名字一致的首个元素,相当于先调用Elements(),然后用LINQ的FirstOrDefault询问方法,如果找不到,则返回null,来表明没找到。
public static void Main()
{
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 XAttribute("test",123)
);
var tlb = bench.Element("toolbox");
Console.WriteLine(tlb.Element("handtool").Value);
var jhn = bench.Element("johnYang");
Console.WriteLine(jhn == null);
}
output:
Hammer
True
得到Descendants
XContainer提供了Descendants和DescendantNodes方法,前者返回所有后代元素(XElement),后者返回所有后代节点,即不包含所有XAttribute的对象。
public static void Main()
{
var bench = new XElement("bench",
new XElement("toolbox", //子后代元素1,子后代节点1
new XElement("handtool", "Hammer"), //子后代元素2,子后代节点2,3(字符串被算为XText)
new XElement("handtool", "Rasp"),//子后代元素3,子后代节点4,5
new XComment("testcomment")//子后代节点6
),
new XElement("toolbox",//子后代元素4 ,子后代节点7
new XElement("handtool", "Saw"),//子后代元素5 ,子后代节点8,9
new XElement("powertool", "Nailgun")),//子后代元素6,子后代10,11
new XAttribute("test",123)
);
Console.WriteLine(bench.Descendants().Count());
foreach (var des in bench.Descendants())
Console.WriteLine(des.Name);
Console.WriteLine(bench.DescendantNodes().Count());
foreach (var node in bench.DescendantNodes())
Console.WriteLine(node);
}
output
6
toolbox
handtool
handtool
toolbox
handtool
powertool
11
Hammer
Rasp
Hammer
Hammer
Rasp
Rasp
Saw
Nailgun
Saw
Saw
Nailgun
Nailgun
上述打印并无XAttribute。
下方代码抽取了所有XComment:
public static void Main()
{
var bench = new XElement("bench",
new XElement("toolbox",
new XElement("handtool", "Hammer"),
new XElement("handtool", "Rasp"),
new XComment("testcomment")
),
new XElement("toolbox",
new XElement("handtool", "Saw"),
new XElement("powertool", "Nailgun")),
new XComment("nice name"),
new XAttribute("test",123)
);
Console.WriteLine(bench);
var commentQuery = from c in bench.DescendantNodes().OfType()
select c;
Console.WriteLine("-------------------------------------");
foreach (var c in commentQuery)
Console.WriteLine(c);
}
output:
Hammer Rasp Saw Nailgun -------------------------------------
LINQ中的OfType在筛选特定类型时候,起了作用,与Cast不同的是,当类型转换失败后,OfType选择忽略,而Cast选择报错。
例如:
static void Main(string[] args)
{
var a=new List { new DbPt(0,90),new DbLine(new DbPt(0,0),new DbPt(100,100)),new DbPt(9,9)};
var b=from c in a.OfType()
select c;
foreach(var c in b)
Console.WriteLine(c);
}
其中,DbPt和DbLine的基类均为Db。
output:
(0,90,0)
(9,9,0)
Parent Navigation
所有节点都有Parent属性,和AncestorXXX方法,用来游历父元素。
如果x是XElement下面的打印永远是true:
foreach(var child in x.Nodes)
{
Console.WriteLine(child.Parent==x);
}
如果x是XDocument就不对了,因为XDocument有点奇怪,它可以有子元素,却不是任何子元素的父元素。为了访问到XDocument,可以使用Document属性,而这是X-DOM中任何对象都有的属性,因为XObject就定义了它。
Ancestors返回一个序列,即它的父元素,父元素的父元素,等等,直到根部元素。
public static void Main()
{
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("poweetool","Nailgun"))
);
var a = bench.Element("toolbox").Element("handtool");
var b = a.Ancestors();
foreach (var c in b)
Console.WriteLine(c.Name);
}
output:
toolbox
bench
可以通过AncestorsAndSelf().Last()获取根元素,另一种方法是Document.Root,但前提是XDocument必须存在。
Navigating Peer Node,Update X-DOM
public static void Main()
{
var bench = new XElement("bench",
new XElement("toolbox",
new XElement("handtool","Hammer"),
new XElement("handtool","Rasp"),
new XComment("nice name")
),
new XElement("toolbox",
new XElement("handtool","Saw",new XAttribute("Test","Yang")),
new XElement("poweetool","Nailgun"),
new XAttribute("pd","John")
),
new XAttribute("id",123)
);
Console.WriteLine("显示bench");
Console.WriteLine(bench);
Console.WriteLine("bench后代节点数量:");
Console.WriteLine(bench.DescendantNodes().Count());
var a = bench.Elements().Elements().Last(); //最后一个元素
Console.WriteLine("bench最后一个元素");
Console.WriteLine(a);
var b = a.PreviousNode;//最后一个元素的前一个节点,即Saw元素
Console.WriteLine("倒数第二个元素b");
Console.WriteLine(b);
Console.WriteLine("b是否在a之前");
Console.WriteLine(b.IsBefore(a));
var c = b.ElementsBeforeSelf();
Console.WriteLine("在b之前的元素的个数");
Console.WriteLine(c.Count());
Console.WriteLine("b是否有特性");
Console.WriteLine(((XElement)b).HasAttributes);//是否有特性
var bb = (XElement)b;
bb.SetValue("Not Only Saw But Also Do it");
Console.WriteLine("b修改value值后");
Console.WriteLine(b);
Console.WriteLine("b添加子节点timeout");
bb.SetElementValue("timeout", 30);
Console.WriteLine(bb);
Console.WriteLine("b更新子节点timeout");
bb.SetElementValue("timeout", 60);
Console.WriteLine(bb);
bb.ReplaceWith(new XComment("replaced"));
Console.WriteLine(bb);
var cc = bb.Elements();
Console.WriteLine("去除b元素的子元素");
cc.Remove();//只能是nodes或者attributes的序列调用Remove
Console.WriteLine(bb);
Console.WriteLine("bench去除所有XComment");
bench.DescendantNodes().OfType().Remove();
Console.WriteLine(bench);
}
output
显示bench
Hammer
Rasp
Saw
Nailgun
bench后代节点数量:
11
bench最后一个元素
Nailgun
倒数第二个元素b
Saw
b是否在a之前
True
在b之前的元素的个数
0
b是否有特性
True
b修改value值后
Not Only Saw But Also Do it
b添加子节点timeout
Not Only Saw But Also Do it30
b更新子节点timeout
Not Only Saw But Also Do it60
Not Only Saw But Also Do it60
去除b元素的子元素
Not Only Saw But Also Do it
bench去除所有XComment
Hammer
Rasp
Nailgun
Values
XElement和XAttribute都有Value属性,该属性为string类型。如果一个元素只有一个单独的XText子节点,XElement的Value属性就直接是获取该XText子节点的内容。
设定值
有两种方法来赋值:(1)调用SetValue(2)直接对Value属性进行赋值。
SetValue更灵活,因为它不仅接受字符串,而且还可以接受简单的数据类型,比如:
public static void Main()
{
var e = new XElement("data", DateTime.Now);
e.SetValue(DateTime.Now.AddDays(1));
Console.WriteLine(e);
}
output:
2022-01-15T23:02:51.5792172+08:00
而直接对Value进行赋值,则必须先将Datetime转换为字符串,才可以赋值。
得到值
可仅仅将XElement转换为所需要的类型,就可以获取值。
public static void Main()
{
var e = new XElement("date", DateTime.Now);
string b = e.Value;
var d = new XAttribute("resolution", 1.234);
var c = (DateTime)e;//获取值,只需要直接转换类型即可
var f = (double)d;
Console.WriteLine(f);
Console.WriteLine(c);
}
output:
1.234
2022/1/14 23:10:22
元素和特性并没有储存DateTimes或者数字,DateTimes或数字是被储存为文字,它也不记得之前的类型,所以就需要正确的转换类型,来防止运行时错误。考虑到健壮性,可以加一层try/catch来捕捉FormatException异常。
XElement和XAttribute可以直接转换为的类型,如下:
- 所有标准数值类型
- string,bool,DateTime,DateTimeOffset,TimeSpan,Guid
- Nullable<>类型
结合XElement,Attribute方法,转换为可空类型是非常有用的,也因为如果转换的元素不存在,整个转换过程仍然工作。
public static void Main()
{
var e = new XElement("date", DateTime.Now);
int? timeout = (int?)e.Element("timeout");
Console.WriteLine(timeout == null);
}
output
true
还可以用空接符
public static void Main()
{
var e = new XElement("date", DateTime.Now);
int? timeout = (int?)e.Element("timeout")?? 100;
Console.WriteLine(timeout);
}
output
100
public static void Main()
{
int? a = null;
Console.WriteLine(a > 10);
}
output
false
public static void Main()
{
var summary = new XElement("summary",
new XText("An XAttribute is "),
new XElement("bold","not"),
new XText(" an XNode"));
var text = (string)summary;
var text0 = summary.Value;
Console.WriteLine(text);
Console.WriteLine(text0);
}
output:
An XAttribute is not an XNode
An XAttribute is not an XNode
XText自动联结
当给XElement添加内容时,X-DOM自动的在现有的XText之后添加内容,而不是覆盖它。
public static void Main()
{
var e1 = new XElement("test", "Hello");e1.Add("world");
var e2 = new XElement("test", "Hello", "world");
Console.WriteLine(e1);
Console.WriteLine(e2);
}
output:
Helloworld
Helloworld