2

I am trying to cast an XML document to a C# class, and for the most part, it's working fine, but in my XML I have a custom prefix, which gives me some trouble. I don't have any control over how the XML file is being generated.

<mxb:bike>Test Bike</mxb:bike>

I'm getting stuck on the mxb part since it isn't declared. My problem is that it's not declared in the XML file itself, since otherwise, it wouldn't throw the error in the first place, but I hope there is a way of solving this code side. I'm working with a .NET application, and am trying to parse an XML file to a C# model.

I'm getting the data through a post request on an API endpoint, which when I specify a model automatically converts it to that model.

My XML input is as following:

<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxacx="http://www.garmin.com/xmlschemas/AccelerationExtension/v1">
<metadata>
    <time>2022-09-25T12:06:47.081Z</time>
    <extensions>
        <mxb:bike>Test Bike</mxb:bike>
        <mxb:notes>Testing</mxb:notes>
        <mxb:conditions>Dry</mxb:conditions>
        <mxb:track>Test Track</mxb:track>
    </extensions>
</metadata>
<trk>
    <trkseg>
        <trkpt lat="52.914892" lon="4.776444">
            <time>2022-09-25T12:06:47.081Z</time>
        </trkpt>
    </trkseg>
</trk>
</gpx>

My C# model looks like this:

public class GpxData
{
    [XmlRoot(ElementName="extensions", Namespace="http://www.topografix.com/GPX/1/1")]
    public class Extensions {
        [XmlElement(ElementName="bike", Namespace="")] 
        public string? Bike { get; set; } 

        [XmlElement(ElementName="notes", Namespace="")] 
        public string? Notes { get; set; } 

        [XmlElement(ElementName="conditions", Namespace="")] 
        public string? Conditions { get; set; } 

        [XmlElement(ElementName="track", Namespace="")] 
        public string? Track { get; set; }
    }
    
    [XmlRoot(ElementName="metadata", Namespace="http://www.topografix.com/GPX/1/1")]
    public class Metadata {
        [XmlElement(ElementName="time", Namespace="http://www.topografix.com/GPX/1/1")] 
        public DateTime Time { get; set; } 
        
        [XmlElement(ElementName="extensions", Namespace="http://www.topografix.com/GPX/1/1")] 
        public Extensions Extensions { get; set; } 
    }
    
    [XmlRoot(ElementName = "trkpt", Namespace = "http://www.topografix.com/GPX/1/1")]
    public class Trkpt
    {
        [XmlElement(ElementName = "time", Namespace = "http://www.topografix.com/GPX/1/1")]
        public DateTime Time { get; set; }

        [XmlAttribute(AttributeName = "lat", Namespace = "")]
        public double Lat { get; set; }

        [XmlAttribute(AttributeName = "lon", Namespace = "")]
        public double Lon { get; set; }
    }

    [XmlRoot(ElementName = "trkseg", Namespace = "http://www.topografix.com/GPX/1/1")]
    public class Trkseg
    {
        [XmlElement(ElementName = "trkpt", Namespace = "http://www.topografix.com/GPX/1/1")]
        public List<Trkpt> Trkpt { get; set; }
    }

    [XmlRoot(ElementName = "trk", Namespace = "http://www.topografix.com/GPX/1/1")]
    public class Trk
    {
        [XmlElement(ElementName = "trkseg", Namespace = "http://www.topografix.com/GPX/1/1")]
        public Trkseg Trkseg { get; set; }
    }

    [XmlRoot(ElementName = "gpx", Namespace = "http://www.topografix.com/GPX/1/1")]
    public class Gpx
    {
        [XmlElement(ElementName="metadata", Namespace="http://www.topografix.com/GPX/1/1")] 
        public Metadata Metadata { get; set; }

        [XmlElement(ElementName = "trk", Namespace = "http://www.topografix.com/GPX/1/1")]
        public Trk Trk { get; set; }

        [XmlAttribute(AttributeName = "creator", Namespace = "")]
        public string Creator { get; set; }
    }

Whenever I remove the part within the "extensions" tag, it works completely fine.

When I only remove the mxb part, and just keep the part with for example "bike" it doesn't work either.

I tried to declare the MXB prefix in the XML file itself, but since I don't have any control over the XML generation, I can't really change anything within the file, since that would only be a temporary solution, besides the fact that I don't have a schema, and as far as I know, I can't declare a prefix, without giving it a schema. Even if that would work, that wouldn't be a good solution in my case. I also tried to remove the mxb part in the XML file, and even though that solved the undeclared error issue, it would just result in the information not being cast to the model.

All the solutions I tried were related to changing something in the XML file, but that's not something I can do ;-(

I'm pretty new to XML serialzation \ deseralation, so I hope someone is able to help me! Thanks in advance.

Bram
  • 511
  • 1
  • 6
  • 22
  • Does this answer your question? [Deserializing XML with namespace and multiple nested elements](https://stackoverflow.com/questions/27365029/deserializing-xml-with-namespace-and-multiple-nested-elements) – vernou Jan 27 '23 at 10:23
  • Unfortunately not, could you by any chance, clarify the answers given there? In your linked post, ns2 is being declared in the XML file itself, and it has a schema, which both I have not. – Bram Jan 27 '23 at 10:30
  • 1
    mxb namespace is missing. There is no URL for the namespace in xml. The xml is not valid. The xml should of been checked by the developer before you got the corrupted xml. – jdweng Jan 27 '23 at 11:21
  • Agreed, but would there be a solution to solve this anyway without changing the XML, or is the XML just invalid, so I can't properly cast it to a model? – Bram Jan 27 '23 at 11:36
  • The XML file should be properly fixed especially in this case where there is a schema. Without the prefix the schema check will also fail. – jdweng Jan 27 '23 at 15:24

1 Answers1

0

Inspired from this answer.

You can inject a custom XmlNamespaceManager to correct missing namespace.

public class MyXmlNamespaceManager : XmlNamespaceManager
{
    const string MissingNamespacePrefix = "http://missing.namespace.prefix.net/2014/";

    public MyXmlNamespaceManager(XmlNameTable nameTable)
        : base(nameTable)
    { }

    void AddMissingNamespace(string prefix)
    {
        if (string.IsNullOrEmpty(prefix))
            return;

        string uri = MissingNamespacePrefix + prefix;
        AddNamespace(prefix, uri);
    }

    public override bool HasNamespace(string prefix)
    {
        var result = base.HasNamespace(prefix);
        if (!result)
            AddMissingNamespace(prefix);
        result = base.HasNamespace(prefix);
        return result;
    }

    public override string LookupNamespace(string prefix)
    {
        var result = base.LookupNamespace(prefix);
        if (result == null)
            AddMissingNamespace(prefix);
        result = base.LookupNamespace(prefix);
        return result;
    }
}

Thereby, the extension node become :

<extensions>
  <mxb:bike xmlns:mxb="http://missing.namespace.prefix.net/2014/mxb">Test Bike</mxb:bike>
  <mxb:notes xmlns:mxb="http://missing.namespace.prefix.net/2014/mxb">Testing</mxb:notes>
  <mxb:conditions xmlns:mxb="http://missing.namespace.prefix.net/2014/mxb">Dry</mxb:conditions>
  <mxb:track xmlns:mxb="http://missing.namespace.prefix.net/2014/mxb">mxb Track</mxb:track>
</extensions>

You need to adapt your model class like :

[XmlRoot(ElementName = "extensions", Namespace = "http://www.topografix.com/GPX/1/1")]
public class Extensions
{
    [XmlElement(ElementName = "bike", Namespace = "http://missing.namespace.prefix.net/2014/mxb")]
    public string? Bike { get; set; }

    [XmlElement(ElementName = "notes", Namespace = "http://missing.namespace.prefix.net/2014/mxb")]
    public string? Notes { get; set; }

    [XmlElement(ElementName = "conditions", Namespace = "http://missing.namespace.prefix.net/2014/mxb")]
    public string? Conditions { get; set; }

    [XmlElement(ElementName = "track", Namespace = "http://missing.namespace.prefix.net/2014/mxb")]
    public string? Track { get; set; }
}

Then deserialize like :

var xml = "...";

// Fix the Xml
XmlDocument xmlDoc;
using (var stream = new StringReader(xml))
{
    var settings = new XmlReaderSettings();
    settings.NameTable = new NameTable();
    var manager = new MyXmlNamespaceManager(settings.NameTable);
    XmlParserContext context = new XmlParserContext(null, manager, null, XmlSpace.Default);
    using (var xmlReader = XmlReader.Create(stream, settings, context))
    {
        xmlDoc = new XmlDocument();
        xmlDoc.Load(xmlReader);
    }
}

// Deserialize
using (var stream = new MemoryStream())
{
    xmlDoc.Save(stream);
    stream.Seek(0, SeekOrigin.Begin);

    var serializer = new XmlSerializer(typeof(GpxData.Gpx));
    var result = serializer.Deserialize(stream) as GpxData.Gpx;
    Console.WriteLine(result.Metadata.Extensions.Bike);
}
vernou
  • 6,818
  • 5
  • 30
  • 58
  • Thank you so much for your answer! This seems to be precisely what I was looking for. I'll try it out later! – Bram Jan 27 '23 at 17:20