如何使用 'generic xml root'
How to work with 'generic xml root'
一些(外部)天才决定为我们提供 XML:
<message_X>
<header>
<foo>Foo</foo>
<bar>Bar</bar>
</header>
<body>
<blah>
<yadda1 />
<yadda2 />
<yadda3 />
<contentX>
<!-- message_X specific content -->
</contentX>
</blah>
</body>
</message_X>
但是,还有其他消息(例如,message_Y
和 message_Z
)。除了 content
节点中的内容之外,这些都具有完全相同的基本结构,并且这个问题的原因是不同的根节点:
<message_Y>
<header>
<foo>Foo</foo>
<bar>Bar</bar>
</header>
<body>
<blah>
<yadda1 />
<yadda2 />
<yadda3 />
<contentY>
<!-- message_X specific content -->
</contentY>
</blah>
</body>
</message_Y>
为什么根节点不只是命名为 <message>
,因为我会这样做,这让我感到困惑。谁想出这个?
我已经相应地创建了一个摘要class Message
:
public abstract class Message {
public Header Header { get; set; }
public Body Body { get; set; }
}
public class Header {
public string Foo { get; set; }
public string Bar { get; set; }
}
// Etc...
我希望我能做到这一点:
[XmlInclude(typeof(XMessage))]
[XmlInclude(typeof(YMessage))]
public abstract class Message {
// ...
}
[XmlRoot("message_X")]
public class XMessage : Message {
// ...
}
[XmlRoot("message_Y")]
public class YMessage : Message {
// ...
}
但这不起作用:InvalidOperationException: <message_X xmlns=''> was not expected.
。要反序列化,我使用:
var ser = new XmlSerializer(typeof(Message));
using (var sr = new StringReader(xmlString))
return (Message)ser.Deserialize(sr);
我无法控制 XML 并且我不希望为每个 X、Y 和 Z 一次又一次地实现此消息。
我将通过可能将 Message
变成 Message<T>
并通过指定 T 等继承来整理 Content
部分,但这将是以后的问题。
我也试过将 Message
指定为 Type
和 XMessage
以及 YMessage
作为 XmlSerializer Constructor 的 ExtraTypes
但那没有也无济于事。我也尝试过使用 DataContract
、KnownType
等注释走 DataContractSerializer
路线,但这也没有用。
我将不胜感激有关如何以干净的方式解决此问题的提示/指示。
在@steve16351的**想法下我写了下面的反序列化器(我对序列化不感兴趣,只对反序列化感兴趣):
public class XmlDeserializer<T>
where T : class
{
// "Globally" caches T => Dictionary<xmlelementnames, type>
private static readonly ConcurrentDictionary<Type, IDictionary<string, Type>> _typecache = new ConcurrentDictionary<Type, IDictionary<string, Type>>();
// We store instances of serializers per type T in this pool so we need not create a new one each time
private static readonly ConcurrentDictionary<Type, XmlSerializer> _serializers = new ConcurrentDictionary<Type, XmlSerializer>();
// And all serializers get the same instance of XmlReaderSettings which, again saves creating objects / garbage collecting.
private static readonly XmlReaderSettings _readersettings = new XmlReaderSettings() { IgnoreWhitespace = true };
// Lookup for current T, with this we keep a reference for the current T in the global cache so we need one less dictionary lookup
private readonly IDictionary<string, Type> _thistypedict;
public XmlDeserializer()
{
// Enumerate T's XmlInclude attributes
var includes = ((IEnumerable<XmlIncludeAttribute>)typeof(T).GetCustomAttributes(typeof(XmlIncludeAttribute), true));
// Get all the mappings
var mappings = includes.Select(a => new
{
a.Type,
((XmlRootAttribute)a.Type.GetCustomAttributes(typeof(XmlRootAttribute), true).FirstOrDefault())?.ElementName
}).Where(m => !string.IsNullOrEmpty(m.ElementName));
// Store all mappings in our current instance and at the same time store the mappings for T in our "global cache"
_thistypedict = _typecache.GetOrAdd(typeof(T), mappings.ToDictionary(v => v.ElementName, v => v.Type));
}
public T Deserialize(string input)
{
// Read our input
using (var stringReader = new StringReader(input))
using (var xmlReader = XmlReader.Create(stringReader, _readersettings))
{
xmlReader.MoveToContent();
// Make sure we know how to deserialize this element
if (!_thistypedict.TryGetValue(xmlReader.Name, out var type))
throw new InvalidOperationException($"Unable to deserialize type '{xmlReader.Name}'");
// Grab serializer from pool or create one if required
var serializer = _serializers.GetOrAdd(type, (t) => new XmlSerializer(t, new XmlRootAttribute(xmlReader.Name)));
// Finally, now deserialize...
return (T)serializer.Deserialize(xmlReader);
}
}
}
此反序列化器使用 XmlInclude
属性来确定哪些 类 将使用 XmlRoot
属性映射到哪个元素名称。用法再简单不过了:
var ser = new XmlDeserializer<Message>();
ser.Deserialize("<message_X>...");
它做了一些内部 'caching' 和 'pooling' 对象来保持它 memory/GC-friendly 并且相当高效。所以这解决了我的 root-node-different-name-for-each-type 问题。现在我需要弄清楚我将如何处理不同的内容节点...
** 后来谁因为一些不明原因删除了他的回答...
问题的第二部分,'generic content'很容易解决:
public class Blah {
public Yadda1 Yadda1 { get; set; }
public Yadda2 Yadda2 { get; set; }
public Yadda3 Yadda3 { get; set; }
[XmlElement("contentX", typeof(ContentX))]
[XmlElement("contentY", typeof(ContentY))]
[XmlChoiceIdentifier(nameof(PayloadType))]
public Payload Payload { get; set; }
public PayloadType PayloadType { get; set; }
}
public abstract class Payload { ... }
public class ContentX : Payload { ... }
public class ContentY : Payload { ... }
[XmlType]
public enum PayloadType
{
[XmlEnum("contentX")]
ContentX,
[XmlEnum("contentY")]
ContentY,
}
这让我想知道我是否可以在根节点上应用相同的想法...
一些(外部)天才决定为我们提供 XML:
<message_X>
<header>
<foo>Foo</foo>
<bar>Bar</bar>
</header>
<body>
<blah>
<yadda1 />
<yadda2 />
<yadda3 />
<contentX>
<!-- message_X specific content -->
</contentX>
</blah>
</body>
</message_X>
但是,还有其他消息(例如,message_Y
和 message_Z
)。除了 content
节点中的内容之外,这些都具有完全相同的基本结构,并且这个问题的原因是不同的根节点:
<message_Y>
<header>
<foo>Foo</foo>
<bar>Bar</bar>
</header>
<body>
<blah>
<yadda1 />
<yadda2 />
<yadda3 />
<contentY>
<!-- message_X specific content -->
</contentY>
</blah>
</body>
</message_Y>
为什么根节点不只是命名为 <message>
,因为我会这样做,这让我感到困惑。谁想出这个?
我已经相应地创建了一个摘要class Message
:
public abstract class Message {
public Header Header { get; set; }
public Body Body { get; set; }
}
public class Header {
public string Foo { get; set; }
public string Bar { get; set; }
}
// Etc...
我希望我能做到这一点:
[XmlInclude(typeof(XMessage))]
[XmlInclude(typeof(YMessage))]
public abstract class Message {
// ...
}
[XmlRoot("message_X")]
public class XMessage : Message {
// ...
}
[XmlRoot("message_Y")]
public class YMessage : Message {
// ...
}
但这不起作用:InvalidOperationException: <message_X xmlns=''> was not expected.
。要反序列化,我使用:
var ser = new XmlSerializer(typeof(Message));
using (var sr = new StringReader(xmlString))
return (Message)ser.Deserialize(sr);
我无法控制 XML 并且我不希望为每个 X、Y 和 Z 一次又一次地实现此消息。
我将通过可能将 Message
变成 Message<T>
并通过指定 T 等继承来整理 Content
部分,但这将是以后的问题。
我也试过将 Message
指定为 Type
和 XMessage
以及 YMessage
作为 XmlSerializer Constructor 的 ExtraTypes
但那没有也无济于事。我也尝试过使用 DataContract
、KnownType
等注释走 DataContractSerializer
路线,但这也没有用。
我将不胜感激有关如何以干净的方式解决此问题的提示/指示。
在@steve16351的**想法下我写了下面的反序列化器(我对序列化不感兴趣,只对反序列化感兴趣):
public class XmlDeserializer<T>
where T : class
{
// "Globally" caches T => Dictionary<xmlelementnames, type>
private static readonly ConcurrentDictionary<Type, IDictionary<string, Type>> _typecache = new ConcurrentDictionary<Type, IDictionary<string, Type>>();
// We store instances of serializers per type T in this pool so we need not create a new one each time
private static readonly ConcurrentDictionary<Type, XmlSerializer> _serializers = new ConcurrentDictionary<Type, XmlSerializer>();
// And all serializers get the same instance of XmlReaderSettings which, again saves creating objects / garbage collecting.
private static readonly XmlReaderSettings _readersettings = new XmlReaderSettings() { IgnoreWhitespace = true };
// Lookup for current T, with this we keep a reference for the current T in the global cache so we need one less dictionary lookup
private readonly IDictionary<string, Type> _thistypedict;
public XmlDeserializer()
{
// Enumerate T's XmlInclude attributes
var includes = ((IEnumerable<XmlIncludeAttribute>)typeof(T).GetCustomAttributes(typeof(XmlIncludeAttribute), true));
// Get all the mappings
var mappings = includes.Select(a => new
{
a.Type,
((XmlRootAttribute)a.Type.GetCustomAttributes(typeof(XmlRootAttribute), true).FirstOrDefault())?.ElementName
}).Where(m => !string.IsNullOrEmpty(m.ElementName));
// Store all mappings in our current instance and at the same time store the mappings for T in our "global cache"
_thistypedict = _typecache.GetOrAdd(typeof(T), mappings.ToDictionary(v => v.ElementName, v => v.Type));
}
public T Deserialize(string input)
{
// Read our input
using (var stringReader = new StringReader(input))
using (var xmlReader = XmlReader.Create(stringReader, _readersettings))
{
xmlReader.MoveToContent();
// Make sure we know how to deserialize this element
if (!_thistypedict.TryGetValue(xmlReader.Name, out var type))
throw new InvalidOperationException($"Unable to deserialize type '{xmlReader.Name}'");
// Grab serializer from pool or create one if required
var serializer = _serializers.GetOrAdd(type, (t) => new XmlSerializer(t, new XmlRootAttribute(xmlReader.Name)));
// Finally, now deserialize...
return (T)serializer.Deserialize(xmlReader);
}
}
}
此反序列化器使用 XmlInclude
属性来确定哪些 类 将使用 XmlRoot
属性映射到哪个元素名称。用法再简单不过了:
var ser = new XmlDeserializer<Message>();
ser.Deserialize("<message_X>...");
它做了一些内部 'caching' 和 'pooling' 对象来保持它 memory/GC-friendly 并且相当高效。所以这解决了我的 root-node-different-name-for-each-type 问题。现在我需要弄清楚我将如何处理不同的内容节点...
** 后来谁因为一些不明原因删除了他的回答...
问题的第二部分,'generic content'很容易解决:
public class Blah {
public Yadda1 Yadda1 { get; set; }
public Yadda2 Yadda2 { get; set; }
public Yadda3 Yadda3 { get; set; }
[XmlElement("contentX", typeof(ContentX))]
[XmlElement("contentY", typeof(ContentY))]
[XmlChoiceIdentifier(nameof(PayloadType))]
public Payload Payload { get; set; }
public PayloadType PayloadType { get; set; }
}
public abstract class Payload { ... }
public class ContentX : Payload { ... }
public class ContentY : Payload { ... }
[XmlType]
public enum PayloadType
{
[XmlEnum("contentX")]
ContentX,
[XmlEnum("contentY")]
ContentY,
}
这让我想知道我是否可以在根节点上应用相同的想法...