Skip to content

XmlSerializer has different behavior when deserializing an empty XML tag to a collection when DynamicCodeSupport is false #107252

Closed
@ivanpovazan

Description

@ivanpovazan

Description

As the title says, deserializing an empty XML tag to a collection gives different behavior when DynamicCodeSupport feature switch is enabled or disabled.

Considering the following code of a dotnet new console app:

using System.Xml.Serialization;
using System.Diagnostics.CodeAnalysis;

Test.TestSerialization("<MyClass></MyClass>");

class Test
{
    public static XmlSerializer GetSerializer([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type sourceType)
    {
        return new XmlSerializer(sourceType);
    }

    public static void TestSerialization(string input)
    {
        string msg = string.Empty;
        var serializer = GetSerializer(typeof(MyClass));
        using (StringReader reader = new StringReader(input))
        {
            MyClass? result = (MyClass?)serializer.Deserialize(reader);
            if (result!.Items is null)
            {
                msg += "Items is null";
            }
            else
            {
                msg += $"Items has {result.Items.Count} elements";
            }
        }
        Console.WriteLine(msg);
    }
}

public class MyClass
{
    public List<string>? Items { [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(List<>))] get; set; }
}
  • When built/run with -p:DynamicCodeSupport=true gives:
ivanpovazan@EONE-MAC-ARM64 ~/tmp/net8/CoreCLRXmlSerializer $ rm -rf bin obj ; dotnet run -p:DynamicCodeSupport=true 
Items has 0 elements
  • When built/run with -p:DynamicCodeSupport=false gives:
ivanpovazan@EONE-MAC-ARM64 ~/tmp/net8/CoreCLRXmlSerializer $ rm -rf bin obj ; dotnet run -p:DynamicCodeSupport=false
Items is null
  • Same behavior is observed with NativeAOT (which has DynamicCodeSupport=false set by default) giving:
ivanpovazan@EONE-MAC-ARM64 ~/tmp/net8/CoreCLRXmlSerializer $ rm -rf bin obj ; dotnet publish -r osx-arm64 -p:PublishAot=true
  Determining projects to restore...
  Restored /Users/ivan/tmp/net8/CoreCLRXmlSerializer/CoreCLRXmlSerializer.csproj (in 1.02 sec).
/Users/ivan/tmp/net8/CoreCLRXmlSerializer/Program.cs(20,41): warning IL2026: Using member 'System.Xml.Serialization.XmlSerializer.Deserialize(TextReader)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Members from deserialized types may be trimmed if not referenced directly. [/Users/ivan/tmp/net8/CoreCLRXmlSerializer/CoreCLRXmlSerializer.csproj]
/Users/ivan/tmp/net8/CoreCLRXmlSerializer/Program.cs(11,16): warning IL2026: Using member 'System.Xml.Serialization.XmlSerializer.XmlSerializer(Type)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Members from serialized types may be trimmed if not referenced directly. [/Users/ivan/tmp/net8/CoreCLRXmlSerializer/CoreCLRXmlSerializer.csproj]
  CoreCLRXmlSerializer -> /Users/ivan/tmp/net8/CoreCLRXmlSerializer/bin/Release/net8.0/osx-arm64/CoreCLRXmlSerializer.dll
  Generating native code
/Users/ivan/tmp/net8/CoreCLRXmlSerializer/Program.cs(20): Trim analysis warning IL2026: Test.TestSerialization(String): Using member 'System.Xml.Serialization.XmlSerializer.Deserialize(TextReader)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Members from deserialized types may be trimmed if not referenced directly. [/Users/ivan/tmp/net8/CoreCLRXmlSerializer/CoreCLRXmlSerializer.csproj]
/Users/ivan/tmp/net8/CoreCLRXmlSerializer/Program.cs(11): Trim analysis warning IL2026: Test.GetSerializer(Type): Using member 'System.Xml.Serialization.XmlSerializer.XmlSerializer(Type)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Members from serialized types may be trimmed if not referenced directly. [/Users/ivan/tmp/net8/CoreCLRXmlSerializer/CoreCLRXmlSerializer.csproj]
/Users/ivan/.nuget/packages/runtime.osx-arm64.microsoft.dotnet.ilcompiler/8.0.8/framework/System.Private.Xml.dll : warning IL3053: Assembly 'System.Private.Xml' produced AOT analysis warnings. [/Users/ivan/tmp/net8/CoreCLRXmlSerializer/CoreCLRXmlSerializer.csproj]
  CoreCLRXmlSerializer -> /Users/ivan/tmp/net8/CoreCLRXmlSerializer/bin/Release/net8.0/osx-arm64/publish/
ivanpovazan@EONE-MAC-ARM64 ~/tmp/net8/CoreCLRXmlSerializer $ bin/Release/net8.0/osx-arm64/publish/CoreCLRXmlSerializer
Items is null
  • More importantly, this specifically impacts mobile device scenarios with Mono where:
    • DynamicCodeSupport=true:

      • when JIT is available, which covers:
        • debug/release builds for .NET Android or .NET MAUI Android apps on both devices and simulators
      • when AOT is required and interpreter enabled, which covers:
        • debug builds for .NET MAUI iOS apps on both devices and simulators (hot reload enabled by default)
    • DynamicCodeSupport=false:

      • when AOT is required and interpreter disabled, which covers:
        • release builds for .NET iOS or .NET MAUI iOS apps on both devices and simulators
        • debug builds for .NET iOS apps on both devices and simulators (hot reload disabled by default)

Additional notes

Switching DynamicCodeSupport on and off changes the serialization mode, where when:

  • DynamicCodeSupport=true serialization mode is ReflectionAsBackup
  • DynamicCodeSupport=false serialization mode is ReflectionOnly

This was introduced in: b3ab2eb
I haven't looked deeper than this, but if I am not mistaken ReflectionAsBackup uses RefEmit which could be producing the different result.

Workaround

As mentioned in my comment one workaround could be to add an initalizer on problematic type members.

For reference: MAUI repro for mobile

Click to expand a repro example for a MAUI app

This is observable in .NET9 as well (tested with net9-p7).

  1. Create a new MAUI app:
  2. Exchange MainPage.xaml.cs with:
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
namespace MauiXmlSerial;

public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
	}

	private void OnCounterClicked(object sender, EventArgs e)
	{
		string msg;
		string xml = "<MyClass></MyClass>";

		XmlSerializer serializer = new XmlSerializer(typeof(MyClass));
		using (StringReader reader = new StringReader(xml))
		{
			MyClass result = (MyClass)serializer.Deserialize(reader);
			if (result.Items == null)
			{
				msg = "Items is null";
			}
			else
			{
				msg = $"Items have {result.Items.Count} elements";
			}
		}

		CounterBtn.Text = msg;

		SemanticScreenReader.Announce(CounterBtn.Text);
	}
}

public class MyClass
{
    public List<string> Items { get; set; }
}
  1. Build/run on iOS simulator with:
dotnet build -f net9.0-ios -t:Run
  1. Click the button on the startup page and observe that it shows: Items have 0 elements
  2. Build/run on a iOS device with:
dotnet build -c Release -f net9.0-ios -r ios-arm64 -t:Run
  1. Click the button on the startup page and observe that it shows: Items is null

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-Serializationin-prThere is an active PR which will close this issue when it is mergedos-iosApple iOS

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions