Skip to content

DataContractSerializer bug when serializing nullable structs in .net >= 8.0 #126873

@antmjones

Description

@antmjones

Description

In .NET >= 8, When a DataContractSerializer is used to serialize a nullable custom value type (Nullable<T>) and the corresponding type T has not previously been seen by a DataContractSerializer, an exception is thrown:

System.Runtime.Serialization.SerializationException: An internal error has occurred. DataContract cache overflow.
   at System.Runtime.Serialization.DataContracts.DataContract.DataContractCriticalHelper.GetIdForInitialization(ClassDataContract classContract)
   at System.Runtime.Serialization.XmlFormatReaderGenerator.CriticalHelper.CreateObject(ClassDataContract classContract)
   at System.Runtime.Serialization.XmlFormatReaderGenerator.CriticalHelper.GenerateClassReader(ClassDataContract classContract)
   at System.Runtime.Serialization.DataContracts.ClassDataContract.get_XmlFormatReaderDelegate()
   at System.Runtime.Serialization.DataContracts.ClassDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator reader, String name, String ns, Type declaredType, DataContract& dataContract)
   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator xmlReader, Type declaredType, DataContract dataContract, String name, String ns)
   at System.Runtime.Serialization.DataContractSerializer.InternalReadObject(XmlReaderDelegator xmlReader, Boolean verifyObjectName, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(XmlReaderDelegator reader, Boolean verifyObjectName, DataContractResolver dataContractResolver)

This appears to be a bug introduce in the following commit:

adaebca#diff-343a32c8e8d13ca319b989de56100d9b829145aa2f7be2908f814e78cbe12b6eL378

Specifically, the commit changed line 378 of DataContract.cs from:

if (ContractMatches(classContract, s_dataContractCache[i]))

to

if (ContractMatches(classContract, s_dataContractCache.GetItem(id)))

Note the change from i to id as the argument to GetItem(), which appears to be inadvertent.

Reproduction Steps

The following code works as expected (no exception thrown) when targetting .NET 6, but fails with an exception in .NET 8/9/10:

using System.Runtime.Serialization;
using System.Text;
using System.Xml;

namespace DataContractSerializerTest {
    internal class Program {
        [DataContract]
        private struct TestStruct1 {
        }

        static void Main(string[] args) {
            // if this line is uncommented , the code compiles and runs without any exceptions.
            // If it is commented out, an exception is thrown.
            //SerializeAndDeserialize<TestStruct1>(default(TestStruct1));
            SerializeAndDeserialize<TestStruct1?>(default(TestStruct1));
        }

        private static T? SerializeAndDeserialize<T>(T? obj) {
            DataContractSerializer serializer = new DataContractSerializer(typeof(T));

            StringBuilder stringBuilder = new StringBuilder();

            using (XmlWriter writer = XmlWriter.Create(stringBuilder)) {
                serializer.WriteObject(writer, obj);
            }

            using (XmlReader reader = XmlReader.Create(new StringReader(stringBuilder.ToString()))) {
                return (T?)serializer.ReadObject(reader);
            }
        }
    }
}

Expected behavior

Nullable<T> where T is a value type marked with [DataContract] can be serialized successfully as it was in .NET < 8.

Actual behavior

An exception is thrown:

System.Runtime.Serialization.SerializationException: An internal error has occurred. DataContract cache overflow.
   at System.Runtime.Serialization.DataContracts.DataContract.DataContractCriticalHelper.GetIdForInitialization(ClassDataContract classContract)
   at System.Runtime.Serialization.XmlFormatReaderGenerator.CriticalHelper.CreateObject(ClassDataContract classContract)
   at System.Runtime.Serialization.XmlFormatReaderGenerator.CriticalHelper.GenerateClassReader(ClassDataContract classContract)
   at System.Runtime.Serialization.DataContracts.ClassDataContract.get_XmlFormatReaderDelegate()
   at System.Runtime.Serialization.DataContracts.ClassDataContract.ReadXmlValue(XmlReaderDelegator xmlReader, XmlObjectSerializerReadContext context)
   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator reader, String name, String ns, Type declaredType, DataContract& dataContract)
   at System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(XmlReaderDelegator xmlReader, Type declaredType, DataContract dataContract, String name, String ns)
   at System.Runtime.Serialization.DataContractSerializer.InternalReadObject(XmlReaderDelegator xmlReader, Boolean verifyObjectName, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(XmlReaderDelegator reader, Boolean verifyObjectName, DataContractResolver dataContractResolver)

Regression?

Yes, appears to work correctly in .NET <8, and appears to have been introduced in the commit adaebca#diff-343a32c8e8d13ca319b989de56100d9b829145aa2f7be2908f814e78cbe12b6eL378

Known Workarounds

Use a DataContractSerializer to serialize the non-nullable struct T before using it to serialize Nullable<T>.

Configuration

.NET 10, Windows 11, x86.

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions