简体   繁体   中英

Parsing XML into classes using LINQ in C#

I'm having a lot of trouble parsing an XML document into my custom classes. I've tried to read what I can find on the web and on here, but I'm still not getting anywhere. I'm working on a real estate app, and am trying to model a basic property where you have:

  • 1 property
  • 1 property can have multiple buildings
  • Each building can have multiple tenants.

I decided to try to store the data in an xml document, and I made an example as follows:

<?xml version="1.0" encoding="UTF-8"?>
<Property>
    <Name>Grove Center</Name>
    <Building>
        <Name>Building1</Name>
        <Tenant>
            <Name>Tenant1</Name>
            <SquareFeet>2300</SquareFeet>
            <Rent>34000</Rent>
        </Tenant>
        <Tenant>
            <Name>Tenant2</Name>
            <SquareFeet>3100</SquareFeet>
            <Rent>42000</Rent>
        </Tenant>
        <Tenant>
            <Name>Tenant3</Name>
            <SquareFeet>1700</SquareFeet>
            <Rent>29000</Rent>
        </Tenant>
    </Building>
    <Building>
        <Name>Building2</Name>
        <Tenant>
            <Name>Tenant1</Name>
            <SquareFeet>6150</SquareFeet>
            <Rent>80000</Rent>
        </Tenant>
        <Tenant>
            <Name>Tenant2</Name>
            <SquareFeet>4763</SquareFeet>
            <Rent>60000</Rent>
        </Tenant>
    </Building>
</Property>

Actually my first question is if this format is even correct.. I saw some xml examples where they added an extra tag such as <buildings> before they started listing out the individual <Building> tags for each building. Is that necessary? The W3C examples I saw didn't do it that way.. but this post on stackexchange was pretty close to what im doing: Parsing XML with Linq with multiple descendants

Here is the code for my classes in C#:

public class Property
{
    public string Name { get; set; }
    public List<Building> Buildings = new List<Building>();
}

public class Building
{
    public string Name { get; set; }
    public List<Tenant> Tenants = new List<Tenant>();
}

public class Tenant
{
    public string Name { get; set; }
    public int SF { get; set; }
    public decimal Rent { get; set; }
}

I'm not sure if using the new keyword on my lists right in the class definition is good practice.. but I was getting errors trying to add a building or tenant to the list later on in my program so I didn't know what else to do. Right now I'm not much further in my main code than:

Property p = new Property();
XDocument doc = XDocument.Load(@"C:\Users\SampleUser\Desktop\sample-property.xml");

Any help is appreciated, thanks

Following query will give you the correct result:-

Property p = new Property
               {
                  Name = (string)doc.Root.Element("Name"),
                  Buildings = doc.Root.Elements("Building")
                                 .Select(x => new Building
                                  {
                                     Name = (string)x.Element("Name"),
                                     Tenants = x.Elements("Tenant")
                                                .Select(t => new Tenant
                                                 {
                                                     Name = (string)t.Element("Name"),
                                                     SF = (int)t.Element("SquareFeet"),
                                                     Rent = (decimal)t.Element("Rent")
                                                 }).ToList()
                                   }).ToList()
                };

Theres a few things you might want to change.

The property names must match the xml tags, or you have to specify the mapping manually. In your example code, Buildings and Tenants are declared as fields, you should change it to properties. If you want, you can then initialize them to empty list in the constructors:

public class Property
{
    public string Name { get; set; }
    [XmlElement("Building")]
    public List<Building> Buildings { get; set; }

    public Property()
    {
        Buildings = new List<Building>();
    }
}

public class Building
{
    public string Name { get; set; }
    [XmlElement("Tenant")]
    public List<Tenant> Tenants { get; set; }

    public Building()
    {
        Tenants = new List<Tenant>();
    }
}

public class Tenant
{
    public string Name { get; set; }
    [XmlAttribute("SquareFeet")]
    public int SF { get; set; }
    public decimal Rent { get; set; }
}

Further, I would recommend deserializing the file rather than using linq. Consider these helper methods:

public static class XmlHelper
{
    public static T DeserializeFromXmlString<T>(string xml)
    {
        var xmlSerializer = new XmlSerializer(typeof (T));
        using (var stringReader = new StringReader(xml))
        {
            return (T) xmlSerializer.Deserialize(stringReader);
        }
    }

    public static T DeserializeFromXmlFile<T>(string filename) where T : new()
    {
        return DeserializeFromXmlString<T>(File.ReadAllText(filename));
    }
}

Deserialization is then easy:

var listOfProperties = XmlHelper.DeserializeFromXmlFile<Property>(@"C:\Users\SampleUser\Desktop\sample-property.xml");

Intializing your public fields with empty lists is perfectly fine and good practice to avoid the errors you got. If you do not initialize them, they are null, hence the errors.

You could use properties instead of fields for your lists however.

Starting with C# 6 you can use simplified auto-property assignment:

public List<Building> Buildings {get;set;} = new List<Building>();

For C# < 6 you can use auto properties and initialize the property within the constructor or use a property with backing field.

//Auto property with assignment in constructor
public class Property
{
    public string Name { get; set; }
    public List<Building> Buildings {get;set;};
    public Property(){
        Buildings = new List<Building>();
    }
}

//Property with backing field
public class Property
{
    private List<Building> _buildings = new List<Building>();
    public string Name { get; set; }
    public List<Building> Buildings {get {return _buildings;} set {_buildings = value;}};
}

For reading XML and creating the object graph, you can use LINQ in conjuction with object initializers.

Func<IEnumerable<XElement>, IEnumerable<Tenant>> getTenants = elements => {
    return elements.Select (e => new Tenant {
        Name = e.Element("Name").Value,
        Rent = decimal.Parse(e.Element("Rent").Value),
        SF = int.Parse(e.Element("SquareFeet").Value)
    });
};

Func<IEnumerable<XElement>, IEnumerable<Building>> getBuildings = elements => {
    return elements.Select (e => new Building{
        Name = e.Element("Name").Value,
        Tenants = getTenants(e.Elements("Tenant")).ToList()
    });
};

//xdoc is your parsed XML document
//e.g. var xdoc = XDdocument.Parse("xml contents here");
var property = new Property{
    Name = xdoc.Root.Element("Name").Value,
    Buildings = getBuildings(xdoc.Root.Elements("Building")).ToList()
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM