Search code examples
c#xmlexceptiondeserializationxmlserializer

C# deserialize known "UnknownElement" from XML


I'm trying to find a simple way to deserialize compile-time known element to [Serializable()] class while reading XML where the elemnt itself is not the member of the class (any more). In short, I'm trying to deserialize compile-time known element into local variable instead of member when there is no coresponding member in the class being deserialized.

The trick is that when serializing, the member name is used in XML, eg:

<TheFooString>I'm from Foo.A</TheFooString>

If the class being deserialized doesn't have a member called 'TheFooString' (but I do know what to do with it) I can't find the way to deserialize element into local varibale even though I know the element name and the element type at compile time.

As you can see below, I know how to handle unknwon elements in XML and I even sort of know how to try to deserialize unknwon element into local. I just don't know how xD.

Pleae note that I'm looking for generic solution for any type.

Simple example: Having class

    namespace Foo
    {
        [Serializable()]
        public class A
        {
            public string TheFooString { set; get; }

            public string ToXmlString()
            {
                XmlSerializer serializer = new XmlSerializer(base.GetType());
                StringWriter writer = new StringWriter();
                serializer.Serialize((TextWriter)writer, this);
                return writer.ToString();
            }
        }
    }

and class

    namespace Bar
    {
        [Serializable()]
        public class A
        {
            public string TheBarString { set; get; }

            [XmlAnyElement]
            public List<XmlElement> UnknownElements { get; set; }

            public static A Deserialize( string _sXml )
            {
                if( _sXml != string.Empty )
                {
                    XmlSerializer serializer = new XmlSerializer(typeof(A));
                    XmlReader xmlReader = XmlReader.Create(new StringReader(_sXml));
                    A instance = (A)serializer.Deserialize(xmlReader);
                    instance.AfterDeserialize();
                    return instance;
                }
                return null;
            }

            static protected T DeserializeXmlElement<T>( XmlElement xmlElement )
            {
                // Create an XmlSerializer for the specified type (T)
                XmlSerializer serializer = new XmlSerializer(typeof(T));

                // Create a StringReader to read the XmlElement's XML content
                using( StringReader stringReader = new StringReader(xmlElement.OuterXml) )
                {
                    // Deserialize the XML content into an object of type T
                    return (T)serializer.Deserialize(stringReader);
                }
            }

            public void AfterDeserialize()
            {
                foreach( XmlElement xmlElement in UnknownElements )
                {
                    if( xmlElement.Name == "TheFooString" )
                    {
                        try
                        {
                            string BorrowString = DeserializeXmlElement<string>(xmlElement);
                        }
                        catch( Exception ex )
                        {
                            Console.WriteLine( ex.ToString() ); 
                        }
                    }
                }
            }
        }
    }

I will get XML exception while trying

Foo.A foo= new Foo.A();
foo.TheFooString = "I'm from Foo.A";
string xml_foo = foo.ToXmlString();


Bar.A bar = Bar.A.Deserialize( xml_foo );


Solution

  • You don't share a full XML sample, but your problem appears to be that the name of the element containing your string value:

    <TheFooString>I'm from Foo.A</TheFooString>
    

    Does not match the root element name and namespace you get when you serialize a string with XmlSerializer, which happens to be <string> (demo fiddle #1 here):

    <string>I'm from Foo.A</string>
    

    To deserialize a string with a custom root element and namespace you will need to manufacture a custom serializer using the XmlSerializer(Type, XmlRootAttribute) constructor. But if you do, you will need to statically cache and reuse the serializer as explained in this answer by Marc Gravell to Memory Leak using StreamReader and XmlSerializer. To do this, first create the following extension methods:

    public static partial class XmlExtensions
    {
        public static T? Deserialize<T>(this XmlElement element, bool useElementName)
        {
            var serializer = useElementName 
                ? XmlSerializerFactory.Create(typeof(T), element.LocalName, element.NamespaceURI) 
                : new XmlSerializer(typeof(T));
            return element.Deserialize<T>(serializer);
        }
    
        public static T? Deserialize<T>(this XmlElement element, XmlSerializer? serializer = null)
        {
            using (var reader = new ProperXmlNodeReader(element))
                return reader.Deserialize<T>(serializer);
        }
    
        public static T? Deserialize<T>(this XmlReader reader, XmlSerializer? serializer = null)
            => (T?)(serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
    
        class ProperXmlNodeReader : XmlNodeReader
        {
            // Bug fix from this answer https://stackoverflow.com/a/30115691/3744182
            // By https://stackoverflow.com/users/8799/nathan-baulch
            // To https://stackoverflow.com/questions/30102275/deserialize-object-property-with-stringreader-vs-xmlnodereader
            // You may need to test whether this is still necessary, 
            public ProperXmlNodeReader(XmlNode node) : base(node) {}
            public override string? LookupNamespace(string prefix) => base.LookupNamespace(prefix) is {} ns ? NameTable.Add(ns) : null;
        }
    }
    
    public static class XmlSerializerFactory
    {
        // To avoid a memory leak the serializer must be cached.
        // https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
        // This factory taken from 
        // https://stackoverflow.com/questions/34128757/wrap-properties-with-cdata-section-xml-serialization-c-sharp/34138648#34138648
    
        readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache = new ();
        readonly static object padlock = new ();
    
        public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
        {
            if (serializedType == null)
                throw new ArgumentNullException();
            lock (padlock)
            {
                var key = Tuple.Create(serializedType, rootName, rootNamespace);
                if (!cache.TryGetValue(key, out var serializer))
                    cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
                return serializer;
            }
        }
    }
    

    And then modify your AfterDeserialize() method as follows:

    public void AfterDeserialize()
    {
        if (UnknownElements == null)
            return;
        for (var i = UnknownElements.Count-1; i >= 0; i--)
        {
            var xmlElement = UnknownElements[i];
            if( xmlElement.LocalName == "TheFooString" )
            {
                try
                {
                    var BorrowString = xmlElement.Deserialize<string>(useElementName : true);
                    // Postprocess BorrowString as required.  For testing purposes I'm setting TheBarString to the deserialized value:
                    TheBarString = BorrowString;
                    // Maybe you want to remove the obsolete xmlElement at this point?  If so, do:
                    UnknownElements.RemoveAt(i);
                }
                catch( Exception ex )
                {
                    Console.WriteLine( ex.ToString() ); 
                }
            }
        }
    }
    

    And you will be able to deserialize your unknown element to any required type inside your AfterDeserialize() method.

    Notes:

    • If you deserialization code is not working, one easy way to determine the problem is to serialize an instance of your model and compare the generated XML with your input XML. Any discrepancy is likely going to cause a bug. This is what I did when I serialized a string value and compared the result to your XML fragment.

    Demo fiddle #2 here.