Closed
Description
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)
- when JIT is available, which covers:
-
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)
- when AOT is required and interpreter disabled, which covers:
-
Additional notes
Switching DynamicCodeSupport
on and off changes the serialization mode, where when:
DynamicCodeSupport=true
serialization mode isReflectionAsBackup
DynamicCodeSupport=false
serialization mode isReflectionOnly
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).
- Create a new MAUI app:
- 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; }
}
- Build/run on iOS simulator with:
dotnet build -f net9.0-ios -t:Run
- Click the button on the startup page and observe that it shows:
Items have 0 elements
- Build/run on a iOS device with:
dotnet build -c Release -f net9.0-ios -r ios-arm64 -t:Run
- Click the button on the startup page and observe that it shows:
Items is null