使用 ExpandoObject 将 XML 转换为动态对象






4.85/5 (20投票s)
加载 XML 文档并将其转换为动态对象
引言
我开始在互联网上寻找一种简单的方法来加载 XML 文件并将其动态转换为对象。我的目标是为我正在进行的一个 Web 项目动态加载 XML 格式的设置文件。
由于每个设置的 XML 架构定义差异很大,我想使用 dynamic
关键字来加载 XML 文档并将其作为简单对象访问。经过一番浏览,我发现了一些使用 ExpandoObject
的例子。不幸的是,它们要么是关于将对象转换为 XML,要么是如何使用 XML-to-LINQ 来映射特定的动态类/对象。
所以我最终编写了一个小的递归方法,用于加载 XML 文档并将其转换为一个简单的对象。
* 更新
我注意到即使过了 3 年,人们仍然觉得这个技巧很有用。
我认为分享一个小更新是个好主意,它消除了使用特殊类型属性来确定子节点是否是集合的一部分的需要。
我突然意识到我在 Entity Framework 中如此讨厌和害怕的功能,可能正是我一直在寻找的解决方案,它可以消除使用 type=list 属性来装饰我的节点的需要。
我所说的功能是复数化。在 EF 中,我们有一个模型,其中一个类名为 Course
,它会自动更改为名为 Courses
的数据库表。经过一番 Google 搜索,我偶然发现了以下小宝石
它允许将 string
复数化或单数化。然后,我认为这将非常适合确定子元素是否是集合的一部分,因为通常(如果不是总是),父节点名称是其子节点名称的复数形式。
无论如何,我更改了逻辑,如果节点具有满足以下条件的子元素,它将确定该节点是否为容器项
- 该项目有 1 个子元素,并且其节点名称是其父名称的单数形式,或者
- 该项目有多个子元素,但子节点名称必须全部相同。
我相信这将涵盖大多数场景,并且无需任何特殊的 type=list 属性。
* 更新 2
我简直不敢相信我们已经过去了 6 年,这篇文章仍然在使用。
本周,我正在更新我编写的一个产品源解析器的一些代码,我实际上有一些时间
来稍微更新一下这个函数。
首先,它现在是 .NET Standard,因此可以在 .NET Framework 4.5 及更高版本以及 .NET Core 中使用。
我认为新方法对于多种场景也更有用。
好吧,我希望人们会发现它很有用。
代码
private dynamic GetAnonymousType(string xml, XElement element = null)
{
// either set the element directly or parse XML from the xml parameter.
element = string.IsNullOrEmpty(xml) ? element : XDocument.Parse(xml).Root;
// if there's no element than there's no point to continue
if (element == null) return null;
IDictionary<string, dynamic> result = new ExpandoObject();
// grab any attributes and add as properties
element.Attributes().AsParallel().ForAll
(attribute => result[attribute.Name.LocalName] = attribute.Value);
// check if there are any child elements.
if (!element.HasElements)
{
// check if the current element has some value and add it as a property
if (!string.IsNullOrWhiteSpace(element.Value))
result[element.Name.LocalName] = element.Value;
return result;
}
// Check if the child elements are part of a collection (array). If they are not then
// they are either a property of complex type or a property with simple type
var isCollection = (element.Elements().Count() > 1
&& element.Elements().All(e => e.Name.LocalName.ToLower()
== element.Elements().First().Name.LocalName.ToLower())
// the pluralizer is needed in a scenario where you have
// 1 child item and you still want to treat it as an array.
// If this is not important than you can remove the last part
// of the if clause which should speed up this method considerably.
|| element.Name.LocalName.ToLower() ==
new Pluralize.NET.Core.Pluralizer().Pluralize
(element.Elements().First().Name.LocalName).ToLower());
var values = new ConcurrentBag<dynamic>();
// check each child element
element.Elements().ToList().AsParallel().ForAll(i =>
{
// if it's part of a collection then add the collection items to a temp variable
if (isCollection) values.Add(GetAnonymousType(null, i));
else
// if it's not a collection, but it has child elements
// then it's either a complex property or a simple property
if (i.HasElements)
// create a property with the current child elements name
// and process its properties
result[i.Name.LocalName] = GetAnonymousType(null, i);
else
// create a property and just add the value
result[i.Name.LocalName] = i.Value;
});
// for collection items we want skip creating a property with the child item names,
// but directly add the child properties to the
if (values.Count > 0) result[element.Name.LocalName] = values;
// return the properties of the processed element
return result;
}
示例 XML
<?xml version="1.0"?>
<License>
<RegisteredUser>Remco Reitsma</RegisteredUser>
<Company>Xtraworks.com</Company>
<Sites>
<Site>
<Host>xtraworks.com</Host>
<ExpireDate>15/12/2099</ExpireDate>
</Site>
<Site>
<Host>test.com</Host>
<ExpireDate>15/12/2099</ExpireDate>
</Site>
</Sites>
<Modules>
<Module>
<Name>SEO Package</Name>
<Controller>SEO</Controller>
<Version>0.0.1</Version>
<Tables>
<Table>
<Name>SEO_Site</Name>
</Table>
<Table>
<Name>SEO_Pages</Name>
</Table>
</Tables>
</Module>
</Modules>
</License>
用法
dynamic license = GetAnonymousType(xmlString);
// Getting the values from the dynamic object is really easy now.
var registeredUser = license.RegisteredUser;
var companyName = license.Company;
// Getting a collection is just as easy as it simply returns a list
var sites = license.Sites;
foreach(var site in sites)
{
var host = site.Host;
}
// I am sure it's easy enough for you guys to extrapolate from this simple example.
注意
重要的是要意识到这是一个非常简单的例子,绝不是万无一失的。代码在您能够控制提供的 XML 文件的情况下很有用。此外,使用 dynamic
会有一些性能问题,因为它内部需要在运行时进行大量反射,并且您也会失去在编译期间对动态对象进行错误检查。
例如,它可以用于读取配置文件或简单的产品列表。
历史
- 2011 年 7 月 18 日:初始版本