From 19b1b486258d5f3ff498919670b8e68f700b2819 Mon Sep 17 00:00:00 2001 From: Steve Molloy Date: Thu, 15 Jul 2021 13:37:37 -0700 Subject: [PATCH] XmlSerializer dont skip date time offset (#55101) * Add DateTimeOffset as a primitive for XmlSerializer. * Fix DefaultValueAttribute handling for DTO. * Add tests for DateTimeAttribute and XmlSerializer. * Missed a 'KnownType' spot. * Update SGen tests to use current dotnet.exe from build. * Unbreak runtimeconfig creation for testing. * Debugging failure to resolve CoreCLR path. * Test fixup. * More test cleanup. * Temporarily disable generator tests. * Use InitObj instead of constructors for DateTimeOffset and TimeSpan. --- ...osoft.XmlSerializer.Generator.Tests.csproj | 1 + .../System/Xml/Serialization/CodeGenerator.cs | 13 ++++ .../Serialization/PrimitiveXmlSerializers.cs | 44 +++++++++++ .../ReflectionXmlSerializationReader.cs | 6 ++ .../ReflectionXmlSerializationWriter.cs | 1 + .../src/System/Xml/Serialization/Types.cs | 3 + .../Serialization/XmlSerializationReader.cs | 17 +++- .../XmlSerializationReaderILGen.cs | 14 ++-- .../Serialization/XmlSerializationWriter.cs | 23 ++++++ .../System/Xml/Serialization/XmlSerializer.cs | 8 ++ .../tests/XmlSerializer/XmlSerializerTests.cs | 78 +++++++++++++++++++ .../tests/SerializationTypes.cs | 16 ++++ 12 files changed, 213 insertions(+), 11 deletions(-) diff --git a/src/libraries/Microsoft.XmlSerializer.Generator/tests/Microsoft.XmlSerializer.Generator.Tests.csproj b/src/libraries/Microsoft.XmlSerializer.Generator/tests/Microsoft.XmlSerializer.Generator.Tests.csproj index 10a6631611e07a..23f6b79d7626eb 100644 --- a/src/libraries/Microsoft.XmlSerializer.Generator/tests/Microsoft.XmlSerializer.Generator.Tests.csproj +++ b/src/libraries/Microsoft.XmlSerializer.Generator/tests/Microsoft.XmlSerializer.Generator.Tests.csproj @@ -3,6 +3,7 @@ $(DefineConstants);XMLSERIALIZERGENERATORTESTS $(NetCoreAppCurrent) true + true diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cs index ecd33f31633241..8ce0a5cc9aae13 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cs @@ -853,6 +853,19 @@ internal void Ldc(object o) New(TimeSpan_ctor); break; } + else if (valueType == typeof(DateTimeOffset)) + { + ConstructorInfo DateTimeOffset_ctor = typeof(DateTimeOffset).GetConstructor( + CodeGenerator.InstanceBindingFlags, + null, + new Type[] { typeof(long), typeof(TimeSpan) }, + null + )!; + Ldc(((DateTimeOffset)o).Ticks); // ticks + Ldc(((DateTimeOffset)o).Offset); // offset + New(DateTimeOffset_ctor); + break; + } else { throw new NotSupportedException(SR.Format(SR.UnknownConstantType, valueType.AssemblyQualifiedName)); diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/PrimitiveXmlSerializers.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/PrimitiveXmlSerializers.cs index e06311de6d61c9..ea5827445f04d7 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/PrimitiveXmlSerializers.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/PrimitiveXmlSerializers.cs @@ -110,6 +110,18 @@ internal void Write_dateTime(object? o) WriteElementStringRaw(@"dateTime", @"", FromDateTime(((System.DateTime)o))); } + internal void Write_dateTimeOffset(object? o) + { + WriteStartDocument(); + if (o == null) + { + WriteEmptyTag(@"dateTimeOffset", @""); + return; + } + DateTimeOffset dto = (DateTimeOffset)o; + WriteElementStringRaw(@"dateTimeOffset", @"", System.Xml.XmlConvert.ToString(dto)); + } + internal void Write_unsignedByte(object? o) { WriteStartDocument(); @@ -454,6 +466,36 @@ internal sealed class XmlSerializationPrimitiveReader : System.Xml.Serialization return (object?)o; } + internal object? Read_dateTimeOffset() + { + object? o = null; + Reader.MoveToContent(); + if (Reader.NodeType == System.Xml.XmlNodeType.Element) + { + if (((object)Reader.LocalName == (object)_id20_dateTimeOffset && (object)Reader.NamespaceURI == (object)_id2_Item)) + { + if (Reader.IsEmptyElement) + { + Reader.Skip(); + o = default(DateTimeOffset); + } + else + { + o = System.Xml.XmlConvert.ToDateTimeOffset(Reader.ReadElementString()); + } + } + else + { + throw CreateUnknownNodeException(); + } + } + else + { + UnknownNode(null); + } + return (object?)o; + } + internal object? Read_unsignedByte() { object? o = null; @@ -720,6 +762,7 @@ protected override void InitCallbacks() private string _id15_unsignedLong = null!; private string _id7_float = null!; private string _id10_dateTime = null!; + private string _id20_dateTimeOffset = null!; private string _id6_long = null!; private string _id9_decimal = null!; private string _id8_double = null!; @@ -743,6 +786,7 @@ protected override void InitIDs() _id15_unsignedLong = Reader.NameTable.Add(@"unsignedLong"); _id7_float = Reader.NameTable.Add(@"float"); _id10_dateTime = Reader.NameTable.Add(@"dateTime"); + _id20_dateTimeOffset = Reader.NameTable.Add(@"dateTimeOffset"); _id6_long = Reader.NameTable.Add(@"long"); _id9_decimal = Reader.NameTable.Add(@"decimal"); _id8_double = Reader.NameTable.Add(@"double"); diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs index eca311b4d31d9a..0c55638a27a66a 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs @@ -884,6 +884,11 @@ private void WriteMemberElementsIf(Member[] expectedMembers, Member? anyElementM Reader.Skip(); value = default(TimeSpan); } + else if (element.Mapping.TypeDesc!.Type == typeof(DateTimeOffset) && Reader.IsEmptyElement) + { + Reader.Skip(); + value = default(DateTimeOffset); + } else { if (element.Mapping.TypeDesc == QnameTypeDesc) @@ -1219,6 +1224,7 @@ private object WritePrimitive(TypeMapping mapping, Func readFunc "Guid" => XmlConvert.ToGuid(value), "Char" => XmlConvert.ToChar(value), "TimeSpan" => XmlConvert.ToTimeSpan(value), + "DateTimeOffset" => XmlConvert.ToDateTimeOffset(value), _ => throw new InvalidOperationException(SR.Format(SR.XmlInternalErrorDetails, $"unknown FormatterName: {mapping.TypeDesc.FormatterName}")), }; return retObj; diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs index c40076aa83861b..7969b74134e963 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs @@ -1206,6 +1206,7 @@ private string ConvertPrimitiveToString(object o, TypeDesc typeDesc) "Guid" => XmlConvert.ToString((Guid)o), "Char" => XmlConvert.ToString((char)o), "TimeSpan" => XmlConvert.ToString((TimeSpan)o), + "DateTimeOffset" => XmlConvert.ToString((DateTimeOffset)o), _ => o.ToString()!, }; return stringValue; diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/Types.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/Types.cs index 1971544633bf65..32373c9d0fce7d 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/Types.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/Types.cs @@ -550,6 +550,7 @@ static TypeScope() AddNonXsdPrimitive(typeof(Guid), "guid", UrtTypes.Namespace, "Guid", new XmlQualifiedName("string", XmlSchema.Namespace), new XmlSchemaFacet[] { guidPattern }, TypeFlags.CanBeAttributeValue | TypeFlags.CanBeElementValue | TypeFlags.XmlEncodingNotRequired | TypeFlags.IgnoreDefault); AddNonXsdPrimitive(typeof(char), "char", UrtTypes.Namespace, "Char", new XmlQualifiedName("unsignedShort", XmlSchema.Namespace), Array.Empty(), TypeFlags.CanBeAttributeValue | TypeFlags.CanBeElementValue | TypeFlags.HasCustomFormatter | TypeFlags.IgnoreDefault); AddNonXsdPrimitive(typeof(TimeSpan), "TimeSpan", UrtTypes.Namespace, "TimeSpan", new XmlQualifiedName("duration", XmlSchema.Namespace), Array.Empty(), TypeFlags.CanBeAttributeValue | TypeFlags.CanBeElementValue | TypeFlags.XmlEncodingNotRequired); + AddNonXsdPrimitive(typeof(DateTimeOffset), "dateTimeOffset", UrtTypes.Namespace, "DateTimeOffset", new XmlQualifiedName("dateTime", XmlSchema.Namespace), Array.Empty(), TypeFlags.CanBeAttributeValue | TypeFlags.CanBeElementValue | TypeFlags.XmlEncodingNotRequired); AddSoapEncodedTypes(Soap.Encoding); @@ -596,6 +597,8 @@ internal static bool IsKnownType(Type type) return true; else if (type == typeof(TimeSpan)) return true; + else if (type == typeof(DateTimeOffset)) + return true; else if (type == typeof(XmlNode[])) return true; break; diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs index 931735f60c6e67..0ea4307b42ad15 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs @@ -105,6 +105,7 @@ public abstract class XmlSerializationReader : XmlSerializationGeneratedCode private string _charID = null!; private string _guidID = null!; private string _timeSpanID = null!; + private string _dateTimeOffsetID = null!; protected abstract void InitIDs(); @@ -214,6 +215,7 @@ private void InitPrimitiveIDs() _charID = _r.NameTable.Add("char"); _guidID = _r.NameTable.Add("guid"); _timeSpanID = _r.NameTable.Add("TimeSpan"); + _dateTimeOffsetID = _r.NameTable.Add("dateTimeOffset"); _base64ID = _r.NameTable.Add("base64"); _anyURIID = _r.NameTable.Add("anyURI"); @@ -667,6 +669,8 @@ private byte[] ReadByteArray(bool isBase64) value = new Guid(CollapseWhitespace(ReadStringValue())); else if ((object)type.Name == (object)_timeSpanID) value = XmlConvert.ToTimeSpan(ReadStringValue()); + else if ((object)type.Name == (object)_dateTimeOffsetID) + value = XmlConvert.ToDateTimeOffset(ReadStringValue()); else value = ReadXmlNodes(elementCanBeType); } @@ -764,6 +768,8 @@ private byte[] ReadByteArray(bool isBase64) value = default(Nullable); else if ((object)type.Name == (object)_timeSpanID) value = default(Nullable); + else if ((object)type.Name == (object)_dateTimeOffsetID) + value = default(Nullable); else value = null; } @@ -4700,13 +4706,20 @@ private void WriteElement(string source, string? arrayName, string? choiceSource } Writer.Indent++; - if (element.Mapping.TypeDesc!.Type == typeof(TimeSpan)) + if (element.Mapping.TypeDesc!.Type == typeof(TimeSpan) || element.Mapping.TypeDesc!.Type == typeof(DateTimeOffset)) { Writer.WriteLine("if (Reader.IsEmptyElement) {"); Writer.Indent++; Writer.WriteLine("Reader.Skip();"); WriteSourceBegin(source); - Writer.Write("default(System.TimeSpan)"); + if (element.Mapping.TypeDesc!.Type == typeof(TimeSpan)) + { + Writer.Write("default(System.TimeSpan)"); + } + else if (element.Mapping.TypeDesc!.Type == typeof(DateTimeOffset)) + { + Writer.Write("default(System.DateTimeOffset)"); + } WriteSourceEnd(source); Writer.WriteLine(";"); Writer.Indent--; diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs index 41c5c83506d752..73af82643af97d 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs @@ -3096,7 +3096,7 @@ private void WriteElement(string source, string? arrayName, string? choiceSource { } - if (element.Mapping.TypeDesc!.Type == typeof(TimeSpan)) + if ((element.Mapping.TypeDesc!.Type == typeof(TimeSpan)) || element.Mapping.TypeDesc!.Type == typeof(DateTimeOffset)) { MethodInfo XmlSerializationReader_get_Reader = typeof(XmlSerializationReader).GetMethod( "get_Reader", @@ -3121,14 +3121,10 @@ private void WriteElement(string source, string? arrayName, string? choiceSource ilg.Ldarg(0); ilg.Call(XmlSerializationReader_get_Reader); ilg.Call(XmlReader_Skip); - ConstructorInfo TimeSpan_ctor = typeof(TimeSpan).GetConstructor( - CodeGenerator.InstanceBindingFlags, - null, - new Type[] { typeof(long) }, - null - )!; - ilg.Ldc(default(TimeSpan).Ticks); - ilg.New(TimeSpan_ctor); + LocalBuilder tmpLoc = ilg.GetTempLocal(element.Mapping.TypeDesc!.Type); + ilg.Ldloca(tmpLoc); + ilg.InitObj(element.Mapping.TypeDesc!.Type); + ilg.Ldloc(tmpLoc); WriteSourceEnd(source, element.Mapping.TypeDesc.Type); ilg.Else(); WriteSourceBegin(source); diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs index 8308e0de3864ef..25bac35aea2ca7 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs @@ -227,6 +227,11 @@ private XmlQualifiedName GetPrimitiveTypeName(Type type) typeName = "TimeSpan"; typeNs = UrtTypes.Namespace; } + else if (type == typeof(DateTimeOffset)) + { + typeName = "dateTimeOffset"; + typeNs = UrtTypes.Namespace; + } else if (type == typeof(XmlNode[])) { typeName = Soap.UrType; @@ -345,6 +350,12 @@ protected void WriteTypedPrimitive(string? name, string? ns, object o, bool xsiT type = "TimeSpan"; typeNs = UrtTypes.Namespace; } + else if (t == typeof(DateTimeOffset)) + { + value = XmlConvert.ToString((DateTimeOffset)o); + type = "dateTimeOffset"; + typeNs = UrtTypes.Namespace; + } else if (typeof(XmlNode[]).IsAssignableFrom(t)) { if (name == null) @@ -4321,6 +4332,18 @@ private void WriteValue(object value) Writer.Write(((DateTime)value).Ticks.ToString(CultureInfo.InvariantCulture)); Writer.Write(")"); } + else if (type == typeof(DateTimeOffset)) + { + Writer.Write(" new "); + Writer.Write(type.FullName); + Writer.Write("("); + Writer.Write(((DateTimeOffset)value).Ticks.ToString(CultureInfo.InvariantCulture)); + Writer.Write(", new "); + Writer.Write(((DateTimeOffset)value).Offset.GetType().FullName); + Writer.Write("("); + Writer.Write(((DateTimeOffset)value).Offset.Ticks.ToString(CultureInfo.InvariantCulture)); + Writer.Write("))"); + } else if (type == typeof(TimeSpan)) { Writer.Write(" new "); diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs index f0310fa2abb5e8..049836211d7522 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializer.cs @@ -893,6 +893,10 @@ private void SerializePrimitive(XmlWriter xmlWriter, object? o, XmlSerializerNam { writer.Write_TimeSpan(o); } + else if (_primitiveType == typeof(DateTimeOffset)) + { + writer.Write_dateTimeOffset(o); + } else { throw new InvalidOperationException(SR.Format(SR.XmlUnxpectedType, _primitiveType!.FullName)); @@ -971,6 +975,10 @@ private void SerializePrimitive(XmlWriter xmlWriter, object? o, XmlSerializerNam { o = reader.Read_TimeSpan(); } + else if (_primitiveType == typeof(DateTimeOffset)) + { + o = reader.Read_dateTimeOffset(); + } else { throw new InvalidOperationException(SR.Format(SR.XmlUnxpectedType, _primitiveType!.FullName)); diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs index 6df78b2ab9049a..00011463d1e79d 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs @@ -751,6 +751,84 @@ public static void Xml_DeserializeEmptyTimeSpanType() } } + [Fact] + public static void Xml_TypeWithDateTimeOffsetProperty() + { + var now = new DateTimeOffset(DateTime.Now); + var defDTO = default(DateTimeOffset); + var obj = new TypeWithDateTimeOffsetProperties { DTO = now }; + var deserializedObj = SerializeAndDeserialize(obj, +@" + + " + XmlConvert.ToString(now) + @" + " + XmlConvert.ToString(defDTO) + @" + + +"); + Assert.StrictEqual(obj.DTO, deserializedObj.DTO); + Assert.StrictEqual(obj.DTO2, deserializedObj.DTO2); + Assert.StrictEqual(defDTO, deserializedObj.DTO2); + Assert.StrictEqual(obj.DTOWithDefault, deserializedObj.DTOWithDefault); + Assert.StrictEqual(defDTO, deserializedObj.DTOWithDefault); + Assert.StrictEqual(obj.NullableDTO, deserializedObj.NullableDTO); + Assert.True(deserializedObj.NullableDTO == null); + Assert.StrictEqual(obj.NullableDTOWithDefault, deserializedObj.NullableDTOWithDefault); + Assert.True(deserializedObj.NullableDTOWithDefault == null); + } + + [Fact] + public static void Xml_DeserializeTypeWithEmptyDateTimeOffsetProperties() + { + //var def = DateTimeOffset.Parse("3/17/1977 5:00:01 PM -05:00"); // "1977-03-17T17:00:01-05:00" + var defDTO = default(DateTimeOffset); + string xml = @" + + + + + + "; + XmlSerializer serializer = new XmlSerializer(typeof(TypeWithDateTimeOffsetProperties)); + + using (StringReader reader = new StringReader(xml)) + { + TypeWithDateTimeOffsetProperties deserializedObj = (TypeWithDateTimeOffsetProperties)serializer.Deserialize(reader); + Assert.NotNull(deserializedObj); + Assert.Equal(defDTO, deserializedObj.DTO); + Assert.Equal(defDTO, deserializedObj.DTO2); + Assert.Equal(defDTO, deserializedObj.DTOWithDefault); + Assert.True(deserializedObj.NullableDTO == null); + Assert.Equal(defDTO, deserializedObj.NullableDTOWithDefault); + } + } + + [Fact] + public static void Xml_DeserializeDateTimeOffsetType() + { + var now = new DateTimeOffset(DateTime.Now); + string xml = @"" + now.ToString("o") + ""; + XmlSerializer serializer = new XmlSerializer(typeof(DateTimeOffset)); + + using (StringReader reader = new StringReader(xml)) + { + DateTimeOffset deserializedObj = (DateTimeOffset)serializer.Deserialize(reader); + Assert.Equal(now, deserializedObj); + } + } + + [Fact] + public static void Xml_DeserializeEmptyDateTimeOffsetType() + { + string xml = @""; + XmlSerializer serializer = new XmlSerializer(typeof(DateTimeOffset)); + + using (StringReader reader = new StringReader(xml)) + { + DateTimeOffset deserializedObj = (DateTimeOffset)serializer.Deserialize(reader); + Assert.Equal(default(DateTimeOffset), deserializedObj); + } + } + [Fact] public static void Xml_TypeWithByteProperty() { diff --git a/src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs b/src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs index 6c8da649d56e08..bc10a370b033cd 100644 --- a/src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs +++ b/src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs @@ -900,6 +900,22 @@ public class TypeWithBinaryProperty public byte[] Base64Content { get; set; } } +public class TypeWithDateTimeOffsetProperties +{ + public DateTimeOffset DTO { get; set; } + public DateTimeOffset DTO2 { get; set; } + + [XmlElement(ElementName = "DefaultDTO")] + [DefaultValue(typeof(DateTimeOffset), "1/1/0001 0:00:00 AM +00:00")] + public DateTimeOffset DTOWithDefault { get; set; } + + public DateTimeOffset? NullableDTO { get; set; } + + [XmlElement(ElementName = "NullableDefaultDTO")] + [DefaultValue(typeof(DateTimeOffset), "1/1/0001 0:00:00 AM +00:00")] + public DateTimeOffset? NullableDTOWithDefault { get; set; } +} + public class TypeWithTimeSpanProperty { public TimeSpan TimeSpanProperty;