如何使用 '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_Ymessage_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 指定为 TypeXMessage 以及 YMessage 作为 XmlSerializer ConstructorExtraTypes 但那没有也无济于事。我也尝试过使用 DataContractKnownType 等注释走 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,
}

这让我想知道我是否可以在根节点上应用相同的想法...