目录
介绍
我开始在Internet上寻找一种简单的方法来加载XML文件并将其即时转换为对象。我的目标是为正在处理的Web项目以XML格式动态加载设置文件。
由于XML模式的定义在每个设置中都非常不同,因此我想到了使用dynamic关键字加载XML文档并将其作为简单对象进行访问。浏览后,我发现了一些使用的示例ExpandoObject。不幸的是,他们都写关于将对象转换为XML的方法,或者如何使用XML-to-LINQ映射特定的动态类/对象。
因此,我最终编写了一个小的递归方法,该方法可加载XML文档并将其转换为简单对象。
*更新
我注意到,即使3年后,人们仍然发现此技巧有用。
我认为共享一个小的更新将是一个好主意,这样就不需要使用特殊的类型属性来确定子节点是否是集合的一部分。
我突然意识到,我在Entity Framework中如此讨厌和恐惧的功能很可能就是我想要的解决方案,从而消除了用type = list属性装饰节点的需要。
我正在谈论的功能是多元化。在EF中,我们拥有一个模型,该模型的类名为Course,该类在名为Courses的数据库表中自动更改。谷歌搜索后,我遇到了以下小宝石:
它允许将string复数或单数化。然后,我认为这很适合确定子元素是否是集合的一部分,因为父节点名称通常(如果不是始终)是其子名称的复数形式。
无论如何,我已经更改了逻辑,该逻辑将确定如果节点具有满足以下条件的子元素,则该节点是否为容器项目:
- 该项目具有1个子元素,其节点名称是其父名称的单数形式,或者
- 该项目具有多个子元素,但是子节点名称必须全部相同。
我相信这将涵盖大多数情况,并且不需要任何特殊的type = list属性。
*更新2
我不敢相信我们现在已经6年了,本文仍在使用中。
本周,我正在为自己编写的产品Feed解析器更新一些代码,实际上我有一些时间来更新此功能。
首先,它是现在的.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时会产生一些性能问题,因为它在内部需要在运行时进行大量反射,并且您还将在编译期间失去对动态对象的错误检查的机会。
例如,读入配置文件或产品简单列表可能很有用。