Search code examples
c#xml.net-8.0datacontractserializermemory-mapped-files

When deserializing XML from a memory mapped file, how can I fix "SerializationException: The data at the root level is invalid. Line 1, position 1"


When deserializing XML from a memory mapped file, I am getting below error while calling DataContractSerializer.ReadObject():

System.Runtime.Serialization.SerializationException: 'There was an error deserializing the object of type SampleClass. The data at the root level is invalid. Line 1, position 1.'

I previously wrote the XML to the file using DataContractSerializer and so it should be valid. How can I fix this?

Adding my sample code below:

using System.IO.MemoryMappedFiles;
using System.Runtime.Serialization;
using System.Runtime.Versioning;

namespace TestMigration
{
    public class Program
    {
        [SupportedOSPlatform("windows")]
        public static void Main(string[] args)
        {
            using (var mmf = MemoryMappedFile.CreateNew("TempName", 10240, MemoryMappedFileAccess.ReadWrite))
            {
                WriteToMemoryMappedFile(mmf);

                var infoObj = ReadFromMemoryMappedFile(mmf);

                Console.WriteLine(infoObj?.Name);
            }

        }
        private static void WriteToMemoryMappedFile(MemoryMappedFile mmf)
        {
            var serializer = new DataContractSerializer(typeof(SampleClass));

            var infoObj = new SampleClass { Name = "TestApp" };

            using (var stream = mmf.CreateViewStream(0, 0, MemoryMappedFileAccess.Write))
            {
                serializer.WriteObject(stream, infoObj);
                stream.Flush();
            }
        }

        private static SampleClass? ReadFromMemoryMappedFile(MemoryMappedFile mmf)
        {
            var serializer = new DataContractSerializer(typeof(SampleClass));

            using (var stream = mmf.CreateViewStream(0, 0, MemoryMappedFileAccess.Read))
            {
                stream.Position = 0;
                return (SampleClass?)serializer.ReadObject(stream);
            }
        }
    }
}

[DataContract]
public class SampleClass
{
    [DataMember]
    public string? Name { get; set; }
}

Please do not mark this as duplicate, I tried adding workarounds such as using XmlReader, XmlDictionaryReader etc as suggested in other questions but didn't work out.


Solution

  • Your problem is that, one created, memory mapped files have a fixed size [1]. Therefore, after you write your XML to your view stream, the file will contain your XML plus enough uninitialized bytes to pad the length out to the fixed length of 10240. You can confirm this by printing the value of stream.Length in WriteToMemoryMappedFile():

    using (var stream = mmf.CreateViewStream(0, 0, MemoryMappedFileAccess.Write))
    {
        Debug.WriteLine(stream.Length); // Prints 10240
    

    Because of the trailing padding, your memory mapped file is a malformed XML document. And the XmlReader created internally by DataContractSerializer.ReadObject() is designed to verify that by reading past the end of the root object to ensure that the entire file is in fact well-formed. Since it isn't, you get the exception you see.

    So what are your options for a workaround?

    Firstly, you could treat your memory mapped file as a sequence of XML fragments, and read only the first fragment. First introduce the following extension method:

    public static partial class DataContractSerializerExtensions
    {
        public static IEnumerable<T?> ReadObjectFragments<T>(Stream stream, DataContractSerializer? serializer = null, bool closeInput = true)
        {
            var settings = new XmlReaderSettings
            {
                ConformanceLevel = ConformanceLevel.Fragment,
                CloseInput = closeInput,
            };
            serializer ??= new DataContractSerializer(typeof(T));
            using (var outerReader = XmlReader.Create(stream, settings))
            {
                while (outerReader.Read())
                {   // Skip whitespace
                    if (outerReader.NodeType == XmlNodeType.Element)
                        using (var innerReader = outerReader.ReadSubtree())
                        {
                            yield return (T?)serializer.ReadObject(innerReader);
                        }
                }
            }
        }
    }
    

    And then modify ReadFromMemoryMappedFile() as follows:

    private static SampleClass? ReadFromMemoryMappedFile(MemoryMappedFile mmf)
    {
        using (var stream = mmf.CreateViewStream(0, 0, MemoryMappedFileAccess.Read))
        {
            return DataContractSerializerExtensions.ReadObjectFragments<SampleClass>(stream).FirstOrDefault();
        }
    }
    

    Your SampleClass will now be deserialized successfully, as the call to FirstOrDefault() combined with the use of ConformanceLevel.Fragment prevents the underlying XmlReader from attempting to read beyond the initial fragment.

    Demo fiddle #1 here.

    Secondly, since your memory mapped file isn't well-formed XML anyway, you could consider adopting a message framing approach by writing the actual data size before the XML content.

    To do that, first create the following generic methods:

    public static partial class MemoryMappedFileExtensions
    {
        static readonly int LongSize = Marshal.SizeOf<long>();
    
        public static long WriteAndFrame<T>(this MemoryMappedFile mmf, T value, long offset = 0, DataContractSerializer? serializer = null)
        {
            long actualSize;
            serializer ??= new DataContractSerializer(typeof(T));
            
            // Write the data contract data at offset 8.
            using (var stream = mmf.CreateViewStream(checked(offset + LongSize), 0, MemoryMappedFileAccess.Write))
            {
                var startPosition = stream.Position;
                serializer.WriteObject(stream, value);
                stream.Flush();
                actualSize = stream.Position - startPosition;
            }
            
            // Write the message frame size at offset 0.
            using (var accessor = mmf.CreateViewAccessor(offset, LongSize))
            {
                accessor.Write(0, actualSize);
            }
            
            return checked(actualSize + LongSize);
        }
        
        public static (T? Value, long SizeRead) ReadFromFrame<T>(this MemoryMappedFile mmf, long offset = 0, DataContractSerializer? serializer = null)
        {
            long actualSize;
            serializer ??= new DataContractSerializer(typeof(T));
    
            // Read the message frame size from offset zero.
            using (var accessor = mmf.CreateViewAccessor(offset, LongSize))
            {
                accessor.Read(0, out actualSize);
            }
    
            // Read the XML starting at offset 8 with the specified message frame size.
            using (var stream = mmf.CreateViewStream(offset + LongSize, actualSize, MemoryMappedFileAccess.Read))
            {
                return ((T?)serializer.ReadObject(stream), checked(actualSize + LongSize));
            }
        }
    }
    

    The methods return the total number of bytes written or read in case you want to write multiple objects to the file. Now your read and write methods can be rewritten as follows:

    private static void WriteToMemoryMappedFile(MemoryMappedFile mmf)
    {
        var infoObj = new SampleClass { Name = "TestApp" };
        
        mmf.WriteAndFrame(infoObj);
    }
    
    private static SampleClass? ReadFromMemoryMappedFile(MemoryMappedFile mmf) =>
        mmf.ReadFromFrame<SampleClass>().Value;
    

    Demo fiddle #2 here.

    Finally, you might consider whether your approach of using memory mapped files to hold a single XML document is ideal, given that the file size must be fixed in advance.


    [1]See How to dynamically expand a Memory Mapped File for a discussion.