Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
449 views
in Technique[技术] by (71.8m points)

c# - How to deserialize a node in a large document using XmlSerializer

I have a large XML document that I have loaded into an XmlDocument and I want to use the XmlSerializer class to deserialize selected elements from it into a .NET class generated using xsd.exe.

Here's an MCVE of what I've tried so far; the xsd and generated class are at the end of the post. As noted in the comments in the code, I am getting an InvalidOperationException - <Cars xmlns:'http://MyNamespace' /> was not expected:

static string XmlContent = @"
    <RootNode xmlns=""http://MyNamespace"">
        <Cars>
        <Car make=""Volkswagen"" />
        <Car make=""Ford"" />
        <Car make=""Opel"" />
        </Cars>
    </RootNode>";

static void TestMcve()
{
    var doc = new XmlDocument();
    doc.LoadXml(XmlContent);
    var nsMgr = new XmlNamespaceManager(doc.NameTable);
    nsMgr.AddNamespace("myns", "http://MyNamespace");
    var rootSerializer = new XmlSerializer(typeof(RootNode));
    var root = (RootNode) rootSerializer.Deserialize(new XmlNodeReader(doc));
    Console.WriteLine(root.Cars[0].make); // Works fine so far

    var node = doc.DocumentElement.SelectSingleNode("myns:Cars", nsMgr);
    Console.WriteLine(node.OuterXml);
    var carSerializer = new XmlSerializer(typeof(Car));
    using (var reader = new XmlNodeReader(node))
    {
        // What I want is a list of Car instances deserialized from
        // the Car child elements of the Cars element.
        // The following line throws an InvalidOperationException
        // "<Cars xmlns:'http://MyNamespace' /> was not expected"
        // If I change SelectSingleNode above to select "myns:Cars/myns:Car"
        // I get "<Car xmlns:'http://MyNamespace' /> was not expected"
        var result = carSerializer.Deserialize(reader);
    }
}

I also want to subsequently update my Car class instance, and insert it back into the document using the XmlSerializer, which is the subject of a follow-up question How to insert a node in a large document using XmlSerializer .

The xsd and generated classes follow:

<xs:schema xmlns="http://MyNamespace" xmlns:xs="http://www.w3.org/2001/XMLSchema" 
           targetNamespace="http://MyNamespace" 
           elementFormDefault="qualified" attributeFormDefault="unqualified" 
           version="3.9.0.8">
  <xs:complexType name="Cars">
    <xs:sequence>
      <xs:element name="Car" type="Car" maxOccurs="unbounded"/>
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="Car">
    <xs:attribute name="make" type="xs:string" use="required"/>
  </xs:complexType>
  <xs:complexType name="RootNode">
    <xs:sequence>
      <xs:element name="Cars" type="Cars" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>
  <xs:element name="RootNode" type="RootNode" />
</xs:schema>

Code generated by xsd.exe:

using System.Xml.Serialization;


/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://MyNamespace")]
[System.Xml.Serialization.XmlRootAttribute(Namespace="http://MyNamespace", IsNullable=false)]
public partial class RootNode {

    private Car[] carsField;

    /// <remarks/>
    [System.Xml.Serialization.XmlArrayItemAttribute(IsNullable=false)]
    public Car[] Cars {
        get {
            return this.carsField;
        }
        set {
            this.carsField = value;
        }
    }
}

/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://MyNamespace")]
public partial class Car {

    private string makeField;

    /// <remarks/>
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string make {
        get {
            return this.makeField;
        }
        set {
            this.makeField = value;
        }
    }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You have two problems here:

  1. The var node = doc.DocumentElement.SelectSingleNode("myns:Cars", nsMgr); is positioned at the <Cars> element -- the container element for the repeating sequence of <Car> nodes -- but your XmlSerializer is constructed to deserialize a single root element named <Car>. Trying to deserialize a sequence of cars with a serializer constructed to deserialize a single car will not work.

  2. For some reason xsd.exe generated a definition for your Car type without an XmlRoot attribute:

    [System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://MyNamespace")]
    // Not included!
    //[System.Xml.Serialization.XmlRootAttribute(Namespace = "http://MyNamespace")]
    public partial class Car
    {
    }
    

    Thus if you attempt to serialize or deserialize a single instance of a Car as the root XML element of an XML document then XmlSerializer will expect that root element to not be in any namespace. Each <Car> node in your large document is in the "http://MyNamespace" default namespace, so attempting to deserialize each one individually also will not work.

    You could manually add the missing [XmlRoot(Namespace = "http://MyNamespace")] attribute to Car, but having to do this can be a nuisance if the XSD files are subsequently modified and the c# types need to be regenerated.

To avoid both issues, you can use XmlNode.SelectNodes(String,?XmlNamespaceManager) to select every <Car> nodes inside the <Cars> element, then deserialize each one by constructing an XmlSerializer with an override XmlRootAttribute with the element name and namespace of the node being deserialized. First, define the following extension methods:

public static partial class XmlNodeExtensions
{
    public static List<T> DeserializeList<T>(this XmlNodeList nodes)
    {
        return nodes.Cast<XmlNode>().Select(n => n.Deserialize<T>()).ToList();
    }

    public static T Deserialize<T>(this XmlNode node)
    {
        if (node == null)
            return default(T);
        var serializer = XmlSerializerFactory.Create(typeof(T), node.LocalName, node.NamespaceURI);
        using (var reader = new XmlNodeReader(node))
        {
            return (T)serializer.Deserialize(reader);
        }
    }
}

public static class XmlSerializerFactory
{
    // To avoid a memory leak the serializer must be cached.
    // https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
    // This factory taken from 
    // https://stackoverflow.com/questions/34128757/wrap-properties-with-cdata-section-xml-serialization-c-sharp/34138648#34138648

    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;
        }
    }
}

Then deserialize as follows:

var nodes = doc.DocumentElement.SelectNodes("myns:Cars/myns:Car", nsMgr);
var cars = nodes.DeserializeList<Car>();

Node that a serializer constructed with an override root element name or namespace must be cached to avoid a memory leak as explained in this answer by Marc Gravell.

Sample working .Net fiddle.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...