2

I see this question often enough, but nobody's title really seems to depict their question. I get a large response object back from a Web API that contains general response information, along with the data object I want to deserialize.

Full XML:

<?xml version="1.0"?>
<root>
  <status>
      <apiErrorCode>0</apiErrorCode>
      <apiErrorMessage/>
      <dbErrorCode>0</dbErrorCode>
      <dbErrorMessage/>
      <dbErrorList/>
  </status>
<data>
    <modelName>ReportXDTO</modelName>
    <modelData>
        <id>1780</id>
        <reportTitle>Access Level (select) with Door Assignment</reportTitle>
        <hasParameters>true</hasParameters>
        <parameters>
            <dataType>STRING</dataType>
            <title>Access Level:</title>
            <index>1</index>
            <allowMulti>true</allowMulti>
            <selectSql>SELECT DISTINCT [Name] FROM dbo.[Levels] WHERE [PrecisionFlag] = '0' ORDER BY [Name] </selectSql>
            <values>
                <value>Door 1</value>
                <used>1</used>
            </values>
            <values>
                <value>Door 2</value>
                <used>1</used>
            </values>
            <values>
                <value>Door 3</value>
                <used>1</used>
            </values>
       </parameters>
       <sourceSql>SELECT [Name], [SData] FROM [Schedules]</sourceSql>
       <report/>
   </modelData>
   <itemReturned>1</itemReturned>
   <itemTotal>1</itemTotal>
</data>
<listInfo>
    <pageIdRequested>1</pageIdRequested>
    <pageIdCurrent>1</pageIdCurrent>
    <pageIdFirst>1</pageIdFirst>
    <pageIdPrev>1</pageIdPrev>
    <pageIdNext>1</pageIdNext>
    <pageIdLast>1</pageIdLast>
    <itemRequested>1</itemRequested>
    <itemReturned>1</itemReturned>
    <itemStart>1</itemStart>
    <itemEnd>1</itemEnd>
    <itemTotal>1</itemTotal>
</listInfo>
</root>

I only want to deserialize the modelData element. The modelData object type is dynamic, depending on the API call.

I deserialize xml in other applications, and created the following method, but don't know how to specifically ONLY get the modelData element:

    public static T ConvertXmltoClass<T>(HttpResponseMessage http, string elementName) where T : new()
    {
        var newClass = new T();

        try
        {
            var doc = JsonConvert.DeserializeXmlNode(http.Content.ReadAsStringAsync().Result, "root");

            XmlReader reader = new XmlNodeReader(doc);
            reader.ReadToFollowing(elementName);

            //The xml needs to show the proper object name
            var xml = reader.ReadOuterXml().Replace(elementName, newClass.GetType().Name);

            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
            {
                var serializer = new XmlSerializer(typeof(T));
                newClass = (T)serializer.Deserialize(stream);
            }
        }
        catch (Exception e)
        {
            AppLog.LogException(System.Reflection.MethodBase.GetCurrentMethod().Name, e);
        }

        return newClass;
    }

I have updated this thread multiple times now, to stay current. I started updating it with the first solution. But that solution by itself didn't solve the problem. With the code how it is right now, I get no exceptions, but don't get the xml deserialized to my object. Instead I get a new, blank object. Thoughts?

THOUGH the object type can change, here is my current object I am dealing with: (PLEASE NOTE, that I deserialize the exact xml in modelData, in the Web API)

namespace WebApiCommon.DataObjects
{
    [Serializable]
    public class ReportXDto
    {
        public ReportXDto()
        {
            Parameters = new List<ReportParameterXDto>();
        }

        public int Id { get; set; }
        public string ReportTitle { get; set; }
        public bool HasParameters { get; set; } = false;
        public List<ReportParameterXDto> Parameters { get; set; }
        public string SourceSql { get; set; }
        public DataTable Report { get; set; }
    }

    [Serializable]
    public class ReportXDto
    {
        public ReportXDto()
        {
            Parameters = new List<ReportParameterXDto>();
        }

        public int Id { get; set; }
        public string ReportTitle { get; set; }
        public bool HasParameters { get; set; } = false;
        public List<ReportParameterXDto> Parameters { get; set; }
        public string SourceSql { get; set; }
        public DataTable Report { get; set; }
    }

    [Serializable]
    public class ReportParameterValuesXDto
    {
        public string Value { get; set; } = "";
        public bool Used { get; set; } = false;
    }


}
Jonathan Hansen
  • 423
  • 1
  • 7
  • 14
  • I'm a little confused about what you want to do. Why are you converting from JSON to XML? Why not deserialize directly from JSON? Also, why `XmlDocument` and not `XDocument`? – dbc Apr 20 '18 at 17:01
  • The Web API I get the data from, returns a json response. The deserializing from json to an XmlDocument, works great, and I use that method in many other applications. This problem though is unique, because the modelData returned in the API response, is XML. – Jonathan Hansen Apr 20 '18 at 19:14
  • I'm at a point of not caring what solution I have to implement, as long as I can deserialize the modelData into my T object. – Jonathan Hansen Apr 20 '18 at 19:14
  • You can deserialize directly from an `XmlNode` by using `ProperXmlNodeReader` from [Deserialize object property with StringReader vs XmlNodeReader](https://stackoverflow.com/a/30115691/3744182) and [c#, XML How to cycle through an XML node fetched with XMLNode.SelectSingleNode](https://stackoverflow.com/a/36582657/3744182). Or if you switch to LINQ to XML you could use `XObjectExtensions.Deserialize(this XContainer element)` from [Changing type of element in XML Serialization](https://stackoverflow.com/a/30278016). – dbc Apr 20 '18 at 19:27
  • So, what exactly is the problem now? Is it that the element name `` doesn't match the root element name of the type `T` to which you are trying to deserialize? Can you provide a [mcve] that includes the type `T` into which you are trying to deserialize? – dbc Apr 20 '18 at 19:36
  • I changed modelData to the T object GetType.Name, which made the solution stop throwing an exception, because the xml now matched the object class. HOWEVER, I only get back a blank object. – Jonathan Hansen Apr 20 '18 at 19:48
  • Also, I have tried using both a MemoryStream and a StringReader. Both result in a blank object. – Jonathan Hansen Apr 20 '18 at 19:51
  • To help you, I think we may need a [mcve] that shows the type(s). Possibly some namespaces somewhere are not matching. – dbc Apr 20 '18 at 19:55
  • Oh, this looks relevant: [How to deserialize a node in a large document using XmlSerializer](https://stackoverflow.com/a/48680420/3744182). – dbc Apr 20 '18 at 20:05
  • Very simple. Very common mistake. After filling a memory stream, you have to set the position to zero before reading. – jdweng Apr 20 '18 at 20:14
  • Sorry, can you please be more specific as to what you think the issue is? – Jonathan Hansen Apr 20 '18 at 20:39
  • [`XmlSerializer` is case sensitive](https://stackoverflow.com/q/2451997). All of your properties are Pascal Case but the XML element names are camel Case, so nothing will get deserialized. – dbc Apr 20 '18 at 20:44
  • You just made my eyes go wide open. – Jonathan Hansen Apr 20 '18 at 20:48
  • IT WORKS!!!!!!!!!!!!!!! Thank you dbc! – Jonathan Hansen Apr 20 '18 at 21:07

2 Answers2

1

Firstly, XmlSerializer is case sensitive. Thus your property names need to match the XML element names exactly -- unless overridden with an attribute that controls XML serialization such as [XmlElement(ElementName="id")]. To generate a data model with the correct casing I used http://xmltocsharp.azurewebsites.net/ which resulted in:

public class ReportParameterValuesXDto 
{
    [XmlElement(ElementName="value")]
    public string Value { get; set; }
    [XmlElement(ElementName="used")]
    public string Used { get; set; }
}

public class ReportParametersXDto 
{
    [XmlElement(ElementName="dataType")]
    public string DataType { get; set; }
    [XmlElement(ElementName="title")]
    public string Title { get; set; }
    [XmlElement(ElementName="index")]
    public string Index { get; set; }
    [XmlElement(ElementName="allowMulti")]
    public string AllowMulti { get; set; }
    [XmlElement(ElementName="selectSql")]
    public string SelectSql { get; set; }
    [XmlElement(ElementName="values")]
    public List<ReportParameterValuesXDto> Values { get; set; }
}

public class ReportXDto 
{
    [XmlElement(ElementName="id")]
    public string Id { get; set; }
    [XmlElement(ElementName="reportTitle")]
    public string ReportTitle { get; set; }
    [XmlElement(ElementName="hasParameters")]
    public string HasParameters { get; set; }
    [XmlElement(ElementName="parameters")]
    public ReportParametersXDto Parameters { get; set; }
    [XmlElement(ElementName="sourceSql")]
    public string SourceSql { get; set; }
    [XmlElement(ElementName="report")]
    public string Report { get; set; }
}

(After generating the model, I modified the class names to match your naming convention.)

Given the correct data model, you can deserialize directly from a selected XmlNode using an XmlNodeReader as shown in How to deserialize a node in a large document using XmlSerializer without having to re-serialize to an intermediate XML string. The following extension method does the trick:

public static partial class XmlExtensions
{
    public static IEnumerable<T> DeserializeElements<T>(this XmlNode root, string localName, string namespaceUri)
    {
        return new XmlNodeReader(root).DeserializeElements<T>(localName, namespaceUri);
    }

    public static IEnumerable<T> DeserializeElements<T>(this XmlReader reader, string localName, string namespaceUri)
    {
        var serializer = XmlSerializerFactory.Create(typeof(T), localName, namespaceUri);
        while (!reader.EOF)
        {
            if (!(reader.NodeType == XmlNodeType.Element && reader.LocalName == localName && reader.NamespaceURI == namespaceUri))
                reader.ReadToFollowing(localName, namespaceUri);

            if (!reader.EOF)
            {
                yield return (T)serializer.Deserialize(reader);
                // Note that the serializer will advance the reader past the end of the node
            }               
        }
    }
}

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 you would deserialize as follows:

var modelData = doc.DeserializeElements<ReportXDto>("modelData", "").FirstOrDefault();

Working sample .Net fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
0

For Huge xml files always use XmlReader so you do not get an out of memory issue. See code below to get the element as a string :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;

namespace ConsoleApplication1
{
    class Program
    {
        const string FILENAME = @"c:\temp\test.xml";
        static void Main(string[] args)
        {
            //or Create(Stream)
            XmlReader reader = XmlReader.Create(FILENAME);

            reader.ReadToFollowing("modelData");
            if (!reader.EOF)
            {
                string modelDataStr = reader.ReadOuterXml();
            }
        }
    }
}
jdweng
  • 33,250
  • 2
  • 15
  • 20
  • OK.... so in my case, where I am passed in the XML from the response of an API call.... and I need to deserialize only the elements within the modelData element, how do I get to it? – Jonathan Hansen Apr 20 '18 at 16:24
  • var reader = new XmlNodeReader(doc); reader.ReadToFollowing(elementName); reader.ReadOuterXml(); //This doesn't get me the object. Nether does reader.ReadInnerXml() – Jonathan Hansen Apr 20 '18 at 16:26
  • Is the object null? Are you using a filename or a stream? I assume it is coming from http.Content but wasn't sure if you were going to read a string or a stream. I tested with xml posted and it got correct element so make sure you got the proper response. – jdweng Apr 20 '18 at 16:38
  • I have the XmlDocument created from the JsonConvert.DeserializeXmlNode. XmlReader.Create(doc.InnerXml), it throws an exception – Jonathan Hansen Apr 20 '18 at 16:47
  • XmlReader reader = new XmlNodeReader(doc); reader.ReadToFollowing(elementName); reader.ReadOuterXml(); //This doesn't fail until I attempt to deserialize the stream – Jonathan Hansen Apr 20 '18 at 16:56
  • A linq statement doesn't get executed until the instruction that uses the output. So you do not get an error on the ReadToFollowing(). The root element of an xml must be a single node. It cannot be an array. I can't tell if the error is occurring on the Read of serialize. You can hover mouse on reader to check if it is null. I don't think it will be. So the issue is with the root object of the deserialize. You could test by putting the xml into a file and see if it works from a file. – jdweng Apr 20 '18 at 17:11
  • You still need to have the parent element so you have only one node at the root of the xml. – jdweng Apr 20 '18 at 17:12
  • jdweng, I have now updated my question and code above to include your suggestion... and the problem. – Jonathan Hansen Apr 20 '18 at 19:30