Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
3 changes: 3 additions & 0 deletions src/libraries/System.Private.Xml/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2193,6 +2193,9 @@
<data name="XmlUnsupportedSoapTypeKind" xml:space="preserve">
<value>The type {0} may not be serialized with SOAP-encoded messages. Set the Use for your message to Literal.</value>
</data>
<data name="XmlObsoleteIsError" xml:space="preserve">
<value>Cannot serialize member with [Obsolete(IsError=true)]: {0}</value>
</data>
<data name="XmlUnsupportedIDictionary" xml:space="preserve">
<value>The type {0} is not supported because it implements IDictionary.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,15 @@ public static bool IsNetworkingEnabledByDefault
return SwitchesHelpers.GetCachedSwitchValue("System.Xml.XmlResolver.IsNetworkingEnabledByDefault", ref s_isNetworkingEnabledByDefault);
}
}

private static int s_ignoreObsoleteMembers;
public static bool IgnoreObsoleteMembers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return SwitchesHelpers.GetCachedSwitchValue("Switch.System.Xml.IgnoreObsoleteMembers", ref s_ignoreObsoleteMembers);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,28 @@ public SoapAttributes(ICustomAttributeProvider provider)
object[] attrs = provider.GetCustomAttributes(false);
for (int i = 0; i < attrs.Length; i++)
{
if (attrs[i] is SoapIgnoreAttribute || attrs[i] is ObsoleteAttribute)
if (attrs[i] is SoapIgnoreAttribute)
{
_soapIgnore = true;
break;
}
else if (attrs[i] is ObsoleteAttribute obsoleteAttr)
{
if (!System.Xml.LocalAppContextSwitches.IgnoreObsoleteMembers)
{
if (obsoleteAttr.IsError)
{
throw new InvalidOperationException(SR.Format(SR.XmlObsoleteIsError, obsoleteAttr.Message));
}
// If IsError is false, continue processing normally (don't ignore)
}
else
{
// Old behavior: ignore obsolete members when switch is enabled
_soapIgnore = true;
break;
}
}
else if (attrs[i] is SoapElementAttribute)
{
_soapElement = (SoapElementAttribute)attrs[i];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,28 @@ public XmlAttributes(ICustomAttributeProvider provider)
XmlAnyElementAttribute? wildcard = null;
for (int i = 0; i < attrs.Length; i++)
{
if (attrs[i] is XmlIgnoreAttribute || attrs[i] is ObsoleteAttribute)
if (attrs[i] is XmlIgnoreAttribute)
{
_xmlIgnore = true;
break;
}
else if (attrs[i] is ObsoleteAttribute obsoleteAttr)
{
if (!System.Xml.LocalAppContextSwitches.IgnoreObsoleteMembers)
{
if (obsoleteAttr.IsError)
{
throw new InvalidOperationException(SR.Format(SR.XmlObsoleteIsError, obsoleteAttr.Message));
}
// If IsError is false, continue processing normally (don't ignore)
}
else
{
// Old behavior: ignore obsolete members when switch is enabled
_xmlIgnore = true;
break;
}
}
else if (attrs[i] is XmlElementAttribute)
{
_xmlElements.Add((XmlElementAttribute)attrs[i]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,35 @@
using SerializationTypes;
using Xunit;

// Shared fixture to control AppContext switches needed by XmlSerializer tests that rely on
// disabling caching so switches can be evaluated per test scenario.
// Using a collection fixture gives us one-time setup/teardown instead of static constructor hacks.
public sealed class XmlSerializerSwitchFixture : IDisposable
{
private const string DisableCachingSwitch = "TestSwitch.LocalAppContext.DisableCaching";
private readonly bool _hadDisableCaching;

public XmlSerializerSwitchFixture()
{
// Ensure caching disabled for tests needing to manipulate switches mid-run.
_hadDisableCaching = AppContext.TryGetSwitch(DisableCachingSwitch, out bool wasDisableCaching) && wasDisableCaching;
AppContext.SetSwitch(DisableCachingSwitch, true);
}

public void Dispose()
{
// Restore original states (best effort; product code may have cached values earlier in static fields).
AppContext.SetSwitch(DisableCachingSwitch, _hadDisableCaching);
}
}

[CollectionDefinition("XmlSerializerSwitches")]
public sealed class XmlSerializerSwitchesCollection : ICollectionFixture<XmlSerializerSwitchFixture>
{
// Intentionally empty. This class' purpose is to apply the fixture to the named collection.
}

[Collection("XmlSerializerSwitches")]
#if !ReflectionOnly && !XMLSERIALIZERGENERATORTESTS
// Many test failures due to trimming and MakeGeneric. XmlSerializer is not currently supported with NativeAOT.
[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBuiltWithAggressiveTrimming))]
Expand Down Expand Up @@ -2365,6 +2394,111 @@ public static void ValidateXElementArray()
Assert.Equal("Member", retarray.xelements[1].Name);
}

#if !XMLSERIALIZERGENERATORTESTS
[Fact]
public static void ObsoleteAttribute_DoesNotAffectSerialization()
{
// Test that properties marked with [Obsolete(IsError=false)] are still serialized (not ignored like [XmlIgnore])
var testObject = new TypeWithObsoleteProperty
{
NormalProperty = "normal",
#pragma warning disable CS0618 // Type or member is obsolete
ObsoleteProperty = "obsolete",
#pragma warning restore CS0618 // Type or member is obsolete
IgnoredProperty = "ignored"
};

var result = SerializeAndDeserialize(testObject, $"""
<?xml version="1.0" encoding="utf-8"?>
<TypeWithObsoleteProperty xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<NormalProperty>normal</NormalProperty>
<ObsoleteProperty>obsolete</ObsoleteProperty>
</TypeWithObsoleteProperty>
""");

// Verify that the obsolete property was correctly roundtripped
Assert.Equal("normal", result.NormalProperty);
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal("obsolete", result.ObsoleteProperty);
#pragma warning restore CS0618 // Type or member is obsolete
// IgnoredProperty should remain null as it has [XmlIgnore]
Assert.Null(result.IgnoredProperty);
}

[Fact]
public static void ObsoleteAttribute_IsError_ThrowsException()
{
// Test that properties marked with [Obsolete(IsError=true)] throw an exception during serializer creation
var testObject = new TypeWithObsoleteErrorProperty
{
NormalProperty = "normal",
#pragma warning disable CS0618, CS0619 // Type or member is obsolete
ObsoleteProperty = "obsolete",
// This line would cause a compile-time error due to IsError=true. So we set this in the class definition instead.
// ObsoletePropertyWithError = "error",
#pragma warning restore CS0618, CS0619 // Type or member is obsolete
IgnoredProperty = "ignored"
};

// We need to create a type with just the error property to test the exception
// Using reflection to create an XmlSerializer for a type with ObsoletePropertyWithError
Assert.Throws<InvalidOperationException>(() =>
{
var serializer = new XmlSerializer(typeof(TypeWithObsoleteProperty));
var xml = Serialize(testObject, null, () => serializer, true);
});
}

[Fact]
public static void ObsoleteAttribute_WithAppContextSwitch_IgnoresObsoleteMembers()
{
// Enable compat switch
AppContext.TryGetSwitch("Switch.System.Xml.IgnoreObsoleteMembers", out bool originalSwitchValue);
AppContext.SetSwitch("Switch.System.Xml.IgnoreObsoleteMembers", true);

var expectedXml = $"""
<?xml version="1.0" encoding="utf-8"?>
<TypeWithObsoleteProperty xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<NormalProperty>normal</NormalProperty>
</TypeWithObsoleteProperty>
""";

try
{
// Test that when the switch is enabled, obsolete members are ignored (old behavior)
var testObject = new TypeWithObsoleteProperty
{
NormalProperty = "normal",
#pragma warning disable CS0618 // Type or member is obsolete
ObsoleteProperty = "obsolete",
#pragma warning restore CS0618 // Type or member is obsolete
IgnoredProperty = "ignored"
};

// With the switch enabled, obsolete properties should be ignored
var result = SerializeAndDeserialize(testObject, expectedXml);

// Try with the type that has Obsolete(IsError=true) property. It should still get ignored without throwing.
var testObjectWithError = new TypeWithObsoleteErrorProperty
{
NormalProperty = "normal",
#pragma warning disable CS0618, CS0619 // Type or member is obsolete
ObsoleteProperty = "obsolete",
// This line would cause a compile-time error due to IsError=true. So we set this in the class definition instead.
// ObsoletePropertyWithError = "error",
#pragma warning restore CS0618, CS0619 // Type or member is obsolete
IgnoredProperty = "ignored"
};

var resultWithError = SerializeAndDeserialize(testObjectWithError, expectedXml.Replace("TypeWithObsoleteProperty", "TypeWithObsoleteErrorProperty"));
}
finally
{
AppContext.SetSwitch("Switch.System.Xml.IgnoreObsoleteMembers", originalSwitchValue);
}
}
#endif

[MethodImpl(MethodImplOptions.NoInlining)]
private static void ExecuteAndUnload(string assemblyfile, string typename, out WeakReference wref)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,31 @@ public override int GetHashCode()
}
}

public class TypeWithObsoleteProperty
{
public string NormalProperty { get; set; }

[Obsolete("This property is obsolete but should still be serialized")]
public string ObsoleteProperty { get; set; }

[XmlIgnore]
public string IgnoredProperty { get; set; }
}

public class TypeWithObsoleteErrorProperty
{
public string NormalProperty { get; set; }

[Obsolete("This property is obsolete but should still be serialized")]
public string ObsoleteProperty { get; set; }

[Obsolete("This property is obsolete with error", true)]
public string ObsoletePropertyWithError { get; set; } = "error";

[XmlIgnore]
public string IgnoredProperty { get; set; }
}

public class BaseClassForInvalidDerivedClass
{
public int Id;
Expand Down
Loading