使用 XmlSerializer 反序列化 XML,其中 XmlElement 名称不同但内容相同
Deserialize XML with XmlSerializer where XmlElement names differ but have same content
我想将 XML 文件反序列化为具有多个子 class 的 class。 XML 看起来像这样:
<?xml version="1.0" encoding="utf-8"?>
<Objects>
<Group index="1">
<de>
<GroupName>ANTRIEB</GroupName>
</de>
<en>
<GroupName>missing translation!</GroupName>
</en>
<Level>2</Level>
</Group>
<Group index="2">
<de>
<GroupName>BREMSEN</GroupName>
</de>
<Level>3</Level>
</Group>
</Objects>
将 XML 反序列化为 classes 没有问题,如果没有那些语言标签的话。当然,我可以为每种可能的语言标签创建一个 属性。但是可能的语言列表应该是动态的(例如从配置文件中读取)。
这就是为什么我想将这些语言标签及其内容反序列化为一个字典,该字典使用语言作为键和内容的模型。
我的模型是这样的:
[XmlRoot("Objects")]
public class DeactivationsXml
{
[XmlElement("Group")]
public DeactivationsGroup[] Groups { get; set; }
}
[Serializable()]
public class DeactivationsGroup
{
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
}
public class GroupName
{
[XmlElement("GroupName")]
public string Name { get; set; }
}
我搜索了很长时间来解决这个问题,但找不到解决方案。我很确定,仅用属性是不可能解决这个问题的。
是否存在一些混合方法以便将 XML 文件的反序列化与所有无法自动反序列化的 XmlElement 的手动反序列化相结合?
如果我的问题有一个好的且可扩展的解决方案会很棒,因为 XML 结构很复杂(同一个问题好几次但内容不同等等)。
我无法更改XML的结构,所以请不要指出这一点。
方法
IXmlSerializable
我尝试在 DeactivationsGroup class 上实现 IXmlSerializable 接口,以便在给定语言列表中搜索具有这些名称的 XmlElements 并反序列化这些 XmlElements 的内容。
但是这种方法没有奏效,因为您必须手动映射所有属性。
IExtensibleDataObject
该接口仅受 DataContractSerializer 支持。在最坏的情况下,如果找不到其他解决方案,我可以在反序列化后使用此接口进行反序列化..
反序列化
XmlSerializer 不支持此属性,但可以提供我可能需要的功能。
XmlAnyElement
我想这是目前最好的选择。反序列化完成后是否存在一些回调以自动执行此操作?
可执行代码
这是到目前为止的全部代码。
public void Parse()
{
string xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
" <Objects>" +
" <Group index=\"1\">" +
" <de>" +
" <GroupName>ANTRIEB</GroupName>" +
" </de>" +
" <en>" +
" <GroupName>missing translation!</GroupName>" +
" </en>" +
" <Level>2</Level>" +
" </Group>" +
" <Group index=\"2\">" +
" <de>" +
" <GroupName>BREMSEN</GroupName>" +
" </de>" +
" <Level>3</Level>" +
" </Group>" +
" </Objects>";
XmlSerializer serializer = new XmlSerializer(typeof(DeactivationsXml));
using (TextReader fileStream = new StringReader(xml))
{
var result = (DeactivationsXml)serializer.Deserialize(fileStream);
}
}
[XmlRoot("Objects")]
public class DeactivationsXml
{
[XmlElement("Group")]
public DeactivationsGroup[] Groups { get; set; }
}
[Serializable()]
public class DeactivationsGroup
{
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
}
public class GroupName
{
[XmlElement("GroupName")]
public string Name { get; set; }
}
尝试使用 xml linq
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication1
{
class Program
{
const string FILENAME = @"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
DeactivationsGroup.GroupNames = doc.Descendants("Group").Select(x => new {
languages = x.Elements().Where(y => y.Element("GroupName") != null).Select(y => new DeactivationsGroup() {
name = (string)y.Element("GroupName"),
level = (int)x.Element("Level"),
index = byte.Parse((string)x.Attribute("index")),
language = y.Name.LocalName
})
}).SelectMany(y => y.languages)
.GroupBy(x => x.name, y => y)
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
public class DeactivationsGroup
{
public static Dictionary<string, DeactivationsGroup> GroupNames { get; set; }
public string name { get; set; }
public int level { get; set; }
public byte index { get; set; }
public string language { get; set; }
}
}
}
您可以采用 this answer and add a surrogate XmlElement []
property, marked with [XmlAnyElement]
中的方法,对 Dictionary<string, GroupName>
属性 的 key/value 对执行嵌套(反)序列化,绑定字典键到元素名称。
请注意,虽然 XmlAnyElementAttribute
的 documentation 表示
Specifies that the member (a field that returns an array of XmlElement or XmlNode objects) contains objects that represent any XML element that has no corresponding member in the object being serialized or deserialized.
事实上,该属性也可以应用于 属性。因此,不需要(反)序列化回调,因为嵌套序列化可以在 getter 和 setter 内部为代理 属性 本身执行。如果您更喜欢新的 LINQ-to-XML API.
,它也可以应用于返回 XElement
对象数组而不是 XmlElement
的成员
在这种方法中,您的 DeactivationsGroup
看起来像:
[Serializable()]
public class DeactivationsGroup
{
public DeactivationsGroup() { this.GroupNames = new Dictionary<string, GroupName>(); }
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; }
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
[XmlAnyElement]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
public XElement[] XmlGroupNames
{
get
{
return GroupNames.SerializeToXElements(null);
}
set
{
if (value == null || value.Length < 1)
return;
foreach (var pair in value.DeserializeFromXElements<GroupName>())
{
GroupNames.Add(pair.Key, pair.Value);
}
}
}
}
利用以下扩展方法和类:
public static class XmlKeyValueListHelper
{
const string RootLocalName = "Root";
public static XElement [] SerializeToXElements<T>(this IEnumerable<KeyValuePair<string, T>> dictionary, XNamespace ns)
{
if (dictionary == null)
return null;
ns = ns ?? "";
var serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
var array = dictionary
.Select(p => new { p.Key, Value = p.Value.SerializeToXElement(serializer, true) })
// Fix name and remove redundant xmlns= attributes. XmlWriter will add them back if needed.
.Select(p => new XElement(ns + p.Key, p.Value.Attributes().Where(a => !a.IsNamespaceDeclaration), p.Value.Elements()))
.ToArray();
return array;
}
public static IEnumerable<KeyValuePair<string, T>> DeserializeFromXElements<T>(this IEnumerable<XElement> elements)
{
if (elements == null)
yield break;
XmlSerializer serializer = null;
XNamespace ns = null;
foreach (var element in elements)
{
if (serializer == null || element.Name.Namespace != ns)
{
ns = element.Name.Namespace;
serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
}
var elementToDeserialize = new XElement(ns + RootLocalName, element.Attributes(), element.Elements());
yield return new KeyValuePair<string, T>(element.Name.LocalName, elementToDeserialize.Deserialize<T>(serializer));
}
}
public static XmlSerializerNamespaces NoStandardXmlNamespaces()
{
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
return ns;
}
public static XElement SerializeToXElement<T>(this T obj)
{
return obj.SerializeToXElement(null, NoStandardXmlNamespaces());
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializerNamespaces ns)
{
return obj.SerializeToXElement(null, ns);
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, bool omitStandardNamespaces)
{
return obj.SerializeToXElement(serializer, (omitStandardNamespaces ? NoStandardXmlNamespaces() : null));
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
{
var doc = new XDocument();
using (var writer = doc.CreateWriter())
(serializer ?? new XmlSerializer(obj.GetType())).Serialize(writer, obj, ns);
var element = doc.Root;
if (element != null)
element.Remove();
return element;
}
public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
{
using (var reader = element.CreateReader())
{
object result = (serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
return (T)result;
}
}
}
public static class XmlSerializerFactory
{
// To avoid a memory leak the serializer must be cached.
//
// This factory taken from
//
readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache;
readonly static object padlock;
static XmlSerializerFactory()
{
padlock = new object();
cache = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
}
public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
{
if (serializedType == null)
throw new ArgumentNullException();
if (rootName == null && rootNamespace == null)
return new XmlSerializer(serializedType);
lock (padlock)
{
XmlSerializer serializer;
var key = Tuple.Create(serializedType, rootName, rootNamespace);
if (!cache.TryGetValue(key, out serializer))
cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
return serializer;
}
}
}
这很容易完成,如下所示。
使用你的一组 类。
在序列化程序上设置事件处理程序:
var serializer = new XmlSerializer(typeof(DeactivationsXml));
serializer.UnknownElement += Serializer_UnknownElement;
这个处理程序中的代码非常简单:
private void Serializer_UnknownElement(object sender, XmlElementEventArgs e)
{
var group = (DeactivationsGroup)e.ObjectBeingDeserialized;
group.GroupNames.Add(e.Element.Name, new GroupName { Name = e.Element.InnerText });
}
我想将 XML 文件反序列化为具有多个子 class 的 class。 XML 看起来像这样:
<?xml version="1.0" encoding="utf-8"?>
<Objects>
<Group index="1">
<de>
<GroupName>ANTRIEB</GroupName>
</de>
<en>
<GroupName>missing translation!</GroupName>
</en>
<Level>2</Level>
</Group>
<Group index="2">
<de>
<GroupName>BREMSEN</GroupName>
</de>
<Level>3</Level>
</Group>
</Objects>
将 XML 反序列化为 classes 没有问题,如果没有那些语言标签的话。当然,我可以为每种可能的语言标签创建一个 属性。但是可能的语言列表应该是动态的(例如从配置文件中读取)。
这就是为什么我想将这些语言标签及其内容反序列化为一个字典,该字典使用语言作为键和内容的模型。
我的模型是这样的:
[XmlRoot("Objects")]
public class DeactivationsXml
{
[XmlElement("Group")]
public DeactivationsGroup[] Groups { get; set; }
}
[Serializable()]
public class DeactivationsGroup
{
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
}
public class GroupName
{
[XmlElement("GroupName")]
public string Name { get; set; }
}
我搜索了很长时间来解决这个问题,但找不到解决方案。我很确定,仅用属性是不可能解决这个问题的。
是否存在一些混合方法以便将 XML 文件的反序列化与所有无法自动反序列化的 XmlElement 的手动反序列化相结合?
如果我的问题有一个好的且可扩展的解决方案会很棒,因为 XML 结构很复杂(同一个问题好几次但内容不同等等)。 我无法更改XML的结构,所以请不要指出这一点。
方法
IXmlSerializable
我尝试在 DeactivationsGroup class 上实现 IXmlSerializable 接口,以便在给定语言列表中搜索具有这些名称的 XmlElements 并反序列化这些 XmlElements 的内容。
但是这种方法没有奏效,因为您必须手动映射所有属性。
IExtensibleDataObject
该接口仅受 DataContractSerializer 支持。在最坏的情况下,如果找不到其他解决方案,我可以在反序列化后使用此接口进行反序列化..
反序列化
XmlSerializer 不支持此属性,但可以提供我可能需要的功能。
XmlAnyElement
我想这是目前最好的选择。反序列化完成后是否存在一些回调以自动执行此操作?
可执行代码
这是到目前为止的全部代码。
public void Parse()
{
string xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
" <Objects>" +
" <Group index=\"1\">" +
" <de>" +
" <GroupName>ANTRIEB</GroupName>" +
" </de>" +
" <en>" +
" <GroupName>missing translation!</GroupName>" +
" </en>" +
" <Level>2</Level>" +
" </Group>" +
" <Group index=\"2\">" +
" <de>" +
" <GroupName>BREMSEN</GroupName>" +
" </de>" +
" <Level>3</Level>" +
" </Group>" +
" </Objects>";
XmlSerializer serializer = new XmlSerializer(typeof(DeactivationsXml));
using (TextReader fileStream = new StringReader(xml))
{
var result = (DeactivationsXml)serializer.Deserialize(fileStream);
}
}
[XmlRoot("Objects")]
public class DeactivationsXml
{
[XmlElement("Group")]
public DeactivationsGroup[] Groups { get; set; }
}
[Serializable()]
public class DeactivationsGroup
{
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; } = new Dictionary<string, GroupName>();
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
}
public class GroupName
{
[XmlElement("GroupName")]
public string Name { get; set; }
}
尝试使用 xml linq
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ConsoleApplication1
{
class Program
{
const string FILENAME = @"c:\temp\test.xml";
static void Main(string[] args)
{
XDocument doc = XDocument.Load(FILENAME);
DeactivationsGroup.GroupNames = doc.Descendants("Group").Select(x => new {
languages = x.Elements().Where(y => y.Element("GroupName") != null).Select(y => new DeactivationsGroup() {
name = (string)y.Element("GroupName"),
level = (int)x.Element("Level"),
index = byte.Parse((string)x.Attribute("index")),
language = y.Name.LocalName
})
}).SelectMany(y => y.languages)
.GroupBy(x => x.name, y => y)
.ToDictionary(x => x.Key, y => y.FirstOrDefault());
}
public class DeactivationsGroup
{
public static Dictionary<string, DeactivationsGroup> GroupNames { get; set; }
public string name { get; set; }
public int level { get; set; }
public byte index { get; set; }
public string language { get; set; }
}
}
}
您可以采用 this answer and add a surrogate XmlElement []
property, marked with [XmlAnyElement]
中的方法,对 Dictionary<string, GroupName>
属性 的 key/value 对执行嵌套(反)序列化,绑定字典键到元素名称。
请注意,虽然 XmlAnyElementAttribute
的 documentation 表示
Specifies that the member (a field that returns an array of XmlElement or XmlNode objects) contains objects that represent any XML element that has no corresponding member in the object being serialized or deserialized.
事实上,该属性也可以应用于 属性。因此,不需要(反)序列化回调,因为嵌套序列化可以在 getter 和 setter 内部为代理 属性 本身执行。如果您更喜欢新的 LINQ-to-XML API.
,它也可以应用于返回XElement
对象数组而不是 XmlElement
的成员
在这种方法中,您的 DeactivationsGroup
看起来像:
[Serializable()]
public class DeactivationsGroup
{
public DeactivationsGroup() { this.GroupNames = new Dictionary<string, GroupName>(); }
[XmlIgnore]
public Dictionary<string, GroupName> GroupNames { get; set; }
public int Level { get; set; }
[XmlAttribute]
public byte index { get; set; }
[XmlAnyElement]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
public XElement[] XmlGroupNames
{
get
{
return GroupNames.SerializeToXElements(null);
}
set
{
if (value == null || value.Length < 1)
return;
foreach (var pair in value.DeserializeFromXElements<GroupName>())
{
GroupNames.Add(pair.Key, pair.Value);
}
}
}
}
利用以下扩展方法和类:
public static class XmlKeyValueListHelper
{
const string RootLocalName = "Root";
public static XElement [] SerializeToXElements<T>(this IEnumerable<KeyValuePair<string, T>> dictionary, XNamespace ns)
{
if (dictionary == null)
return null;
ns = ns ?? "";
var serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
var array = dictionary
.Select(p => new { p.Key, Value = p.Value.SerializeToXElement(serializer, true) })
// Fix name and remove redundant xmlns= attributes. XmlWriter will add them back if needed.
.Select(p => new XElement(ns + p.Key, p.Value.Attributes().Where(a => !a.IsNamespaceDeclaration), p.Value.Elements()))
.ToArray();
return array;
}
public static IEnumerable<KeyValuePair<string, T>> DeserializeFromXElements<T>(this IEnumerable<XElement> elements)
{
if (elements == null)
yield break;
XmlSerializer serializer = null;
XNamespace ns = null;
foreach (var element in elements)
{
if (serializer == null || element.Name.Namespace != ns)
{
ns = element.Name.Namespace;
serializer = XmlSerializerFactory.Create(typeof(T), RootLocalName, ns.NamespaceName);
}
var elementToDeserialize = new XElement(ns + RootLocalName, element.Attributes(), element.Elements());
yield return new KeyValuePair<string, T>(element.Name.LocalName, elementToDeserialize.Deserialize<T>(serializer));
}
}
public static XmlSerializerNamespaces NoStandardXmlNamespaces()
{
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
return ns;
}
public static XElement SerializeToXElement<T>(this T obj)
{
return obj.SerializeToXElement(null, NoStandardXmlNamespaces());
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializerNamespaces ns)
{
return obj.SerializeToXElement(null, ns);
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, bool omitStandardNamespaces)
{
return obj.SerializeToXElement(serializer, (omitStandardNamespaces ? NoStandardXmlNamespaces() : null));
}
public static XElement SerializeToXElement<T>(this T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
{
var doc = new XDocument();
using (var writer = doc.CreateWriter())
(serializer ?? new XmlSerializer(obj.GetType())).Serialize(writer, obj, ns);
var element = doc.Root;
if (element != null)
element.Remove();
return element;
}
public static T Deserialize<T>(this XContainer element, XmlSerializer serializer)
{
using (var reader = element.CreateReader())
{
object result = (serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
return (T)result;
}
}
}
public static class XmlSerializerFactory
{
// To avoid a memory leak the serializer must be cached.
//
// This factory taken from
//
readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache;
readonly static object padlock;
static XmlSerializerFactory()
{
padlock = new object();
cache = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
}
public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
{
if (serializedType == null)
throw new ArgumentNullException();
if (rootName == null && rootNamespace == null)
return new XmlSerializer(serializedType);
lock (padlock)
{
XmlSerializer serializer;
var key = Tuple.Create(serializedType, rootName, rootNamespace);
if (!cache.TryGetValue(key, out serializer))
cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
return serializer;
}
}
}
这很容易完成,如下所示。
使用你的一组 类。
在序列化程序上设置事件处理程序:
var serializer = new XmlSerializer(typeof(DeactivationsXml));
serializer.UnknownElement += Serializer_UnknownElement;
这个处理程序中的代码非常简单:
private void Serializer_UnknownElement(object sender, XmlElementEventArgs e)
{
var group = (DeactivationsGroup)e.ObjectBeingDeserialized;
group.GroupNames.Add(e.Element.Name, new GroupName { Name = e.Element.InnerText });
}