0

I am trying to deserialize XML response received from moodle web services.

I could parse it into a dotnet object if it had distinct named attributes like id, shortname, idnumber etc. But it has got an array of KEY attributes with actual field name as a value and inside it, there is another node having the field value.

Here is a sample:

    <?xml version="1.0" encoding="UTF-8" ?>
        <RESPONSE>
            <MULTIPLE>
            <SINGLE>
                <KEY name="id">
                    <VALUE>2</VALUE>
                </KEY>
                <KEY name="shortname">
                    <VALUE>CS-101</VALUE>
                </KEY>
                <KEY name="fullname">
                    <VALUE>CS-101</VALUE>
                </KEY>
                <KEY name="enrolledusercount">
                    <VALUE>2</VALUE>
                </KEY>
                <KEY name="idnumber">
                    <VALUE></VALUE>
                </KEY>
                <KEY name="visible">
                    <VALUE>1</VALUE>
                </KEY>
                <KEY name="summary">
                    <VALUE>&lt;p&gt;CS-101&lt;br /&gt;&lt;/p&gt;</VALUE>
                </KEY>
                <KEY name="summaryformat">
                    <VALUE>1</VALUE>
                </KEY>
                <KEY name="format">
                    <VALUE>weeks</VALUE>
                </KEY>
                <KEY name="showgrades">
                    <VALUE>1</VALUE>
                </KEY>
                <KEY name="lang">
                    <VALUE></VALUE>
                </KEY>
                <KEY name="enablecompletion">
                    <VALUE>0</VALUE>
                </KEY>
            </SINGLE>
            </MULTIPLE>
        </RESPONSE>

I want to parse this XML into an object of this class:

class Course
{
    public int id { get; set; }
    public string shortname { get; set; }  //short name of course
    public string fullname { get; set; }   //long name of course
    public int enrolledusercount { get; set; }  //Number of enrolled users in this course
    public string idnumber { get; set; }   //id number of course
    public int visible { get; set; }  //1 means visible, 0 means hidden course
    public string summary { get; set; }
    public int summaryformat { get; set; } //summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN)
    public string format { get; set; } //course format: weeks, topics, social, site
    public int showgrades { get; set; } //true if grades are shown, otherwise false
    public string lang { get; set; } //forced course language
    public int enablecompletion { get; set; } //true if completion is enabled, otherwise false
}

Is there a direct way to do it or should I write a parser method with switch cases for each field?

Danish
  • 694
  • 1
  • 7
  • 15
  • 1
    Would it help to return JSON formatted data instead of XML? Adding the param moodlewsrestformat=json to the webservice request will return in JSON format instead. – davosmith Aug 08 '16 at 07:01
  • actually i knew it returns JSON as well, but somehow I just forgot and couldn't think beyond the default XML format. JSON would be much easier to work with. Thanks. – Danish Aug 08 '16 at 07:08

3 Answers3

2

You need to write your custom parser using XmlReader, no any default deserializer that could do it by any preset.

Also, you don't need to use switch/cases, you can use Reflection to fill your props.

Nigrimmist
  • 10,289
  • 4
  • 52
  • 53
0

As far as I know there is no default deserializer for this structure. And as @Nigrimmist said - you do not need to use switch-case.

You can use XmlReader for read an xml, or do it with XmlDocument. Sample code for parsing below. I have not tested it, so use it careful.

public T ConvertFromXml<T>(string yourXml, params object[] activationData)
    where T : new() //if your type has default constructor
{
    var resultType = typeof(T);
    //if your type has not default constructor
    var result = (T)Activator.CreateInstance(typeof(T), activationData);
    //if it has default constructor
    var result = new T();

    //create an instance of xml reader
    var xmlDocument = new XmlDocument();
    xmlDocument.LoadXml(yourXml);

    //expecting that your xml response will always have the same structure
    if (xmlDocument.SelectSingleNode("RESPONSE/SINGLE") != null)
    {
        foreach(XmlNode node in xmlDocument.SelectNodes("RESPONSE/SINGLE/KEY"))
        {
            var prop = resultType.GetProperty(node.Attributes["name"].Value);
            if (prop != null)
            {
                var value = prop.SelectSingleNode("Value").Value;
                //if value does not exist - check if null value can be assigned
                if (value == null && (!prop.PropertyType.IsValueType || (Nullable.GetUnderlyingType(prop.PropertyType) != null)))
                {
                    prop.SetValue(result, value); //explicitly setting the required value
                } 
                else if (value != null)
                {
                    //we receiving the string, so for number parameters we should parse it
                    if (IsNumberType(prop.PropertyType))
                    {
                        prop.SetValue(result, double.Parse(value));
                    }
                    else
                    {
                        prop.SetValue(result, value); //will throw an exception if property type is not a string
                    }

                    //need some additional work for DateTime, TimeSpan, arrays and other
                }

            }
            else 
            {
                //remove next line if you do not need a validation for property presence
                throw new ArgumentException("Could not find the required property " + node.Attributes["name"].Value);
            }
        }
    }
    else 
    {
        throw new ArgumentException("Xml has invalid structure");
    }

    return result;
}

private bool IsNumberType(Type t)
{
    var numberTypes = new[] {
        typeof(byte), typeof(short), typeof(int), typeof(long),
        typeof(float), typeof(double), typeof(decimal),
        typeof(byte?), typeof(short?), typeof(int?), typeof(long?),
        typeof(float?), typeof(double?), typeof(decimal?)
    };

    return numberTypes.Contains(t);
}

And sample usage:

var xml = /*your method to get string representing a xml*/;
return ConvertFromXml<Course>(xml);

If you can get response in json representation instead of xml, take a look at Json.Net library it is simple to use.

Mikhail Tulubaev
  • 4,141
  • 19
  • 31
0

Use xml linq. See code below. I parsed only id but you can add the other properties.

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)
        {
            Course course = new Course();
            course.ReadXML(FILENAME);
        }
    }
    public class Course
    {
        public static List<Course> courses = new List<Course>(); 

        public int id { get; set; }
        public string shortname { get; set; }  //short name of course
        public string fullname { get; set; }   //long name of course
        public int enrolledusercount { get; set; }  //Number of enrolled users in this course
        public string idnumber { get; set; }   //id number of course
        public int visible { get; set; }  //1 means visible, 0 means hidden course
        public string summary { get; set; }
        public int summaryformat { get; set; } //summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN)
        public string format { get; set; } //course format: weeks, topics, social, site
        public int showgrades { get; set; } //true if grades are shown, otherwise false
        public string lang { get; set; } //forced course language
        public int enablecompletion { get; set; } //true if completion is enabled, otherwise false

        public void ReadXML(string filename)
        {
            XDocument doc = XDocument.Load(filename);
            courses = doc.Descendants("SINGLE").Select(x => ReadKeys(x)).ToList();
        }
        public Course ReadKeys(XElement single)
        {
            Course newCourse = new Course();
            foreach(XElement key in single.Descendants("KEY"))
            {
                switch(key.Attribute("name").Value)
                {
                    case "id" :
                        newCourse.id = (int)key.Element("VALUE");
                        break;
                }
            }
            return newCourse;
        }
    }
}
jdweng
  • 33,250
  • 2
  • 15
  • 20