JamTay317
JamTay317

Reputation: 1017

Converting xml to hierarchical data in c# with classes

I have a class that goes and gets data from a database and it gets the as xml data in the form of hierarchical data.

below is a sample of the data:

<SecurityGroups>
<SecurityGroup>
    <Id>1</Id>
    <Name>View</Name>
</SecurityGroup>
<SecurityGroup>
    <Id>2</Id>
    <Name>Fill</Name>
    <SecurityUsers>
        <SecurityUser>
            <securityId>2</securityId>
            <userId>2</userId>
            <username>Fill</username>
        </SecurityUser>
        <SecurityUser>
            <securityId>2</securityId>
            <userId>3</userId>
            <username>FillOne</username>
        </SecurityUser>
        <SecurityUser>
            <securityId>2</securityId>
            <userId>4</userId>
        <username/></SecurityUser>
    </SecurityUsers>
</SecurityGroup>
<SecurityGroup>
    <Id>3</Id>
    <Name>Update</Name>
    <SecurityUsers>
        <SecurityUser>
            <securityId>3</securityId>
            <userId>5</userId>
            <username>Update</username>
        </SecurityUser>
        <SecurityUser>
            <securityId>3</securityId>
            <userId>6</userId>
            <username>UpdateOne</username>
        </SecurityUser>
    </SecurityUsers>
</SecurityGroup>
<SecurityGroup>
    <Id>4</Id>
    <Name>Admin</Name>
    <SecurityUsers>
        <SecurityUser>
            <securityId>4</securityId>
            <userId>1</userId>
            <username>JTays</username>
        </SecurityUser>
    </SecurityUsers>
</SecurityGroup>

Which works great! Now, I have a class that uses reflection to convert from xml to models that i am eventually going to use in a treeview. below is the entire class.

the models are:

[XmlRootAttribute(Namespace = "", IsNullable = false)]
public class SecurityGroup
{
    [XmlElementAttribute(Form = XmlSchemaForm.Unqualified)]
    public int Id { get; set; }
    [XmlElementAttribute(Form = XmlSchemaForm.Unqualified)]
    public string Name { get; set; }
    [XmlElementAttribute("SecurityUser",
        Form = XmlSchemaForm.Unqualified)]
    public List<SecurityUser> SecurityUsers { get; set; }
}

public class SecurityUser:IEntity
{
    [XmlElement(Form = XmlSchemaForm.Unqualified)]
    public int SecurityId { get; set; }
    [XmlElementAttribute(Form = XmlSchemaForm.Unqualified)]
    public int UserId { get; set; }
    [XmlElementAttribute(Form = XmlSchemaForm.Unqualified)]
    public string Username { get; set; }

    public int Id { get; set; }
}

Here is the class that converts.

    public class AdminManager : IAdminService
{
    public IEnumerable<SecurityGroup> SecurityGroups()
    {
        IEnumerable<SecurityGroup> list = null;
        XmlDocument xmlDoc = new XmlDocument();
        using (var c = new FMContext())
        {
            var xmlData = c.Database.SqlQuery<string>("AllSecurtyUsersProc").FirstOrDefault();
            if (xmlData != null)
            {
                xmlDoc.LoadXml(xmlData);
                list = ConvertXmlToClass<SecurityGroup>(xmlDoc, "/SecurityGroups/SecurityGroup");
            }
        }
        return list;
    }

    public static IEnumerable<T> ConvertXmlToClass<T>( XmlDocument doc, string nodeString)
        where T:class, new()
    {
        var xmlNodes = doc.SelectNodes(nodeString);
        List<T> list = new List<T>();
        foreach (XmlNode node in xmlNodes)
        {
            var item = GetNewItem<T>(node);
            list.Add(item);
        }
        return list;
    }

    public static T GetNewItem<T>(XmlNode node)
        where T:class, new()
    {
        var type = typeof (T);
        var item = new T();
        var properties = type.GetProperties();
        foreach (var property in properties)
        {
            var propertyType = property.PropertyType;
            var propertyName = property.Name;
            object value = null;
            if (IsEnumerable(property))
            {
                value = GetNodeCollectionValue(property,node);
            }
            else
            {
                value = GetNodeValue(node, propertyName);
            }
            if (value!=null)
            {
                property.SetValue(item, Convert.ChangeType(value, propertyType), null);
            }

        }
        return item;
    }

    private static object GetNodeCollectionValue(PropertyInfo property, XmlNode node)
    {
        var doc = new XmlDocument();
        var itemType = property.PropertyType.GenericTypeArguments[0];
        var xml = $"<{property.Name}><{itemType.Name}>{node.InnerXml}</{itemType.Name}></{property.Name}>";
        doc.LoadXml(xml);
        if (itemType != null)
        {
            var type = typeof (AdminManager);
            var methodInfo = type.GetMethod("ConvertXmlToClass");
            if (methodInfo!=null)
            {
                var method = methodInfo.MakeGenericMethod(itemType);
                if (method != null)
                {
                    object[] args = {doc, property.Name};
                    object result = method.Invoke(null, args);
                    return result;
                }
            }

        }

        return new object();
    }


    private static bool IsEnumerable(PropertyInfo property)
    {
        var type = property.PropertyType;
        return typeof (IEnumerable).IsAssignableFrom(type) && type.IsGenericType;
    }

    private static object GetNodeValue(XmlNode node, string nodeName)
    {
        var i = node[nodeName]?.InnerText;
        return i ?? null;
    }
}

Okay, so now my issue. My problem is that when it converts it doesn't get the correct data for the SecurtyUsers classes it adds them as an object, but everything is null or 0. Can someone help me figure out where i went worng?

Upvotes: 0

Views: 617

Answers (2)

JamTay317
JamTay317

Reputation: 1017

I found the issue, it was that i was passing the entire xml into the ConvertXmlToClass<T> method. I have fixed this and below is the working code.

public class AdminManager : IAdminService
{
    public IEnumerable<SecurityGroup> SecurityGroups()
    {
        IEnumerable<SecurityGroup> list = null;
        XmlDocument xmlDoc = new XmlDocument();
        using (var c = new FMContext())
        {
            var xmlData = c.Database.SqlQuery<string>("AllSecurtyUsersProc").FirstOrDefault();
            if (xmlData != null)
            {
                xmlDoc.LoadXml(xmlData);
                list = ConvertXmlToClass<SecurityGroup>(xmlDoc, "/SecurityGroups/SecurityGroup");
            }
        }
        return list;
    }

    public static IEnumerable<T> ConvertXmlToClass<T>(XmlDocument doc, string nodeString)
        where T : class, new()
    {
        var xmlNodes = doc.SelectNodes(nodeString);
        List<T> list = new List<T>();
        foreach (XmlNode node in xmlNodes)
        {
            var item = GetNewItem<T>(node);
            list.Add(item);
        }
        return list;
    }

    public static T GetNewItem<T>(XmlNode node)
        where T : class, new()
    {
        var type = typeof(T);
        var item = new T();
        var properties = type.GetProperties();
        foreach (var property in properties)
        {
            var propertyType = property.PropertyType;
            var propertyName = property.Name;
            object value = null;
            if (IsEnumerable(property))
            {
                value = GetNodeCollectionValue(property, node);
            }
            else
            {
                value = GetNodeValue(node, propertyName);
            }
            if (value != null)
            {
                property.SetValue(item, Convert.ChangeType(value, propertyType), null);
            }

        }
        return item;
    }

    private static object GetNodeCollectionValue(PropertyInfo property, XmlNode node)
    {
        var doc = new XmlDocument();
        var itemType = property.PropertyType.GenericTypeArguments[0];
        var xml = node.InnerXml;
        if (xml.Contains(property.Name))
        {
            var start = xml.IndexOf($"<{property.Name}>");
            var length = xml.IndexOf($"</{property.Name}>") - start + ($"</{property.Name}>").Length;
            xml = xml.Substring(start, length);
            doc.LoadXml(xml);
            if (itemType != null)
            {
                var type = typeof(AdminManager);
                var methodInfo = type.GetMethod("ConvertXmlToClass");
                if (methodInfo != null)
                {
                    var method = methodInfo.MakeGenericMethod(itemType);
                    if (method != null)
                    {
                        object[] args = { doc, $"/{property.Name}/{itemType.Name}" };
                        object result = method.Invoke(null, args);
                        var r = result as IEnumerable<object>;
                        return r;
                    }
                }
            }
        }

        return null;
    }


    private static bool IsEnumerable(PropertyInfo property)
    {
        var type = property.PropertyType;
        return typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType;
    }

    private static object GetNodeValue(XmlNode node, string nodeName)
    {
        if (node[nodeName] != null)
        {
            var i = node[nodeName].InnerText;
            return i;
        }
        return null;
    }
}

I hope this helps someone else!

Upvotes: 0

ckerth
ckerth

Reputation: 316

First: your example XML doesn't include the closing tag </SecurityGroups>.

In your GetNodeCollectionValue you pass property.Name but shouldn't it be property.Name + "/" + itemType.Name because you want to access the children of that node?

Additionally node.InnerXml is not traversed, you pass the same node again and again.

I highly recommend using the .Net XML deserializer instead of reinventing the wheel.


Generate a XML schema for your XML example. Afterwards create the .Net classes from the XSD. Pass your XML from the server to a StringReader and Deserialize using the reader. Then just access the SecurityGroup from the class instance.

[update] to show usage

Create the XML Schema either manually or using the XSD.exe (you will need the Windows SDK). See the Examples.

  • create a sample.xml file, content is your example
  • locate and run the XSD tool xsd sample.xml /outputDir:XmlSerialization
  • a .xsd file should be generated, you may want to modify some element types
    • for example SecurityGroup.Id is maybe auto-generated as xs:string, change it to i.e. xs:int
    • or remove the minOccurs="0" at SecurityUser.userId if you know that the property is always set
    • you maybe have to fix SecurityGroup.SecurityUsers from
<xs:element name="SecurityUsers" minOccurs="0" maxOccurs="unbounded">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="SecurityUser" minOccurs="0" maxOccurs="unbounded">

to

<xs:element name="SecurityUsers" minOccurs="0">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="SecurityUser" maxOccurs="unbounded">
  • run the XSD tool using the XSD xsd sample.xsd /outputDir:XmlSerialization /c
  • a .cs file should be generated, verify it
    • I needed to fix the XSD manually because i.e. SecurityGroup.SecurityUser was generated as a 2-dim array
  • use the auto-generated classes in your project / script
  • create a XmlSerializer - see Example - and deserialize the XML string, access the data afterwards
SecurityGroups deserialized;
var serializer = new System.Xml.Serialization.XmlSerializer(typeof(SecurityGroups));
using (var stringReader = new System.IO.StringReader(xmlData))
{
    deserialized = serializer.Deserialize(stringReader) as SecurityGroups;
}
  • see my manually fixed XSD here, verified working with your sample data

Upvotes: 1

Related Questions