扩展 LINQ to XML






4.70/5 (6投票s)
一组 LINQ to XML 的扩展方法。
引言
Linq to XML 是一个很棒且简洁的 API。 然而,使用它一段时间后,我对它有一些担忧。 让我们考虑一下它们,然后看看我们可以做些什么来使这个 API 更好。
使用代码
作为示例 XML,我们将解析来自 Amazon 搜索响应的这段摘录
<Item>
<ASIN>059035342X</ASIN>
<SmallImage>
<URL>http://ecx.images-amazon.com/images/I/51MU5VilKpL._SL75_.jpg</URL>
<Height Units="pixels">75</Height>
<Width Units="pixels">51</Width>
</SmallImage>
<ItemAttributes>
<Author>J.K. Rowling</Author>
<EAN>9780590353427</EAN>
<ISBN>0439708184</ISBN>
<ListPrice>
<Amount>1099</Amount>
<CurrencyCode>USD</CurrencyCode>
<FormattedPrice>$10.99</FormattedPrice>
</ListPrice>
<PublicationDate>1999-10-01</PublicationDate>
<Title>Harry Potter and the
Sorcerer's Stone (Book 1)</Title>
</ItemAttributes>
</Item>
解析XML
如果我有一个包含上述 XML 的 XElement 变量,我想通过以下方式获取其属性
var title = item.Element("ItemAttributes").Element("Title").Value;
var imageUrl = new Uri(item.Element("SmallImage").Element("URL").Value);
var imageHeight = (int) item.Element("SmallImage").Element("Height");
var publicationDate = (DateTime) item.Element("ItemAttributes").Element("PublicationDate");
这里的问题是,此响应中的几乎所有节点都是可选的。 我可以假设某些节点(例如 Author 和 Title)始终会为任何书籍指定。 大多数其他节点实际上是可选的。 有些书可能没有图片,有些书缺货且没有价格,还有一些书仅可预订且尚未发布日期。 另一个问题是,转换为值类型可能不顺利,您会收到 FormatException,其中包含有关错误上下文的少量信息。
所以真实的防御性代码看起来像这样
var smallImageElement = item.Element("SmallImage");
if (smallImageElement != null)
{
var urlElement = smallImageElement.Element("URL");
if (urlElement != null && Uri.IsWellFormedUriString(urlElement.Value, UriKind.Absolute))
{
var imageUrl = new Uri(urlElement.Value);
}
}
这根本不好。 我们可以将其隐藏到辅助方法或属性中。 此 XML 很可能会隐藏到 DTO 类型中,该类型将负责 XML 解析,并且它将为每个值公开属性。 无论如何,我宁愿不写大量如此丑陋的代码。
我想编写这样的代码
- 可以轻松且安全地进行链式操作。
- 将元素和属性标记为强制或可选。 如果强制元素丢失,则抛出有意义的异常,但不要为丢失的可选节点抛出异常。
- 获取指定类型的值。 如果元素是可选的,请尝试获取值,如果值丢失,则使用一些默认值。
以下是使用多个扩展方法重写的代码
var title = item.MandatoryElement("ItemAttributes").MandatoryElement("Title").Value;
var imageUrl = item.ElementOrEmpty("SmallImage").ElementOrEmpty("URL").Value(value =>
Uri.IsWellFormedUriString(value, UriKind.Absolute) ? new Uri(value) : null);
var imageHeight = item.ElementOrEmpty("SmallImage").ElementOrEmpty("Height").Value<int>(0);
var publicationDate = item.MandatoryElement("ItemAttributes").ElementOrEmpty("PublicationDate").Value<DateTime>();
此代码不需要任何空值检查或捕获 NullReferenceException
但是,如果强制元素丢失,则 MandatoryElement 方法将抛出 XmlException,消息为“The element 'Item' doesn't contain mandatory child element 'ItemAttributes'.”。 这正是我们想要的——检测到我们期望存在的元素丢失了。 另一方面,ElementOrEmpty 方法永远不会抛出异常。 如果元素丢失(例如,Element 方法返回 null),它将创建并返回具有指定名称的空元素,以便可以继续链接(NullObject 模式)。
Value 方法也可能抛出 XmlException,例如:“The element 'Amount' has value 'Five' which cannot be converted to the value of type 'int'.”,其中包含更多的上下文信息(原始 FormatException 作为内部异常包含在内)。 错误处理的方式是所有异常都具有 XmlException 类型,并且包含必要的上下文信息,以便轻松定位和重现发生的问题。
让我们看看一些扩展方法的实现。
public static XElement MandatoryElement(this XElement element, XName name)
{
XElement childElement = element.Element(name);
if (childElement == null)
{
throw new XmlException(string.Format("The element '{0}' doesn't contain mandatory child element '{1}'.", element.Name, name));
}
return childElement;
}
MandatoryElement 检查 Element 方法的结果,如果返回 null 则抛出异常。 没什么特别的,它只是将空值处理从您的解析逻辑中分离出来。
ElementOrEmpty:
public static XElement ElementOrEmpty(this XElement element, XName name)
{
if (element != null)
{
XElement childElement = element.Element(name);
return childElement ?? new XElement(name);
}
return new XElement(name);
}
带有自定义转换函数的 Value<T> 方法:
public static T Value<T>(this XElement element, Func<string, T> convert)
{
if (element == null)
{
return default(T);
}
return convert(element.Value);
}
在这种情况下,不正确值的错误处理是 convert 方法的责任,如上所示。
最后一种方法是 Value<T> 方法,它解析许多预定义的值类型和枚举值:
public static T Value<T>(this XElement element, T defaultValue = default(T)) where T : struct, IConvertible
{
if (element == null || string.IsNullOrEmpty(element.Value))
{
return defaultValue;
}
string value = element.Value;
try
{
Type typeOfT = typeof(T);
if (typeOfT.IsEnum)
{
return (T)Enum.Parse(typeOfT, value, ignoreCase: true);
}
return (T)Convert.ChangeType(value, typeof(T));
}
catch (Exception ex)
{
throw new XmlException(string.Format("The element '{0}' has value '{1}' which cannot be converted to the value of type '{2}'.", element.Name, element.Value, typeof(T).Name), ex);
}
}
此方法允许解析 int、uint、byte、sbyte、short、ushort、char、long、ulong、float、double、decimal、bool 或 DateTime 值。 此外,它可以处理自定义枚举,包括标志枚举。 假设我们有 Color 枚举
[Flags]
private enum Colors
{
None = 0,
Red = 1,
Green = 2,
Blue = 4
}
有了这个,您可以使用 Value<T> 方法来反序列化枚举值
var enumElement = new XElement("color", Colors.Red | Colors.Green); // Value = "Red, Green"
Colors colors = enumElement.Value<Colors>();
生成 XML
让我们看看 XML 的生成:
var itemXml = new XElement("Item",
new XElement("ASIN", item.Asin),
new XElement("SmallImage",
new XElement("URL", item.SmallImage.Url),
new XElement("Height",
new XAttribute("Units", item.SmallImage.Units),
item.SmallImage.Height),
new XElement("Width",
new XAttribute("Units", item.SmallImage.Units),
item.SmallImage.Width)),
new XElement("ItemAttributes",
new XElement("Author", item.ItemAttributes.Author),
new XElement("Binding", item.ItemAttributes.Binding)));
如果始终需要输出所有元素和属性,则此代码看起来不错。 当您有包含可选结构的层次结构数据(例如此示例中的 item.SmallImage,它可能为 null)和/或您不想输出空节点时,问题就开始了。 为了处理这些问题,我们将使用几个静态方法。
itemXml = new XElement("Item",
new XElement("ASIN", item.Asin),
XmlExtensions.NewOptionalXElements("SmallImage", () => item.SmallImage != null,
new XElement("URL", item.SmallImage.Url),
new XElement("Height",
new XAttribute("Units", item.SmallImage.Units),
item.SmallImage.Height),
new XElement("Width",
new XAttribute("Units", item.SmallImage.Units),
item.SmallImage.Width)),
new XElement("ItemAttributes",
new XElement("Author", item.ItemAttributes.Author),
XmlExtensions.NewOptionalXElement("Binding", item.ItemAttributes.Binding)));
NewOptionalXElement 测试值是否为 null,如果它为null,则根本不输出节点:
public static XElement NewOptionalXElement(XName name, object value)
{
if (value != null)
{
return new XElement(name, value);
}
return null;
}
NewOptionalElements 接受一个函数,该函数测试是否必须将其他内容添加到输出中:
public static XElement NewOptionalXElements(XName name, Func<bool> condition, params object[] value)
{
if (condition())
{
return new XElement(name, value);
}
return null;
}
它允许向节点组添加一些条件。
其他有用的功能
附加的项目还有几个您可能会发现有用的扩展方法。
XAttribute MandatoryAttribute(this XElement element, XName name);
XAttribute AttributeOrEmpty(this XElement element, XName name, string defaultValue = null);
T Value<T>(this XAttribute attribute, T defaultValue = default(T));
T Value<T>(this XAttribute attribute, Func<string, T> convert);
void AssertName(XElement element, XName expectedName);
XAttribute NewOptionalXAttribute(XName name, object value, object defaultValue = null);
正如你所看到的,通过稍微调整一下,LINQ to XML API 变得更好了。 随意在您的项目中使用这些扩展。
祝您编码愉快!