Skip to content

ReflectionXmlSerializationWriter throws NullReferenceException when base class contains "ShouldSerialize" methods #120561

@SpazeDev

Description

@SpazeDev

Description

When using an XmlSerializer in ReflectionOnly mode (Running in iOS) to serialize an object of a type where the base class contains at least one "ShouldSerialize" method it thows an InvalidOperationException.

Reproduction Steps

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

namespace ConsoleTest
{
    public class TestBase
    {
        public string BaseProp { get; set; }

        public bool ShouldSerializeBaseProp() => false;
    }

    public class Test : TestBase
    {
        public string TestProp { get; set; }

        public bool ShouldSerializeTestProp() => false;
    }

    public static class Program
    {
        public static void Main(string[] args)
        {
            var serializer = new XmlSerializer(typeof(Test));

            var obj = new Test()
            {
                TestProp = "1",
                BaseProp = "2",
            };

            var output = new StringBuilder();
            using (var xmlWriter = XmlWriter.Create(output))
            {
                serializer.Serialize(xmlWriter, obj);
            }
        }
    }
}

Use the code above and force the XmlSerializer into "ReflectionOnly" mode by adding this to the csproj:

<ItemGroup>
	<RuntimeHostConfigurationOption Include="System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported" Value="false" />
</ItemGroup>

Expected behavior

Output contains an "empty" <Test /> XML

Actual behavior

This exception is thrown:

System.InvalidOperationException: There was an error generating the XML document.
 ---> System.NullReferenceException: Object reference not set to an instance of an object
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteStructMethod(StructMapping mapping, String n, String ns, Object o, Boolean isNullable, Boolean needType)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteElement(Object o, ElementAccessor element, Boolean writeAccessor)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteElements(Object o, Object choiceSource, ElementAccessor[] elements, TextAccessor text, ChoiceIdentifierAccessor choice, Boolean writeAccessors, Boolean isNullable)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteArrayItems(ElementAccessor[] elements, TextAccessor text, ChoiceIdentifierAccessor choice, Object o, Object choiceSources)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteElement(Object o, ElementAccessor element, Boolean writeAccessor)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteElements(Object o, Object choiceSource, ElementAccessor[] elements, TextAccessor text, ChoiceIdentifierAccessor choice, Boolean writeAccessors, Boolean isNullable)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteMember(Object o, Object choiceSource, ElementAccessor[] elements, TextAccessor text, ChoiceIdentifierAccessor choice, TypeDesc memberTypeDesc, Boolean writeAccessors)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteStructMethod(StructMapping mapping, String n, String ns, Object o, Boolean isNullable, Boolean needType)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteElement(Object o, ElementAccessor element, Boolean writeAccessor)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteElements(Object o, Object choiceSource, ElementAccessor[] elements, TextAccessor text, ChoiceIdentifierAccessor choice, Boolean writeAccessors, Boolean isNullable)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteMember(Object o, Object choiceSource, ElementAccessor[] elements, TextAccessor text, ChoiceIdentifierAccessor choice, TypeDesc memberTypeDesc, Boolean writeAccessors)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.GenerateTypeElement(Object o, XmlTypeMapping xmlMapping)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteObjectOfTypeElement(Object o, XmlTypeMapping mapping)
   at System.Xml.Serialization.ReflectionXmlSerializationWriter.WriteObject(Object o)
   at System.Xml.Serialization.XmlSerializer.SerializeUsingReflection(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id)
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle)
   at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces)

Regression?

Noticed this while porting an old Xamarin.Forms app to MAUI. It worked back then but not sure if that is relevant here.

Known Workarounds

Either migrate from ShouldSerialize methods to Specified properties:

public class TestBase
{
    public string BaseProp { get; set; }

    public bool BasePropSpecified => false;
}

Or also implement the ShouldSerialize method in derived types:

public class Test : TestBase
{
    public string TestProp { get; set; }

    public bool ShouldSerializeTestProp() => false;

    // Copied from base type to prevent serializer exception
    public new bool ShouldSerializeBaseProp() => false;
}

Configuration

Initially noticed this on net9.0-ios but is reproducible on other platforms.

Other information

I first tried to work around the problem by generating serializer assemblies but noticed that they are not used when dynamic code is not supported.
I understand why generation of new temporary assemblies is not supported in that scenario but why cant we load and use already generated assemblies?
Maybe adding a new mode PreGenOnlyWithReflectionAsBackup would be a good idea here?

Then after searching the XmlSerializer code I believe that the exception is thrown here:

if (m.CheckShouldPersist)
{
string methodInvoke = $"ShouldSerialize{m.Name}";
MethodInfo method = o!.GetType().GetMethod(methodInvoke, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)!;
shouldPersist = (bool)method.Invoke(o, Array.Empty<object>())!;
}

if (m.CheckShouldPersist)
{
string methodInvoke = $"ShouldSerialize{m.Name}";
MethodInfo method = o!.GetType().GetMethod(methodInvoke, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)!;
shouldPersist = (bool)method.Invoke(o, Array.Empty<object>())!;
}

The BindingFlags.DeclaredOnly specified here causes it to ignore everything on the base types therefor not finding the method and returning null.
When looking at the generated serializer assembly the check looks like this:

if (((TestBase)o).ShouldSerializeBaseProp())
{
    // Write Tag
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions