Skip to content

Commit d925d45

Browse files
[release/10.0] Add DateOnly and TimeOnly serializer primitives with restrictions (#119915)
* Add DateOnly and TimeOnly as primities for XmlSerializer - With Tests * Add option to tag TimeOnly fields with DataType=time to allow handling xsd:time while ignoring offsets * Add DateOnly and TimeOnly as primities for DCS - With Tests * Add tests for schema import/export considerations. * Copilot PR feedback. * Address some PR feedback. * Different approach to managing AppContext switches and caching * Remaining PR feedback * TypeWithDateAndTimeOnlyProperties uses the DefaultValue attribute - need to make the test conditional so it doesn't fail AggressiveTrimming test runs. * Re-add tests that were mysteriously removed when copilot created import/export tests. --------- Co-authored-by: Steve Molloy <[email protected]>
1 parent 7365770 commit d925d45

File tree

30 files changed

+5054
-2842
lines changed

30 files changed

+5054
-2842
lines changed

src/libraries/Microsoft.XmlSerializer.Generator/tests/Expected.SerializableAssembly.XmlSerializers.cs

Lines changed: 3801 additions & 2829 deletions
Large diffs are not rendered by default.

src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,10 @@ internal static bool TryCreateBuiltInDataContract(Type type, [NotNullWhen(true)]
747747
dataContract = new TimeSpanDataContract();
748748
else if (type == typeof(Guid))
749749
dataContract = new GuidDataContract();
750+
else if (type == typeof(DateOnly))
751+
dataContract = new DateOnlyDataContract();
752+
else if (type == typeof(TimeOnly))
753+
dataContract = new TimeOnlyDataContract();
750754
else if (type == typeof(Enum) || type == typeof(ValueType))
751755
{
752756
dataContract = new SpecialTypeDataContract(type, DictionaryGlobals.ObjectLocalName, DictionaryGlobals.SchemaNamespace);
@@ -864,6 +868,10 @@ internal static bool TryCreateBuiltInDataContract(string name, string ns, [NotNu
864868
dataContract = new GuidDataContract();
865869
else if (DictionaryGlobals.CharLocalName.Value == name)
866870
dataContract = new CharDataContract();
871+
else if (DictionaryGlobals.DateOnlyLocalName.Value == name)
872+
dataContract = new DateOnlyDataContract();
873+
else if (DictionaryGlobals.TimeOnlyLocalName.Value == name)
874+
dataContract = new TimeOnlyDataContract();
867875
else if ("ArrayOfanyType" == name)
868876
dataContract = new CollectionDataContract(typeof(Array));
869877
}

src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DictionaryGlobals.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,5 +96,7 @@ internal static class DictionaryGlobals
9696

9797
// 60
9898
public static readonly XmlDictionaryString AsmxTypesNamespace = s_dictionary.Add("http://microsoft.com/wsdl/types/");
99+
public static readonly XmlDictionaryString DateOnlyLocalName = s_dictionary.Add("dateOnly");
100+
public static readonly XmlDictionaryString TimeOnlyLocalName = s_dictionary.Add("timeOnly");
99101
}
100102
}

src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Globals.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ internal static partial class Globals
3737
internal static Type TypeOfGuid => field ??= typeof(Guid);
3838
internal static Type TypeOfDateTimeOffset => field ??= typeof(DateTimeOffset);
3939
internal static Type TypeOfDateTimeOffsetAdapter => field ??= typeof(DateTimeOffsetAdapter);
40+
internal static Type TypeOfDateOnly => field ??= typeof(DateOnly);
41+
internal static Type TypeOfTimeOnly => field ??= typeof(TimeOnly);
4042
internal static Type TypeOfMemoryStream => field ??= typeof(MemoryStream);
4143
internal static Type TypeOfMemoryStreamAdapter => field ??= typeof(MemoryStreamAdapter);
4244
internal static Type TypeOfUri => field ??= typeof(Uri);
@@ -264,6 +266,16 @@ internal static Type TypeOfHashtable
264266
<xs:attribute name='FactoryType' type='xs:QName' />
265267
<xs:attribute name='Id' type='xs:ID' />
266268
<xs:attribute name='Ref' type='xs:IDREF' />
269+
<xs:simpleType name='dateOnly'>
270+
<xs:restriction base='xs:date'>
271+
<xs:pattern value='([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])' />
272+
</xs:restriction>
273+
</xs:simpleType>
274+
<xs:simpleType name='timeOnly'>
275+
<xs:restriction base='xs:time'>
276+
<xs:pattern value='([01][0-9]|2[0-3]):([0-5][0-9])(:([0-5][0-9])(\.[0-9]{1,7})?)?' />
277+
</xs:restriction>
278+
</xs:simpleType>
267279
</xs:schema>
268280
";
269281
}

src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/PrimitiveDataContract.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,78 @@ internal override void WriteXmlElement(XmlWriterDelegator xmlWriter, object? obj
705705
}
706706
}
707707

708+
internal sealed class DateOnlyDataContract : PrimitiveDataContract
709+
{
710+
public DateOnlyDataContract() : this(DictionaryGlobals.DateOnlyLocalName, DictionaryGlobals.SerializationNamespace)
711+
{
712+
}
713+
714+
internal DateOnlyDataContract(XmlDictionaryString name, XmlDictionaryString ns) : base(typeof(DateOnly), name, ns)
715+
{
716+
}
717+
718+
internal override string WriteMethodName => "WriteDateOnly";
719+
internal override string ReadMethodName => "ReadElementContentAsDateOnly";
720+
721+
[RequiresDynamicCode(DataContract.SerializerAOTWarning)]
722+
[RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
723+
internal override void WriteXmlValue(XmlWriterDelegator writer, object obj, XmlObjectSerializerWriteContext? context)
724+
{
725+
writer.WriteDateOnly((DateOnly)obj);
726+
}
727+
728+
[RequiresDynamicCode(DataContract.SerializerAOTWarning)]
729+
[RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
730+
internal override object? ReadXmlValue(XmlReaderDelegator reader, XmlObjectSerializerReadContext? context)
731+
{
732+
return (context == null) ? reader.ReadElementContentAsDateOnly()
733+
: HandleReadValue(reader.ReadElementContentAsDateOnly(), context);
734+
}
735+
736+
[RequiresDynamicCode(DataContract.SerializerAOTWarning)]
737+
[RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
738+
internal override void WriteXmlElement(XmlWriterDelegator xmlWriter, object? obj, XmlObjectSerializerWriteContext context, XmlDictionaryString name, XmlDictionaryString? ns)
739+
{
740+
xmlWriter.WriteDateOnly((DateOnly)obj!, name, ns);
741+
}
742+
}
743+
744+
internal sealed class TimeOnlyDataContract : PrimitiveDataContract
745+
{
746+
public TimeOnlyDataContract() : this(DictionaryGlobals.TimeOnlyLocalName, DictionaryGlobals.SerializationNamespace)
747+
{
748+
}
749+
750+
internal TimeOnlyDataContract(XmlDictionaryString name, XmlDictionaryString ns) : base(typeof(TimeOnly), name, ns)
751+
{
752+
}
753+
754+
internal override string WriteMethodName => "WriteTimeOnly";
755+
internal override string ReadMethodName => "ReadElementContentAsTimeOnly";
756+
757+
[RequiresDynamicCode(DataContract.SerializerAOTWarning)]
758+
[RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
759+
internal override void WriteXmlValue(XmlWriterDelegator writer, object obj, XmlObjectSerializerWriteContext? context)
760+
{
761+
writer.WriteTimeOnly((TimeOnly)obj);
762+
}
763+
764+
[RequiresDynamicCode(DataContract.SerializerAOTWarning)]
765+
[RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
766+
internal override object? ReadXmlValue(XmlReaderDelegator reader, XmlObjectSerializerReadContext? context)
767+
{
768+
return (context == null) ? reader.ReadElementContentAsTimeOnly()
769+
: HandleReadValue(reader.ReadElementContentAsTimeOnly(), context);
770+
}
771+
772+
[RequiresDynamicCode(DataContract.SerializerAOTWarning)]
773+
[RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)]
774+
internal override void WriteXmlElement(XmlWriterDelegator xmlWriter, object? obj, XmlObjectSerializerWriteContext context, XmlDictionaryString name, XmlDictionaryString? ns)
775+
{
776+
xmlWriter.WriteTimeOnly((TimeOnly)obj!, name, ns);
777+
}
778+
}
779+
708780
internal class StringDataContract : PrimitiveDataContract
709781
{
710782
public StringDataContract() : this(DictionaryGlobals.StringLocalName, DictionaryGlobals.SchemaNamespace)

src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlReaderDelegator.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,86 @@ internal virtual DateTime ReadContentAsDateTime()
509509
return reader.ReadContentAsDateTime();
510510
}
511511

512+
internal virtual DateOnly ReadElementContentAsDateOnly()
513+
{
514+
if (isEndOfEmptyElement)
515+
ThrowNotAtElement();
516+
string s = reader.ReadElementContentAsString();
517+
try
518+
{
519+
return ParseDateOnly(s);
520+
}
521+
catch (Exception ex) when (ex is FormatException || ex is ArgumentException)
522+
{
523+
ThrowConversionException(s, nameof(DateOnly));
524+
throw; // unreachable
525+
}
526+
}
527+
528+
internal virtual DateOnly ReadContentAsDateOnly()
529+
{
530+
if (isEndOfEmptyElement)
531+
ThrowConversionException(string.Empty, nameof(DateOnly));
532+
string s = reader.ReadContentAsString();
533+
try
534+
{
535+
return ParseDateOnly(s);
536+
}
537+
catch (Exception ex) when (ex is FormatException || ex is ArgumentException)
538+
{
539+
ThrowConversionException(s, nameof(DateOnly));
540+
throw; // unreachable
541+
}
542+
}
543+
544+
internal virtual TimeOnly ReadElementContentAsTimeOnly()
545+
{
546+
if (isEndOfEmptyElement)
547+
ThrowNotAtElement();
548+
549+
string s = reader.ReadElementContentAsString();
550+
551+
try
552+
{
553+
var dto = XmlConvert.ToDateTimeOffset(s);
554+
return TimeOnly.FromTimeSpan(dto.TimeOfDay);
555+
}
556+
catch (Exception ex) when (ex is FormatException || ex is ArgumentException)
557+
{
558+
ThrowConversionException(s, nameof(TimeOnly));
559+
throw; // unreachable
560+
}
561+
}
562+
563+
internal virtual TimeOnly ReadContentAsTimeOnly()
564+
{
565+
if (isEndOfEmptyElement)
566+
ThrowConversionException(string.Empty, nameof(TimeOnly));
567+
568+
string s = reader.ReadContentAsString();
569+
try
570+
{
571+
var dto = XmlConvert.ToDateTimeOffset(s);
572+
return TimeOnly.FromTimeSpan(dto.TimeOfDay);
573+
}
574+
catch (Exception ex) when (ex is FormatException || ex is ArgumentException)
575+
{
576+
ThrowConversionException(s, nameof(TimeOnly));
577+
throw; // unreachable
578+
}
579+
}
580+
581+
private static DateOnly ParseDateOnly(string s)
582+
{
583+
return DateOnly.ParseExact(s, "yyyy-MM-dd", DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite);
584+
}
585+
586+
private static TimeOnly ParseTimeOnly(string s)
587+
{
588+
// Strictly parse the expected TimeOnly format. No timezone/offset allowed.
589+
return TimeOnly.ParseExact(s, "HH:mm:ss.FFFFFFF", DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite);
590+
}
591+
512592
internal int ReadElementContentAsInt()
513593
{
514594
if (isEndOfEmptyElement)

src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlWriterDelegator.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ internal void WriteAnyType(object value, Type valueType)
347347
WriteUri((Uri)value);
348348
else if (valueType == Globals.TypeOfXmlQualifiedName)
349349
WriteQName((XmlQualifiedName)value);
350+
else if (valueType == Globals.TypeOfDateOnly)
351+
WriteDateOnly((DateOnly)value);
352+
else if (valueType == Globals.TypeOfTimeOnly)
353+
WriteTimeOnly((TimeOnly)value);
350354
else
351355
handled = false;
352356
break;
@@ -426,6 +430,10 @@ internal void WriteExtensionData(IDataNode dataNode)
426430
WriteUri(((DataNode<Uri>)dataNode).GetValue());
427431
else if (valueType == Globals.TypeOfXmlQualifiedName)
428432
WriteQName(((DataNode<XmlQualifiedName>)dataNode).GetValue());
433+
else if (valueType == Globals.TypeOfDateOnly)
434+
WriteDateOnly(((DataNode<DateOnly>)dataNode).GetValue());
435+
else if (valueType == Globals.TypeOfTimeOnly)
436+
WriteTimeOnly(((DataNode<TimeOnly>)dataNode).GetValue());
429437
else
430438
handled = false;
431439
break;
@@ -465,6 +473,30 @@ internal void WriteDateTime(DateTime value, XmlDictionaryString name, XmlDiction
465473
WriteEndElementPrimitive();
466474
}
467475

476+
// DateOnly / TimeOnly
477+
internal virtual void WriteDateOnly(DateOnly value)
478+
{
479+
writer.WriteString(value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
480+
}
481+
internal void WriteDateOnly(DateOnly value, XmlDictionaryString name, XmlDictionaryString? ns)
482+
{
483+
WriteStartElementPrimitive(name, ns);
484+
WriteDateOnly(value);
485+
WriteEndElementPrimitive();
486+
}
487+
internal virtual void WriteTimeOnly(TimeOnly value)
488+
{
489+
// Use optional fractional second digits (F) so trailing zeros and the '.' are omitted automatically.
490+
// "f" forces zeros; "F" suppresses them. "HH:mm:ss.FFFFFFF" yields minimal length representation.
491+
writer.WriteString(value.ToString("HH:mm:ss.FFFFFFF", CultureInfo.InvariantCulture));
492+
}
493+
internal void WriteTimeOnly(TimeOnly value, XmlDictionaryString name, XmlDictionaryString? ns)
494+
{
495+
WriteStartElementPrimitive(name, ns);
496+
WriteTimeOnly(value);
497+
WriteEndElementPrimitive();
498+
}
499+
468500
internal virtual void WriteDecimal(decimal value)
469501
{
470502
writer.WriteValue(value);

src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ public static bool IgnoreKindInUtcTimeSerialization
3939
}
4040
}
4141

42+
private static int s_allowXsdTimeToTimeOnlyWithOffsetLoss;
43+
public static bool AllowXsdTimeToTimeOnlyWithOffsetLoss
44+
{
45+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
46+
get
47+
{
48+
return SwitchesHelpers.GetCachedSwitchValue("Switch.System.Xml.AllowXsdTimeToTimeOnlyWithOffsetLoss", ref s_allowXsdTimeToTimeOnlyWithOffsetLoss);
49+
}
50+
}
51+
4252
private static int s_limitXPathComplexity;
4353
public static bool LimitXPathComplexity
4454
{

src/libraries/System.Private.Xml/src/System/Xml/Serialization/CodeGenerator.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,34 @@ internal void Ldc(object o)
864864
New(DateTimeOffset_ctor);
865865
break;
866866
}
867+
else if (valueType == typeof(DateOnly))
868+
{
869+
ConstructorInfo DateOnly_ctor = typeof(DateOnly).GetConstructor(
870+
CodeGenerator.InstanceBindingFlags,
871+
null,
872+
new Type[] { typeof(int), typeof(int), typeof(int) },
873+
null
874+
)!;
875+
DateOnly dateOnly = (DateOnly)o;
876+
Ldc(dateOnly.Year);
877+
Ldc(dateOnly.Month);
878+
Ldc(dateOnly.Day);
879+
New(DateOnly_ctor);
880+
break;
881+
}
882+
else if (valueType == typeof(TimeOnly))
883+
{
884+
ConstructorInfo TimeOnly_ctor = typeof(TimeOnly).GetConstructor(
885+
CodeGenerator.InstanceBindingFlags,
886+
null,
887+
new Type[] { typeof(long) },
888+
null
889+
)!;
890+
TimeOnly timeOnly = (TimeOnly)o;
891+
Ldc(timeOnly.Ticks);
892+
New(TimeOnly_ctor);
893+
break;
894+
}
867895
else
868896
{
869897
throw new NotSupportedException(SR.Format(SR.UnknownConstantType, valueType.AssemblyQualifiedName));

0 commit comments

Comments
 (0)