Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport XmlSerializer reflection changes to 8.0-staging #112320

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<RootNamespace>System.Xml</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableAOTAnalyzer>false</EnableAOTAnalyzer>
<IsPackable>true</IsPackable>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<ServicingVersion>1</ServicingVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1663,6 +1663,11 @@ void Wrapper(object? collection, object? collectionItems)
{
if (member.Source == null && mapping.TypeDesc.IsArrayLike && !(mapping.Elements!.Length == 1 && mapping.Elements[0].Mapping is ArrayMapping))
{
// Always create a collection for (non-array) collection-like types, even if the XML data says the collection should be null.
if (!mapping.TypeDesc.IsArray)
{
member.Collection ??= new CollectionMember();
}
member.Source = (item) =>
{
member.Collection ??= new CollectionMember();
Expand All @@ -1675,26 +1680,51 @@ void Wrapper(object? collection, object? collectionItems)

if (member.Source == null)
{
var isList = mapping.TypeDesc.IsArrayLike && !mapping.TypeDesc.IsArray;
var pi = member.Mapping.MemberInfo as PropertyInfo;
if (pi != null && typeof(IList).IsAssignableFrom(pi.PropertyType)
&& (pi.SetMethod == null || !pi.SetMethod.IsPublic))

// Here we have to deal with some special cases for property members. The old serializers would trip over
// private property setters generally - except in the case of list-like properties. Because lists get special
// treatment, a private setter for a list property would only be a problem if the default constructor didn't
// already create a list instance for the property. If it does create a list, then the serializer can still
// populate it. Try to emulate the old serializer behavior here.

// First, for non-list properties, private setters are always a problem.
if (!isList && pi != null && pi.SetMethod != null && !pi.SetMethod.IsPublic)
{
member.Source = (value) =>
member.Source = (value) => throw new InvalidOperationException(SR.Format(SR.XmlReadOnlyPropertyError, pi.Name, pi.DeclaringType!.FullName));
}

// Next, for list properties, we need to handle not only the private setter case, but also the case where
// there is no setter at all. Because we need to give the default constructor a chance to create the list
// first before we make noise about not being able to set a list property.
else if (isList && pi != null && (pi.SetMethod == null || !pi.SetMethod.IsPublic))
{
var addMethod = mapping.TypeDesc.Type!.GetMethod("Add");

if (addMethod != null)
{
var getOnlyList = (IList)pi.GetValue(o)!;
if (value is IList valueList)
member.Source = (value) =>
{
foreach (var v in valueList)
var getOnlyList = pi.GetValue(o)!;
if (getOnlyList == null)
{
getOnlyList.Add(v);
// No-setter lists should just be ignored if they weren't created by constructor. Private-setter lists are the noisy exception.
if (pi.SetMethod != null && !pi.SetMethod.IsPublic)
throw new InvalidOperationException(SR.Format(SR.XmlReadOnlyPropertyError, pi.Name, pi.DeclaringType!.FullName));
}
}
else
{
getOnlyList.Add(value);
}
};
else if (value is IEnumerable valueList)
{
foreach (var v in valueList)
{
addMethod.Invoke(getOnlyList, new object[] { v });
}
}
};
}
}

// For all other members (fields, public setter properties, etc), just carry on as normal
else
{
if (member.Mapping.Xmlns != null)
Expand All @@ -1710,6 +1740,21 @@ void Wrapper(object? collection, object? collectionItems)
member.Source = (value) => setterDelegate(o, value);
}
}

// Finally, special list handling again. ANY list that we can assign/populate should be initialized with
// an empty list if it hasn't been initialized already. Even if the XML data says the list should be null.
// This is an odd legacy behavior, but it's what the old serializers did.
if (isList && member.Source != null)
{
member.EnsureCollection = (obj) =>
{
if (GetMemberValue(obj, mapping.MemberInfo!) == null)
{
var empty = ReflectionCreateObject(mapping.TypeDesc.Type!);
member.Source(empty);
}
};
}
}

if (member.Mapping.CheckSpecified == SpecifiedAccessor.ReadWrite)
Expand Down Expand Up @@ -1763,23 +1808,29 @@ void Wrapper(object elementNameObject)
WriteAttributes(allMembers, anyAttribute, unknownNodeAction, ref o);

Reader.MoveToElement();

if (Reader.IsEmptyElement)
{
Reader.Skip();
return o;
}

Reader.ReadStartElement();
bool IsSequenceAllMembers = IsSequence();
if (IsSequenceAllMembers)
else
{
// https://github.com/dotnet/runtime/issues/1402:
// Currently the reflection based method treat this kind of type as normal types.
// But potentially we can do some optimization for types that have ordered properties.
}
Reader.ReadStartElement();
bool IsSequenceAllMembers = IsSequence();
if (IsSequenceAllMembers)
{
// https://github.com/dotnet/runtime/issues/1402:
// Currently the reflection based method treat this kind of type as normal types.
// But potentially we can do some optimization for types that have ordered properties.
}

WriteMembers(allMembers, unknownNodeAction, unknownNodeAction, anyElementMember, anyTextMember);
WriteMembers(allMembers, unknownNodeAction, unknownNodeAction, anyElementMember, anyTextMember);

ReadEndElement();
}

// Empty element or not, we need to ensure all our array-like members have been initialized in the same
// way as the IL / CodeGen - based serializers.
foreach (Member member in allMembers)
{
if (member.Collection != null)
Expand All @@ -1791,9 +1842,10 @@ void Wrapper(object elementNameObject)
var setMemberValue = GetSetMemberValueDelegate(o, memberInfo.Name);
setMemberValue(o, collection);
}

member.EnsureCollection?.Invoke(o!);
}

ReadEndElement();
return o;
}
}
Expand Down Expand Up @@ -2071,6 +2123,7 @@ internal sealed class Member
public Action<object?>? CheckSpecifiedSource;
public Action<object>? ChoiceSource;
public Action<string, string>? XmlnsSource;
public Action<object>? EnsureCollection;

public Member(MemberMapping mapping)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Xml.Schema;
Expand Down Expand Up @@ -125,7 +126,7 @@ private void WriteMember(object? o, object? choiceSource, ElementAccessor[] elem
}
else
{
WriteElements(o, elements, text, choice, writeAccessors, memberTypeDesc.IsNullable);
WriteElements(o, choiceSource, elements, text, choice, writeAccessors, memberTypeDesc.IsNullable);
}
}

Expand All @@ -150,11 +151,11 @@ private void WriteArray(object o, object? choiceSource, ElementAccessor[] elemen
}
}

WriteArrayItems(elements, text, choice, o);
WriteArrayItems(elements, text, choice, o, choiceSource);
}

[RequiresUnreferencedCode("calls WriteElements")]
private void WriteArrayItems(ElementAccessor[] elements, TextAccessor? text, ChoiceIdentifierAccessor? choice, object o)
private void WriteArrayItems(ElementAccessor[] elements, TextAccessor? text, ChoiceIdentifierAccessor? choice, object o, object? choiceSources)
{
var arr = o as IList;

Expand All @@ -163,7 +164,8 @@ private void WriteArrayItems(ElementAccessor[] elements, TextAccessor? text, Cho
for (int i = 0; i < arr.Count; i++)
{
object? ai = arr[i];
WriteElements(ai, elements, text, choice, true, true);
var choiceSource = ((Array?)choiceSources)?.GetValue(i);
WriteElements(ai, choiceSource, elements, text, choice, true, true);
}
}
else
Expand All @@ -174,17 +176,18 @@ private void WriteArrayItems(ElementAccessor[] elements, TextAccessor? text, Cho
IEnumerator e = a.GetEnumerator();
if (e != null)
{
int c = 0;
while (e.MoveNext())
{
object ai = e.Current;
WriteElements(ai, elements, text, choice, true, true);
var choiceSource = ((Array?)choiceSources)?.GetValue(c++);
WriteElements(ai, choiceSource, elements, text, choice, true, true);
}
}
}
}

[RequiresUnreferencedCode("calls CreateUnknownTypeException")]
private void WriteElements(object? o, ElementAccessor[] elements, TextAccessor? text, ChoiceIdentifierAccessor? choice, bool writeAccessors, bool isNullable)
private void WriteElements(object? o, object? choiceSource, ElementAccessor[] elements, TextAccessor? text, ChoiceIdentifierAccessor? choice, bool writeAccessors, bool isNullable)
{
if (elements.Length == 0 && text == null)
return;
Expand Down Expand Up @@ -222,16 +225,35 @@ private void WriteElements(object? o, ElementAccessor[] elements, TextAccessor?
}
else if (choice != null)
{
if (o != null && o.GetType() == element.Mapping!.TypeDesc!.Type)
// This looks heavy - getting names of enums in string form for comparison rather than just comparing values.
// But this faithfully mimics NetFx, and is necessary to prevent confusion between different enum types.
// ie EnumType.ValueX could == 1, but TotallyDifferentEnumType.ValueY could also == 1.
TypeDesc td = element.Mapping!.TypeDesc!;
bool enumUseReflection = choice.Mapping!.TypeDesc!.UseReflection;
string enumTypeName = choice.Mapping!.TypeDesc!.FullName;
string enumFullName = (enumUseReflection ? "" : enumTypeName + ".@") + FindChoiceEnumValue(element, (EnumMapping)choice.Mapping, enumUseReflection);
string choiceFullName = (enumUseReflection ? "" : choiceSource!.GetType().FullName + ".@") + choiceSource!.ToString();

if (choiceFullName == enumFullName)
{
WriteElement(o, element, writeAccessors);
return;
// Object is either non-null, or it is allowed to be null
if (o != null || (!isNullable || element.IsNullable))
{
// But if Object is non-null, it's got to match types
if (o != null && !td.Type!.IsAssignableFrom(o!.GetType()))
{
throw CreateMismatchChoiceException(td.FullName, choice.MemberName!, enumFullName);
}

WriteElement(o, element, writeAccessors);
return;
}
}
}
else
{
TypeDesc td = element.IsUnbounded ? element.Mapping!.TypeDesc!.CreateArrayTypeDesc() : element.Mapping!.TypeDesc!;
if (o!.GetType() == td.Type)
if (td.Type!.IsAssignableFrom(o!.GetType()))
{
WriteElement(o, element, writeAccessors);
return;
Expand Down Expand Up @@ -280,6 +302,58 @@ private void WriteElements(object? o, ElementAccessor[] elements, TextAccessor?
}
}

private static string FindChoiceEnumValue(ElementAccessor element, EnumMapping choiceMapping, bool useReflection)
{
string? enumValue = null;

for (int i = 0; i < choiceMapping.Constants!.Length; i++)
{
string xmlName = choiceMapping.Constants[i].XmlName;

if (element.Any && element.Name.Length == 0)
{
if (xmlName == "##any:")
{
if (useReflection)
enumValue = choiceMapping.Constants[i].Value.ToString(CultureInfo.InvariantCulture);
else
enumValue = choiceMapping.Constants[i].Name;
break;
}
continue;
}
int colon = xmlName.LastIndexOf(':');
string? choiceNs = colon < 0 ? choiceMapping.Namespace : xmlName.Substring(0, colon);
string choiceName = colon < 0 ? xmlName : xmlName.Substring(colon + 1);

if (element.Name == choiceName)
{
if ((element.Form == XmlSchemaForm.Unqualified && string.IsNullOrEmpty(choiceNs)) || element.Namespace == choiceNs)
{
if (useReflection)
enumValue = choiceMapping.Constants[i].Value.ToString(CultureInfo.InvariantCulture);
else
enumValue = choiceMapping.Constants[i].Name;
break;
}
}
}

if (string.IsNullOrEmpty(enumValue))
{
if (element.Any && element.Name.Length == 0)
{
// Type {0} is missing enumeration value '##any' for XmlAnyElementAttribute.
throw new InvalidOperationException(SR.Format(SR.XmlChoiceMissingAnyValue, choiceMapping.TypeDesc!.FullName));
}
// Type {0} is missing value for '{1}'.
throw new InvalidOperationException(SR.Format(SR.XmlChoiceMissingValue, choiceMapping.TypeDesc!.FullName, element.Namespace + ":" + element.Name, element.Name, element.Namespace));
}
if (!useReflection)
CodeIdentifier.CheckValidIdentifier(enumValue);
return enumValue;
}

private void WriteText(object o, TextAccessor text)
{
if (text.Mapping is PrimitiveMapping primitiveMapping)
Expand Down Expand Up @@ -376,7 +450,7 @@ private void WriteElement(object? o, ElementAccessor element, bool writeAccessor
if (o != null)
{
WriteStartElement(name, ns, false);
WriteArrayItems(mapping.ElementsSortedByDerivation!, null, null, o);
WriteArrayItems(mapping.ElementsSortedByDerivation!, null, null, o, null);
WriteEndElement();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,6 @@ public void Serialize(XmlWriter xmlWriter, object? o, XmlSerializerNamespaces? n
}
else if (_tempAssembly == null || _typedSerializer)
{
// The contion for the block is never true, thus the block is never hit.
XmlSerializationWriter writer = CreateWriter();
writer.Init(xmlWriter, namespaces == null || namespaces.Count == 0 ? DefaultNamespaces : namespaces, encodingStyle, id);
Serialize(o, writer);
Expand Down
Loading
Loading