How can I get position in the original xml file of an xml tag after deserialization into a .NET object using XmlSerializer ?
Here is an example XML
4 ABC Bern 3 ABCD Prague
XMLto C# object mapping
[XmlRoot("Root")] public class AddressDetails { [XmlElement("Number")] public int HouseNo; [XmlElement("Street")] public string StreetName; [XmlElement("CityName")] public string City; }
Desired result
XmlSerializer serializer = new XmlSerializer(typeof(List)); var list = serializer.Deserialize(@"C:\Xml.txt") as List; // this is what I would like to do // getting information to origin of the property City of the 2nd object in the list var position = XmlSerializerHelper.GetPosition(o => list[1].City, @"C:\Xml.txt"); // should print "starts line=10, column=8" Console.WriteLine("starts line={0}, column={1}", position.Start.Line, position.Start.Column); // should print "ends line=10, column=35" Console.WriteLine("ends line={0}, column={1}", position.End.Line, position.Start.Column); // should print "type=XmlElement, name=CityName, value=Prague" Console.WriteLine("xml info type={0}, name={1}, value={2}", position.Type, position.Name, position.Value);
Another, more simple approach: Let the deserializer do the work.
Add LineInfo and LinePosition properties to all classes for which you would like to have position information:
[XmlRoot("Root")] public class AddressDetails { [XmlAttribute] public int LineNumber { get; set; } [XmlAttribute] public int LinePosition { get; set; } ... }
This of course can be done by subclassing.
Load an XDocument with LoadOptions.SetLineInfo.
Add LineInfo and LinePosition attributes to all elements:
foreach (var element in xdoc.Descendants()) { var li = (IXmlLineInfo) element; element.SetAttributeValue("LineNumber", li.LineNumber); element.SetAttributeValue("LinePosition", li.LinePosition); }
Deserializing will populate LineInfo and LinePosition.
Cons:
- Line information only for elements that are deserialized as class, not for simple elements, not for attributes.
- Need to add attributes to all classes.
I tried several approaches. Two of them:
Implement IXmlSerializable. In principle this would work, but then one would have to implement the complete deserialization mechanism.
Deserialize using an XmlReader, save a reference to that XmlReader at some static location, and in the constructor of classes that are deserialized, access that XmlReader and retrieve line and position information. Cons: static; relatively complicated.
I did not find a way to get more information than start line and position, but honestly, that’s enough for my use case.
Currently I think I will use the following approach.
Load the XML file with line information:
XDocument xdoc = XDocument.Parse(xml, LoadOptions.SetLineInfo);
To all elements in xdoc, add a new attribute (e.g. NewId) that allows to identify the elements, e.g. give the first element a 0, the second a 1, etc.
Make sure all classes that are deserialized (in your case AddressDetails) will have such a NewId, e.g.:
[XmlAttribute] public int NewId { get; set; }
This can be done by deriving from a common base class, and even when xsd.exe is used to create the classes, this is possible because it will create all classes as partial (e.g. a partial class foo : MyBaseClassElement { } needs to be added somewhere for all classes).
After deserializing, for each element that has been deserialized as a class object, the XElement from which it has been deserialized can be looked up using NewId. (To speed up the look up, one can use a List that contains all XElement such that NewId is the index in that list.)
Casting the XElement to IXmlLineInfo will give line and position information.
For elements that have not been deserialized as a class object (e.g. Number in your example) and for attributes, first look up the XElement that contains a Ne