-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Add DateOnly and TimeOnly serializer primitives with restrictions #119835
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
Add DateOnly and TimeOnly serializer primitives with restrictions #119835
Conversation
…g xsd:time while ignoring offsets
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds first-class serialization support for DateOnly and TimeOnly types across .NET's three major XML serialization stacks: XmlSerializer, DataContractSerializer (DCS), and DataContractJsonSerializer (DCJS). The implementation treats these types as primitives with standardized ISO-like wire formats and includes comprehensive schema export/import support.
Key changes include:
- Extended primitive registration tables in all serializers to recognize
DateOnlyandTimeOnlyas first-class primitives - Added canonical wire formats:
DateOnly→yyyy-MM-dd,TimeOnly→HH:mm:ss[.FFFFFFF] - Implemented custom XSD schema types with pattern restrictions to prevent data loss from offset-bearing inputs
Reviewed Changes
Copilot reviewed 29 out of 30 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| System.Xml.XmlSerializer/ref/System.Xml.XmlSerializer.cs | Added protected method signatures for DateOnly/TimeOnly serialization helpers |
| XsdDataContractExporterTests/SchemaUtils.cs | Added optional parameter to control serialization namespace inclusion in schema dumps |
| XsdDataContractExporterTests/ExporterTypesTests.cs | Updated global type counts and added comprehensive schema export validation tests |
| XsdDataContractExporterTests/ExporterApiTests.cs | Updated expected type counts to account for new primitive types |
| SerializationTypes.cs | Added test wrapper classes for DateOnly/TimeOnly with various attributes and configurations |
| DataContractSerializer.cs | Added comprehensive round-trip tests for DateOnly/TimeOnly in DCS |
| DataContractJsonSerializer.cs | Added JSON serialization tests for DateOnly/TimeOnly primitives |
| XmlSerializerTests.cs | Added extensive XmlSerializer tests including error handling, cross-type compatibility, and schema validation |
| XmlSerializerTests.RuntimeOnly.cs | Added runtime-specific schema import/export tests |
| Xmlcustomformatter.cs | Implemented core formatting/parsing logic for DateOnly/TimeOnly |
| XmlSerializer.cs | Extended primitive serialization dispatch to handle new types |
| XmlSerializationWriter.cs | Added writer methods and primitive type name resolution |
| XmlSerializationReader.cs | Added reader methods and primitive ID registration |
| XmlReflectionImporter.cs | Enhanced DataType attribute handling to support secondary primitive mappings |
| Types.cs | Extended type system with secondary primitive registration and pattern-based schema generation |
| ReflectionXmlSerializationWriter.cs | Added primitive value writing support for DateOnly/TimeOnly |
| ReflectionXmlSerializationReader.cs | Added empty element handling for new primitive types |
| PrimitiveXmlSerializers.cs | Added dedicated serializer implementations for DateOnly/TimeOnly |
| CodeGenerator.cs | Extended constant loading to support DateOnly/TimeOnly constructor calls |
| LocalAppContextSwitches.cs | Added compatibility switch for TimeOnly offset handling |
| XmlWriterDelegator.cs | Implemented DCS writing support for DateOnly/TimeOnly |
| XmlReaderDelegator.cs | Implemented DCS reading support with strict parsing |
| PrimitiveDataContract.cs | Added DataContract implementations for DateOnly/TimeOnly |
| Globals.cs | Added type references and schema definitions for new primitives |
| DictionaryGlobals.cs | Added dictionary entries for primitive type names |
| DataContract.cs | Extended built-in contract registration for DateOnly/TimeOnly |
...tem.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlReaderDelegator.cs
Outdated
Show resolved
Hide resolved
...tem.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlReaderDelegator.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs
Outdated
Show resolved
Hide resolved
|
A few notes about binary format. When DateTime is serialized using the binary XmlDictionaryWriter, it writes is using a 64 bit ulong integer. There are 3 options that could be done when writing using a binary xml format. Write it using the datetime formatThe binary XML datetime format writes the underlying 64bit integer as a variable length integer. As a variable length integer, a 64bit integer actually requires 9 bytes on the wire. Along with that, there's a single data type byte emitted, so a total of 10 bytes. Write it using the textual xml representationTo represent as a string, you have the string itself (10-15 characters/bytes), plus a string data type byte, plus a string length (1 byte when < 127 characters). This makes the length when encoding as a string 12-17 bytes. Write it using a new binary representationDateOnly is represented internally as an unsigned 32bit integer. Having a custom binary representation would be 1 byte for data type, 5 bytes for integer value, so 6 bytes total. For TimeOnly, it's internally represented as a 64bit unsigned long, so the same math as DateTime, which is 10 bytes. To write it using a binary representation, XmlDictionaryWriter/XmlDictionaryReader would need to have support added for it. It would also need the relevant Microsoft protocol documentation updated to add these to it. If there's interest in doing this, we don't need to rush as we can actually defer this to a later time. The existing implementation for reading DateTime with a binary XmlDictionaryReader checks the data type byte, and if it's a string, reads the string from the Xml document then parses it. If the data type byte says it's a variable length integer, then it reads the integer and uses the constructor overload for DateTime which takes the raw integer value (ticks). Basically, DateTime was originally serialized as text like you are doing here, and a later decision was made to switch to binary. The reader was modified to handle either so it's backwards compatible. I recommend leaving things as they currently are in this PR, reading/writing as text, but be open to adding native binary support later. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Types.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Xmlcustomformatter.cs
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs
Outdated
Show resolved
Hide resolved
...ntime.Serialization.Schema/tests/System/Runtime/Serialization/Schema/Import/ImporterTests.cs
Outdated
Show resolved
Hide resolved
mconnew
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
![]()
|
/backport to release/10.0 |
|
Started backporting to release/10.0: https://github.com/dotnet/runtime/actions/runs/17871035137 |
…eed to make the test conditional so it doesn't fail AggressiveTrimming test runs.
|
/backport to release/10.0-rc2 |
|
Started backporting to release/10.0-rc2: https://github.com/dotnet/runtime/actions/runs/17953698641 |
|
@StephenMolloy backporting to "release/10.0-rc2" failed, the patch most likely resulted in conflicts: $ git am --3way --empty=keep --ignore-whitespace --keep-non-patch changes.patch
Applying: Add DateOnly and TimeOnly as primities for XmlSerializer - With Tests
Applying: Add option to tag TimeOnly fields with DataType=time to allow handling xsd:time while ignoring offsets
Applying: Add DateOnly and TimeOnly as primities for DCS - With Tests
Applying: Add tests for schema import/export considerations.
Applying: Copilot PR feedback.
Applying: Address some PR feedback.
Applying: Different approach to managing AppContext switches and caching
.git/rebase-apply/patch:163: trailing whitespace.
warning: 1 line adds whitespace errors.
Using index info to reconstruct a base tree...
M src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs
M src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
Falling back to patching base and 3-way merge...
Auto-merging src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs
Auto-merging src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Patch failed at 0007 Different approach to managing AppContext switches and caching
Error: The process '/usr/bin/git' failed with exit code 128Please backport manually! |
Fixes #56711
Summary
This PR adds first-class serialization support for
DateOnlyandTimeOnlyacross two major .NET XML serialization stacks: DataContractSerializer (DCS/DCJS) and XmlSerializer. The implementation treats these types as primitives (similar toDateTime,TimeSpan, etc.) rather than complex types, ensuring clean schema export/import and efficient round-trip serialization.Key Changes
Core Implementation
DateOnlyandTimeOnlyas first-class primitivesDateOnly→yyyy-MM-dd(ISO 8601 date format)TimeOnly→HH:mm:ss[.FFFFFFF](ISO-like time format with optional fractional seconds)Schema Export/Import
http://microsoft.com/wsdl/types/(URT namespace)http://schemas.microsoft.com/2003/10/Serialization/XsdDataContractExporter,XmlSchemaExporter, etc.)Interoperability Features
[XmlElement(DataType="time")]forces export as standardxs:timefor legacy schema compatibilityxs:time→DateTimebehavior unchanged for backward compatibilityDesign Decisions & Rationale
1. Primitive vs Adapter Pattern
Decision: Implement as first-class primitives rather than adapter wrappers (like
DateTimeOffsetAdapter).Why:
DateTimeOffsetrequired an adapter historically due to its composite semantics (instant + offset) and late introduction when primitive tables were frozen.DateOnly/TimeOnlyare lexically atomic, map cleanly to existing XSD bases, and the primitive infrastructure is now mature and stable.2. Custom Restricted Types vs Direct XSD Mapping
Decision: Default to custom
timeOnly/dateOnlytypes (restrictions ofxs:time/xs:date) rather than direct mapping.Why: Data preservation is serialization rule #1. Raw
xs:timelegally permits timezone offsets (13:45:00Z,09:30:00-05:00) thatTimeOnlycannot represent. Using custom restricted types with pattern facets prevents offset-bearing inputs that would force either silent data truncation or late runtime rejection; constraining the lexical space up-front makes the contract explicit and tool-validated.3. Parsing Strictness
Decision: Enforce canonical forms only—reject offsets, zone indicators,
24:00:00, extra whitespace.Why: Guarantees deterministic round-trips, prevents implicit data loss, enables simpler/faster parsing, and leaves room for future opt-in relaxed modes without breaking existing contracts.
4. Fractional Seconds Handling
Decision: Emit minimal form (trim trailing zeros, no forced width).
Why: Aligns with existing
DateTimeserialization behavior across XmlSerializer, DCS, andSystem.Text.Json(all omit unnecessary trailing fractional zeros). Shows only meaningful precision, produces stable canonical representations, and avoids implying precision that was never present.5. No Binary Protocol Tokens
Decision: Use textual serialization only, no new binary XML tokens.
Why: Payloads are already minimal (10-15 characters), benefits would be negligible, and adding binary forms would require expanding public writer/reader APIs with significant maintenance overhead for little gain.
Testing Strategy
TimeOnly→xs:timemappingDateTimebehaviors unchangedWire Format Examples
XML (DCS & XmlSerializer)
JSON (DCJS)
{ "dateOnlyField": "2024-01-15", "timeOnlyField": "14:30:25.123" }Schema Export
Backward Compatibility
DateTime/DateTimeOffsetserialization behaviors preservedxs:date/xs:time) can still deserialize their lexical values intoDateTimewithout information loss, effectively falling back on the base XSD types while ignoring the additional restriction pattern.Future Extensibility
The implementation leaves room for future enhancements:
/Date(ticks)/form (still seen with olderDateTimeserializers) originated before broad ISO 8601 convergence; choosing modern ISO text for these new primitives avoids perpetuating an anachronistic shape.Files Changed
System.Private.Xml.TestsandSystem.Runtime.Serialization.Xml.TestsThis implementation provides a solid foundation for modern date/time serialization while maintaining full backward compatibility and following established .NET serialization patterns.