65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ExpandoObject 从 XML 创建动态对象

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (8投票s)

2012 年 9 月 18 日

CPOL

5分钟阅读

viewsIcon

99577

downloadIcon

2652

使用 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 日

  • 重新格式化了损坏的文本,包括修复了在重新格式化过程中损坏的代码。
  • 更新了用法代码。
  • 添加了示例项目。
© . All rights reserved.