使用 ExpandoObject 从 XML 创建动态对象






4.67/5 (8投票s)
使用 ExpandoObject 从 XML 创建动态对象。
介绍
解析 XML 可能是一项繁琐、耗费人力的事情。尝试将 XML 封装到数据对象中可以改进这个过程。根据具体问题,创建和维护数据对象本身也可能很麻烦,这取决于 XML 的结构以及 XML 更改的频率。
背景
在我最近的一个项目中,第三方生成 XML 文件以导入我正在编写的应用程序。每个 XML 文件包含一批交易。每笔交易由一个表单和支持文档组成。每个文件可以包含任意数量的表单,并且表单有不同的类型(每种表单类型都有不同的元素)。每个表单可以包含任意数量的支持文档,并且文档有不同的类型(每种文档类型都有不同的元素)。在解析时,直到深入了解 XML 后,你才能真正知道需要创建对应元素数据对象的哪些信息。到那时,为什么还要费力创建数据对象呢?我可以直接开始处理原始 XML。一定有更好的方法。
在寻找更好的方法时,我发现了 .NET 4.0 的 System.Dynamic.ExpandoObject
类。ExpandoObject
实例能够在运行时添加和删除成员。经过进一步研究,我发现了两个将 ExpandoObject
用于表示 XML 文档的示例。也就是说,在解析时,会创建一个 ExpandoObject
实例,并将每个 XML 元素添加为 ExpandoObject
的成员。例如,这个 XML
<car>
<make>Ford</make>
<model>Explorer</model>
<color>Silver</color>
</car>
...基本上会变成以下动态“虚拟”类的实例
public class Car
{
public string make;
public string model;
public string color;
}
我发现了两个构建 ExpandoObject
从 XML 的示例。两者都很接近,但没有一个完全符合我的要求。两者都对要解析的 XML 进行了假设。一个示例假设可以向 XML 标记添加属性来改变创建 ExpandoObject
的行为。我是 XML 的使用者。我无法控制发布者提供的内容。另一个示例假设列表节点始终位于非列表节点之前。而我的源 XML 并非如此。总之,这两个潜在的解决方案都对正在读取的 XML 进行了假设,这些假设在我的问题空间中是错误的。我需要一个通用且更健壮的解决方案。
从最适合的开始
在我找到的两个将 XML 转换为 ExpandoObject
实例的示例中,最接近我需求的那个发布在 ITDevSpace.com 上。我毫不讳言我的解决方案源于使用这段代码。事实上,仔细查看两者,只有少数区别。我将其中 90% 的功劳归于他们,因为对我来说,这是一个 90% 的解决方案。这篇文章描述了我的 10%。
要求 1
原始类在处理对象列表方面做得很好。但是,它假设列表是 XML 中的第一个子元素。例如,根据 ITDevSpace.com 的解决方案,此 XML 是“好的”
<car>
<owners>
<owner>Bob Jones</owner>
<owner>Betty Jones</owner>
</owners>
<make>Ford</make>
<model>Explorer</model>
<color>Silver</color>
</car>
此 XML 是“坏的”
<car>
<make>Ford</make>
<model>Explorer</model>
<color>Silver</color>
<owners>
<owner>Bob Jones</owner>
<owner>Betty Jones</owner>
</owners>
</car>
这很不幸,因为它们在语法上是相同的 XML。我的 XML 看起来像“坏”的 XML,所以那行不通。我需要一种方法来通用地处理列表,无论它们在 XML 中的位置如何。
要求 2
虽然原始类在处理列表方面做得很好,但如果列表只包含一项,就会出现问题。例如,如果解析上面的“好” XML,原始代码会生成此类
public class Car
{
public string make;
public string model;
public string color;
public dynamic owners; // Containing a List<object> member with the <owner> elements
}
但是,如果只有一个所有者。也就是说,此 XML
<car>
<make>Ford</make>
<model>Explorer</model>
<color>Silver</color>
<owners>
<owner>Bob Jones</owner>
</owners>
</car>
会生成此类
public class Car
{
public string make;
public string model;
public string color;
public dynamic owner; // Containing a dynamic member of a single <owner> element
}
与上面“好” XML 的输出相比,在使用 Car.owners.owner
成员时,我不知道我是在查看 List<>
还是单个对象,除非通过反射进行检查。我想要(而不是“需要”)一个标准的接口来处理我知道是列表的项。
要求 3
属性!属性怎么样?原始类在处理中间节点时,并不考虑它们的属性。例如,此 XML
<car>
<make>Ford</make>
<model>Explorer</model>
<color>Silver</color>
<owners type=”Current”>
<owner>Bob Jones</owner>
</owners>
</car>
...会丢失 type 属性中包含的数据。我需要所有属性!我需要类似这样的结果
public class Car
{
public string make;
public string model;
public string color;
public dynamic owners;
}
public class owners
{
public string type;
public <List>dynamic owner; // A representation of dynamic owners in the Car class
};
代码
这是最新版本。此类也包含在本文提供的示例项目中供下载。
public static class ExpandoObjectHelper
{
private static List<string> KnownLists;
public static void Parse(dynamic parent, XElement node, List<string> knownLists = null)
{
if (knownLists != null)
{
KnownLists = knownLists;
}
IEnumerable<xelement> sorted = from XElement elt in node.Elements()
orderby node.Elements(elt.Name.LocalName).Count() descending select elt;
if (node.HasElements)
{
int nodeCount = node.Elements(sorted.First().Name.LocalName).Count();
bool foundNode = false;
if (KnownLists != null && KnownLists.Count > 0)
{
foundNode = (from XElement el in node.Elements()
where KnownLists.Contains(el.Name.LocalName) select el).Count() > 0;
}
if (nodeCount>1 || foundNode==true)
{
// At least one of the child elements is a list
var item = new ExpandoObject();
List<dynamic> list = null;
string elementName = string.Empty;
foreach (var element in sorted)
{
if (element.Name.LocalName != elementName)
{
list = new List<dynamic>;
elementName = elementName.LocalName;
}
if (element.HasElements ||
(KnownLists != null && KnownLists.Contains(element.Name.LocalName)))
{
Parse(list, element);
AddProperty(item, element.Name.LocalName, list);
}
else
{
Parse(item, element);
}
}
foreach (var attribute in node.Attributes())
{
AddProperty(item, attribute.Name.ToString(), attribute.Value.Trim());
}
AddProperty(parent, node.Name.ToString(), item);
}
else
{
var item = new ExpandoObject();
foreach (var attribute in node.Attributes())
{
AddProperty(item, attribute.Name.ToString(), attribute.Value.Trim());
}
//element
foreach (var element in sorted)
{
Parse(item, element);
}
AddProperty(parent, node.Name.ToString(), item);
}
}
else
{
AddProperty(parent, node.Name.ToString(), node.Value.Trim());
}
}
private static void AddProperty(dynamic parent, string name, object value)
{
if (parent is List<dynamic>)
{
(parent as List<dynamic>).Add(value);
}
else
{
(parent as IDictionary<string, object>)[name] = value;
}
}
}
用法
以下代码部分直接摘自本文提供的示例项目供下载。第一行代码从磁盘加载 XML 文件。下一部分创建已知包含项目列表的 XML 节点名称列表。一旦 XML 加载完毕并准备好节点列表,就可以通过调用 ExpandoObjectHelper.Parse
来开始创建 dynamic
对象。解析完成后,就可以使用已解析的数据了。
// Load an XML document.
var xmlDocument = XDocument.Load("test.xml");
// Convert the XML document in to a dynamic C# object.
List<string> listNodes = new List<string>() {"owners"};
dynamic xmlContent = new ExpandoObject();
ExpandoObjectHelper.Parse(xmlContent, xmlDocument.Root, listNodes);
Console.WriteLine("Make: {0}", xmlContent.car.make);
已知问题
万事皆有缺点,对吧?我注意到,与我之前的 XML 解析方法相比,使用 ExpandoObject
速度很慢。我没有进行基准测试来知道慢多少,但这是显而易见的。我知道我的原始解析方法使用 XPath 是造成部分差异的原因。在我意识到数据文件都不同之前,我能够精确地定位我想要的节点。创建 ExpandoObject
时,代码必须遍历整个文件——每个元素和每个属性。如果文件很大,这可能需要一些时间。由于我的项目是一个计划在夜间运行且无人值守的作业,我不认为这对我来说是个问题。你的体验可能会有所不同。
参考
历史
2012 年 10 月 25 日
- 错误修复。发现了一个在同一节点级别上存在多个对象列表的问题(请参见下方的论坛帖子),其中一个列表的属性会与兄弟列表的属性混合。
2012 年 9 月 19 日
- 重新格式化了损坏的文本,包括修复了在重新格式化过程中损坏的代码。
- 更新了用法代码。
- 添加了示例项目。