问题
I am reading a bunch of XML files into a list (IEnumerable really) of XElements. Then I want to convert the XElement list (these XElements contain a bunch of child-elements) into a list of classes, so I can do later operations with the data more easily.
Now if I know in advance the structure of XElements, this would be easy; I'd just create a class that mimics the XElement structure and fill instances of it with the XElement contents. But here's the caveat; my XML file element structure is mostly similar, but there could be the odd element that has a different structure. To better illustrate the situation let me take an example.
Let's say my XML files contain a bunch of 'Person' elements. The Person elements has some common elements that will be in ALL the elements, but there are some children of Person which can be found only in some of the elements.
For example all Person elements have these mandatory children:
<Person> <Name/> <Age/> <City/> <Country/> </Person>
But, some Person elements may contain additional children as follows:
<Person> <Name/> <Age/> <City/> <Country/> <EyeColor/> <Profession/> </Person>
To make things worse, these child elements can also have mostly similar structure that occasionally varies.
So is there a way that I can go through these XElements in just one loop, and put them into an instance that is somehow dynamically created, say, based on the element names or something similar? I could create a class with all the mandatory elements and leave few additional member variables for the odd new ones, but that's not ideal for two reasons; one, it would be a waste of space, and two, there could be more child element than I have extra variables in my class.
So I'm looking for a way to create the class instances dynamically to fit the XElement structure. In other words I'd really like to mimic the element structure right down to the deepest level.
Thanks in advance!
回答1:
I think the best route personally would be to get an XSD, if you cannot get that then make up a serializable class that has all the possibilities and then reference that. EG: You have two fields where one get's set sometimes and one you have never seen set but there is the potential in a spec somewhere it may happen.
So let's make up a pretend class:
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace GenericTesting.Models
{
[Serializable()]
public class Location
{
[XmlAttribute()]
public int Id { get; set; }
[XmlAttribute()]
public double PercentUsed { get; set; }
[XmlElement]
public string ExtraGarbage { get; set; }
[XmlText]
public string UsedOnceInTheUniverse { get; set; }
}
}
And for the purpose of serializing/deserializing let me give extension methods for those:
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace GenericTesting
{
static class ExtensionHelper
{
public static string SerializeToXml<T>(this T valueToSerialize)
{
dynamic ns = new XmlSerializerNamespaces();
ns.Add("", "");
StringWriter sw = new StringWriter();
using (XmlWriter writer = XmlWriter.Create(sw, new XmlWriterSettings { OmitXmlDeclaration = true }))
{
dynamic xmler = new XmlSerializer(valueToSerialize.GetType());
xmler.Serialize(writer, valueToSerialize, ns);
}
return sw.ToString();
}
public static T DeserializeXml<T>(this string xmlToDeserialize)
{
dynamic serializer = new XmlSerializer(typeof(T));
using (TextReader reader = new StringReader(xmlToDeserialize))
{
return (T)serializer.Deserialize(reader);
}
}
}
}
And a simple main entry point in a console app:
static void Main(string[] args)
{
var locations = new List<Location>
{
new Location { Id = 1, PercentUsed = 0.5, ExtraGarbage = "really important I'm sure"},
new Location { Id = 2, PercentUsed = 0.6},
new Location { Id = 3, PercentUsed = 0.7},
};
var serialized = locations.SerializeToXml();
var deserialized = serialized.DeserializeXml<List<Location>>();
Console.ReadLine();
}
I know this is not exactly what you are asking for but I personally think well typed is better for XML and any third party you ever deal with should have at the very least some type of spec sheet or details on what they are giving you. Else you are losing standards. Xml should not be created from reflection or other means dynamically as it is meant if anything to enforce strict typing if anything.
回答2:
if you want to just enumerate over any child element of <Person>
and xml is relatively small
you could use linq to xml
var listOfElementChildNames = XDocument.Parse(xml).Element("Person")
.Elements()
.Select(e => e.Name)
.ToList();
Edit:
instead of select .Select(e => e.Name) we could map to any class:
public class Person
{
public string Name {get;set;}
public int Age {get;set;}
public string City {get;set;}
}
var xml = @"<Person>
<Name>John</Name>
<Age>25</Age>
<City>New York</City>
</Person>";
var people = XDocument.Parse(xml).Elements("Person")
.Select(p => new Person
{
Name = p.Element("Name").Value,
Age = int.Parse(p.Element("Age").Value),
City = p.Element("City").Value
}).ToList();
回答3:
Let me first apologize for the VB, but that is what I do.
If I understand what you are wanting you could use a Dictionary. I shortened your example to have fewer mandatory items, but hopefully you get the idea. Here is the person class that simply iterates the children adding them to the dictionary by their element name.
Public Class Person
Private _dict As New Dictionary(Of String, XElement)
Public Sub New(persEL As XElement)
'if the class is intended to modify the original XML
'use this declaration.
Dim aPers As XElement = persEL
'if the original XML will go away during the class lifetime
'use this declaration.
'Dim aPers As XElement =New XElement( persEL)
For Each el As XElement In aPers.Elements
Me._dict.Add(el.Name.LocalName, el)
Next
End Sub
'mandatory children are done like this
Public Property Name() As String
Get
Return Me._dict("Name").Value
End Get
Set(ByVal value As String)
Me._dict("Name").Value = value
End Set
End Property
Public Property Age() As Integer
Get
Return CInt(Me._dict("Age").Value)
End Get
Set(ByVal value As Integer)
Me._dict("Age").Value = value.ToString
End Set
End Property
'end mandatory children
Public Property OtherChildren(key As String) As String
Get
Return Me._dict(key).Value
End Get
Set(ByVal value As String)
Me._dict(key).Value = value
End Set
End Property
Public Function HasChild(key As String) As Boolean
Return Me._dict.ContainsKey(key)
End Function
End Class
Here is a simple test to see how it works
Dim onePersXE As XElement = <Person>
<Name>C</Name>
<Age>22</Age>
<Opt1>optional C1</Opt1>
<Opt2>optional C2</Opt2>
</Person>
Dim onePers As New Person(onePersXE)
onePers.Name = "new name"
onePers.Age = 42
onePers.OtherChildren("Opt1") = "new opt1 value"
onePers.OtherChildren("Opt2") = "opt 2 has new value"
As you can see there are two mandatory elements and in this case two optional children.
Here is another example to show how persons might work
Dim persons As XElement
persons = <persons>
<Person>
<Name>A</Name>
<Age>32</Age>
</Person>
<Person>
<Name>B</Name>
<Age>42</Age>
<Opt1>optional B1</Opt1>
<Opt2>optional B2</Opt2>
</Person>
</persons>
Dim persList As New List(Of Person)
For Each el As XElement In persons.Elements
persList.Add(New Person(el))
Next
Hope this at least gives you some ideas.
来源:https://stackoverflow.com/questions/41946906/create-a-list-from-xelements-dynamically