From d8e9581c45791ca634f06488fafd48b0c58b44de Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Thu, 3 Oct 2024 14:50:45 -0700 Subject: [PATCH 1/2] Fixes #2293: Enable DateOnly and TimeOnly Can write the 'DateOnly' and 'TimeOnly' Can read the 'DateOnly' and 'TimeOnly' --- .../Serialization/PrimitiveType.cs | 6 ++- .../Serialization/PrimitiveXmlConverter.cs | 54 ++++++++++++++++++- .../Evaluation/EdmValueUtils.cs | 12 +++++ .../Evaluation/LiteralFormatter.cs | 12 +++++ .../Json/JsonWriterExtensions.Async.cs | 10 ++++ .../Json/JsonWriterExtensions.cs | 15 ++++++ .../Json/ODataJsonWriterUtils.cs | 4 +- .../Metadata/EdmLibraryExtensions.cs | 5 ++ .../ODataPayloadValueConverter.cs | 12 +++++ .../ODataRawValueUtils.cs | 12 +++++ .../Uri/ODataUriConversionUtils.cs | 6 +++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 5 +- src/Microsoft.OData.Edm/Schema/Date.cs | 20 +++++++ src/Microsoft.OData.Edm/Schema/TimeOfDay.cs | 20 +++++++ src/PlatformHelper.cs | 32 +++++++++++ .../Json/JsonWriterBaseTests.cs | 30 +++++++++++ .../Json/JsonWriterTests.cs | 12 +++++ .../ODataJsonValueSerializerAsyncTests.cs | 30 +++++++++++ .../Metadata/EdmLibraryExtensionsTests.cs | 21 ++++++++ .../ODataMessageReaderTests.cs | 1 + .../Evaluation/KeyGenerationPinningTest.cs | 14 +++++ .../Json/BinaryValueEncodingTests.cs | 22 ++++++++ 22 files changed, 351 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OData.Client/Serialization/PrimitiveType.cs b/src/Microsoft.OData.Client/Serialization/PrimitiveType.cs index 6b1f6486a1..05c8016c2f 100644 --- a/src/Microsoft.OData.Client/Serialization/PrimitiveType.cs +++ b/src/Microsoft.OData.Client/Serialization/PrimitiveType.cs @@ -8,6 +8,7 @@ namespace Microsoft.OData.Client { using System; using System.Collections.Generic; + using System.ComponentModel; using System.Diagnostics; using System.Linq; @@ -367,7 +368,10 @@ private static void InitializeTypes() RegisterKnownType(typeof(GeometryMultiPolygon), XmlConstants.EdmGeometryMultiPolygonTypeName, EdmPrimitiveTypeKind.GeometryMultiPolygon, new GeometryTypeConverter(), true); RegisterKnownType(typeof(DataServiceStreamLink), XmlConstants.EdmStreamTypeName, EdmPrimitiveTypeKind.Stream, new NamedStreamTypeConverter(), false); RegisterKnownType(typeof(Date), XmlConstants.EdmDateTypeName, EdmPrimitiveTypeKind.Date, new DateTypeConverter(), true); - RegisterKnownType(typeof(TimeOfDay), XmlConstants.EdmTimeOfDayTypeName, EdmPrimitiveTypeKind.TimeOfDay, new TimeOfDayConvert(), true); + RegisterKnownType(typeof(TimeOfDay), XmlConstants.EdmTimeOfDayTypeName, EdmPrimitiveTypeKind.TimeOfDay, new TimeOfDayConverter(), true); + + RegisterKnownType(typeof(DateOnly), XmlConstants.EdmDateTypeName, EdmPrimitiveTypeKind.Date, new DateOnlyTypeConverter(), true); + RegisterKnownType(typeof(TimeOnly), XmlConstants.EdmTimeOfDayTypeName, EdmPrimitiveTypeKind.TimeOfDay, new TimeOnlyConverter(), true); // Following are known types are mapped to existing Edm type RegisterKnownType(typeof(Char), XmlConstants.EdmStringTypeName, EdmPrimitiveTypeKind.String, new CharTypeConverter(), false); diff --git a/src/Microsoft.OData.Client/Serialization/PrimitiveXmlConverter.cs b/src/Microsoft.OData.Client/Serialization/PrimitiveXmlConverter.cs index 79335c53c9..76217c5f72 100644 --- a/src/Microsoft.OData.Client/Serialization/PrimitiveXmlConverter.cs +++ b/src/Microsoft.OData.Client/Serialization/PrimitiveXmlConverter.cs @@ -858,10 +858,36 @@ internal override string ToString(object instance) } } + /// + /// Convert between primitive types Edm.Date and string + /// + internal sealed class DateOnlyTypeConverter : PrimitiveTypeConverter + { + /// + /// Create an instance of primitive type from a string representation + /// + /// The string representation + /// An instance of primitive type + internal override object Parse(string text) + { + return PlatformHelper.ConvertStringToDateOnly(text); + } + + /// + /// Convert an instance of primitive type to string + /// + /// The instance + /// The string representation of the instance + internal override string ToString(object instance) + { + return ((Date)(DateOnly)instance).ToString(); + } + } + /// /// Convert between primitive types Edm.TimeOfDay and string /// - internal sealed class TimeOfDayConvert : PrimitiveTypeConverter + internal sealed class TimeOfDayConverter : PrimitiveTypeConverter { /// /// Create an instance of primitive type from a string representation @@ -883,4 +909,30 @@ internal override string ToString(object instance) return ((TimeOfDay)instance).ToString(); } } + + /// + /// Convert between primitive types Edm.TimeOfDay and string + /// + internal sealed class TimeOnlyConverter : PrimitiveTypeConverter + { + /// + /// Create an instance of primitive type from a string representation + /// + /// The string representation + /// An instance of primitive type + internal override object Parse(string text) + { + return PlatformHelper.ConvertStringToTimeOnly(text); + } + + /// + /// Convert an instance of primitive type to string + /// + /// The instance + /// The string representation of the instance + internal override string ToString(object instance) + { + return ((TimeOfDay)(TimeOnly)instance).ToString(); + } + } } diff --git a/src/Microsoft.OData.Core/Evaluation/EdmValueUtils.cs b/src/Microsoft.OData.Core/Evaluation/EdmValueUtils.cs index 5c8bd27bf8..f914b8bc2d 100644 --- a/src/Microsoft.OData.Core/Evaluation/EdmValueUtils.cs +++ b/src/Microsoft.OData.Core/Evaluation/EdmValueUtils.cs @@ -283,6 +283,12 @@ private static IEdmDelayedValue ConvertPrimitiveValueWithoutTypeCode(object prim return new EdmDateConstant(dateType, (Date)primitiveValue); } + if (primitiveValue is DateOnly dateOnly) + { + IEdmPrimitiveTypeReference dateType = EnsurePrimitiveType(type, EdmPrimitiveTypeKind.Date); + return new EdmDateConstant(dateType, dateOnly); + } + if (primitiveValue is DateTimeOffset) { IEdmTemporalTypeReference dateTimeOffsetType = (IEdmTemporalTypeReference)EnsurePrimitiveType(type, EdmPrimitiveTypeKind.DateTimeOffset); @@ -301,6 +307,12 @@ private static IEdmDelayedValue ConvertPrimitiveValueWithoutTypeCode(object prim return new EdmTimeOfDayConstant(timeOfDayType, (TimeOfDay)primitiveValue); } + if (primitiveValue is TimeOnly timeOnly) + { + IEdmTemporalTypeReference timeOfDayType = (IEdmTemporalTypeReference)EnsurePrimitiveType(type, EdmPrimitiveTypeKind.TimeOfDay); + return new EdmTimeOfDayConstant(timeOfDayType, timeOnly); + } + if (primitiveValue is TimeSpan) { IEdmTemporalTypeReference timeType = (IEdmTemporalTypeReference)EnsurePrimitiveType(type, EdmPrimitiveTypeKind.Duration); diff --git a/src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs b/src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs index 08105aae46..c7605e8c64 100644 --- a/src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs +++ b/src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs @@ -27,12 +27,14 @@ namespace Microsoft.OData.Evaluation #if ODATA_CORE using Microsoft.OData.Edm; using Microsoft.Spatial; + using System.Globalization; #else using System.Xml.Linq; using Microsoft.OData; using Microsoft.OData.Edm; using Microsoft.Spatial; using ExpressionConstants = XmlConstants; + using System.Globalization; #endif /// @@ -223,6 +225,11 @@ private static string FormatRawLiteral(object value) return value.ToString(); } + if (value is DateOnly dateOnly) + { + return ((Date)dateOnly).ToString(); + } + if (value is DateTimeOffset) { return XmlConvert.ToString((DateTimeOffset)value); @@ -233,6 +240,11 @@ private static string FormatRawLiteral(object value) return value.ToString(); } + if (value is TimeOnly timeOnly) + { + return ((TimeOfDay)timeOnly).ToString(); + } + if (value is TimeSpan) { return EdmValueWriter.DurationAsXml((TimeSpan)value); diff --git a/src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs b/src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs index a492ddfcd3..d979b83bf1 100644 --- a/src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs +++ b/src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs @@ -135,11 +135,21 @@ internal static Task WritePrimitiveValueAsync(this IJsonWriter jsonWriter, objec return jsonWriter.WriteValueAsync((Date)value); } + if (value is DateOnly dateOnly) + { + return jsonWriter.WriteValueAsync(dateOnly); // will implicitly to call 'Date' version + } + if (value is TimeOfDay) { return jsonWriter.WriteValueAsync((TimeOfDay)value); } + if (value is TimeOnly timeOnly) + { + return jsonWriter.WriteValueAsync(timeOnly); // will implicitly to call 'TimeOfDay' version + } + return TaskUtils.GetFaultedTask( new ODataException(ODataErrorStrings.ODataJsonWriter_UnsupportedValueType(value.GetType().FullName))); } diff --git a/src/Microsoft.OData.Core/Json/JsonWriterExtensions.cs b/src/Microsoft.OData.Core/Json/JsonWriterExtensions.cs index 5b244dbb32..686be70805 100644 --- a/src/Microsoft.OData.Core/Json/JsonWriterExtensions.cs +++ b/src/Microsoft.OData.Core/Json/JsonWriterExtensions.cs @@ -147,12 +147,27 @@ internal static void WritePrimitiveValue(this IJsonWriter jsonWriter, object val return; } + // Why don't merge 'DateOnly' into 'Date' if clause? + // Because 'value' is System.Object, it's a boxed of 'DateOnly' and will throw exception to cast it to 'Date'. + // It's same for TimeOnly + if (value is DateOnly dateOnly) + { + jsonWriter.WriteValue(dateOnly); + return; + } + if (value is TimeOfDay) { jsonWriter.WriteValue((TimeOfDay)value); return; } + if (value is TimeOnly timeOnly) + { + jsonWriter.WriteValue(timeOnly); + return; + } + throw new ODataException(ODataErrorStrings.ODataJsonWriter_UnsupportedValueType(value.GetType().FullName)); } diff --git a/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs b/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs index 3c2be05599..c12cd6a223 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs @@ -121,7 +121,9 @@ internal static void ODataValueToString(StringBuilder sb, ODataValue value) valueAsString = JsonValueUtils.GetEscapedJsonString(valueAsString); sb.Append('"').Append(valueAsString).Append('"'); } - else if (valueAsObject is byte[] || valueAsObject is DateTimeOffset || valueAsObject is Guid || valueAsObject is TimeSpan | valueAsObject is Date || valueAsObject is TimeOfDay) + else if (valueAsObject is byte[] || valueAsObject is DateTimeOffset || valueAsObject is Guid + || valueAsObject is TimeSpan || valueAsObject is Date || valueAsObject is TimeOfDay + || valueAsObject is DateOnly || valueAsObject is TimeOnly) { sb.Append('"').Append(valueAsString).Append('"'); } diff --git a/src/Microsoft.OData.Core/Metadata/EdmLibraryExtensions.cs b/src/Microsoft.OData.Core/Metadata/EdmLibraryExtensions.cs index 25e7b89216..63e3b218f7 100644 --- a/src/Microsoft.OData.Core/Metadata/EdmLibraryExtensions.cs +++ b/src/Microsoft.OData.Core/Metadata/EdmLibraryExtensions.cs @@ -140,6 +140,11 @@ static EdmLibraryExtensions() PrimitiveTypeReferenceMap.Add(typeof(TimeOfDay), ToTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.TimeOfDay), false)); PrimitiveTypeReferenceMap.Add(typeof(TimeOfDay?), ToTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.TimeOfDay), true)); + PrimitiveTypeReferenceMap.Add(typeof(DateOnly), ToTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Date), false)); + PrimitiveTypeReferenceMap.Add(typeof(DateOnly?), ToTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.Date), true)); + PrimitiveTypeReferenceMap.Add(typeof(TimeOnly), ToTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.TimeOfDay), false)); + PrimitiveTypeReferenceMap.Add(typeof(TimeOnly?), ToTypeReference(EdmCoreModel.Instance.GetPrimitiveType(EdmPrimitiveTypeKind.TimeOfDay), true)); + #if !NETSTANDARD1_1 // Pack type codes of supported primitive types in the bitmap // See the type codes here: https://learn.microsoft.com/en-us/dotnet/api/system.typecode diff --git a/src/Microsoft.OData.Core/ODataPayloadValueConverter.cs b/src/Microsoft.OData.Core/ODataPayloadValueConverter.cs index 615f3bd333..be3e5b53e0 100644 --- a/src/Microsoft.OData.Core/ODataPayloadValueConverter.cs +++ b/src/Microsoft.OData.Core/ODataPayloadValueConverter.cs @@ -174,6 +174,18 @@ private static object ConvertStringValue(string stringValue, Type targetType) return PlatformHelper.ConvertStringToTimeOfDay(stringValue); } + // DateOnly + if (targetType == typeof(DateOnly)) + { + return PlatformHelper.ConvertStringToDateOnly(stringValue); + } + + // TimeOnly + if (targetType == typeof(TimeOnly)) + { + return PlatformHelper.ConvertStringToTimeOnly(stringValue); + } + // DateTimeOffset needs to be read using the XML rules (as per the Json spec). if (targetType == typeof(DateTimeOffset)) { diff --git a/src/Microsoft.OData.Core/ODataRawValueUtils.cs b/src/Microsoft.OData.Core/ODataRawValueUtils.cs index 7368672822..36bcec0360 100644 --- a/src/Microsoft.OData.Core/ODataRawValueUtils.cs +++ b/src/Microsoft.OData.Core/ODataRawValueUtils.cs @@ -123,6 +123,12 @@ internal static bool TryConvertPrimitiveToString(object value, out string result return true; } + if ( value is DateOnly dateOnly) + { + result = ODataRawValueConverter.ToString(dateOnly); + return true; + } + if (value is TimeOfDay) { // Edm.TimeOfDay @@ -130,6 +136,12 @@ internal static bool TryConvertPrimitiveToString(object value, out string result return true; } + if (value is TimeOnly timeOnly) + { + result = ODataRawValueConverter.ToString(timeOnly); + return true; + } + result = null; return false; } diff --git a/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs b/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs index 2dac432aee..928ec6f31e 100644 --- a/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs +++ b/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs @@ -512,6 +512,12 @@ internal static object CoerceTemporalType(object primitiveValue, IEdmPrimitiveTy return new DateTimeOffset(dateValue.Year, dateValue.Month, dateValue.Day, 0, 0, 0, new TimeSpan(0)); } + if (primitiveValue is DateOnly dateOnly) + { + var dateValue = (Date)dateOnly; + return new DateTimeOffset(dateValue.Year, dateValue.Month, dateValue.Day, 0, 0, 0, new TimeSpan(0)); + } + break; case EdmPrimitiveTypeKind.Date: diff --git a/src/Microsoft.OData.Edm/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.OData.Edm/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 5f282702bb..585a15cfd3 100644 --- a/src/Microsoft.OData.Edm/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OData.Edm/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ - \ No newline at end of file +static Microsoft.OData.Edm.Date.implicit operator Microsoft.OData.Edm.Date(System.DateOnly operand) -> Microsoft.OData.Edm.Date +static Microsoft.OData.Edm.Date.implicit operator System.DateOnly(Microsoft.OData.Edm.Date operand) -> System.DateOnly +static Microsoft.OData.Edm.TimeOfDay.implicit operator Microsoft.OData.Edm.TimeOfDay(System.TimeOnly timeOnly) -> Microsoft.OData.Edm.TimeOfDay +static Microsoft.OData.Edm.TimeOfDay.implicit operator System.TimeOnly(Microsoft.OData.Edm.TimeOfDay time) -> System.TimeOnly \ No newline at end of file diff --git a/src/Microsoft.OData.Edm/Schema/Date.cs b/src/Microsoft.OData.Edm/Schema/Date.cs index 158e93d9b4..6239fa3396 100644 --- a/src/Microsoft.OData.Edm/Schema/Date.cs +++ b/src/Microsoft.OData.Edm/Schema/Date.cs @@ -230,6 +230,26 @@ public static implicit operator Date(DateTime operand) return new Date(operand.Year, operand.Month, operand.Day); } + /// + /// Convert Date to Clr DateOnly + /// + /// Date Value + /// DateTime Value which represent the Date + public static implicit operator DateOnly(Date operand) + { + return DateOnly.FromDateTime(operand.dateTime); + } + + /// + /// Convert Clr DateOnly to Date + /// + /// DateOnly Value + /// Date Value from DateOnly + public static implicit operator Date(DateOnly operand) + { + return new Date(operand.Year, operand.Month, operand.Day); + } + /// /// Convert Date to String /// diff --git a/src/Microsoft.OData.Edm/Schema/TimeOfDay.cs b/src/Microsoft.OData.Edm/Schema/TimeOfDay.cs index 4ca91d4ab7..ac749c2861 100644 --- a/src/Microsoft.OData.Edm/Schema/TimeOfDay.cs +++ b/src/Microsoft.OData.Edm/Schema/TimeOfDay.cs @@ -253,6 +253,26 @@ public static implicit operator TimeOfDay(TimeSpan timeSpan) } } + /// + /// Convert TimeOfDay to .Net TimeOnly + /// + /// TimeOfDay Value + /// TimeOnly Value which represent the TimeOfDay + public static implicit operator TimeOnly(TimeOfDay time) + { + return TimeOnly.FromTimeSpan(time.timeSpan); + } + + /// + /// Convert .Net Clr TimeOnly to TimeOfDay + /// + /// TimeOnly Value + /// TimeOfDay Value from TimeOnly + public static implicit operator TimeOfDay(TimeOnly timeOnly) + { + return new TimeOfDay(timeOnly.Ticks); + } + /// /// Compares the value of this instance to a specified TimeOfDay value /// and returns an bool that indicates whether this instance is same as the specified TimeOfDay value. diff --git a/src/PlatformHelper.cs b/src/PlatformHelper.cs index 52fcc0c809..f40dd2d0fe 100644 --- a/src/PlatformHelper.cs +++ b/src/PlatformHelper.cs @@ -235,6 +235,22 @@ internal static Date ConvertStringToDate(string text) return date; } + /// + /// Converts a string to a DateOnly. + /// + /// String to be converted. + /// DateOnly value + internal static DateOnly ConvertStringToDateOnly(string text) + { + DateOnly date; + if (!DateOnly.TryParse(text, out date)) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "String '{0}' was not recognized as a valid Edm.Date.", text)); + } + + return date; + } + /// /// Converts a string to a TimeOfDay. /// @@ -267,6 +283,22 @@ internal static TimeOfDay ConvertStringToTimeOfDay(string text) return timeOfDay; } + + /// + /// Converts a string to a TimeOnly. + /// + /// String to be converted. + /// TimeOnly value + internal static TimeOnly ConvertStringToTimeOnly(string text) + { + TimeOnly timeOfDay; + if (!TimeOnly.TryParse(text, out timeOfDay)) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "String '{0}' was not recognized as a valid Edm.TimeOfDay.", text)); + } + + return timeOfDay; + } #endif /// diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterBaseTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterBaseTests.cs index 8c8009916b..be1cbccb51 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterBaseTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterBaseTests.cs @@ -1099,6 +1099,36 @@ public void WriteFloat(float value, string expectedOutput) Assert.Equal(expectedOutput, rawOutput); } + [Fact] + public void WriteDateOnly() + { + using MemoryStream stream = new MemoryStream(); + IJsonWriter jsonWriter = CreateJsonWriter(stream, isIeee754Compatible: false, Encoding.UTF8); + jsonWriter.WriteValue(new DateOnly(2024,10,1)); + jsonWriter.Flush(); + + stream.Seek(0, SeekOrigin.Begin); + + using StreamReader reader = new StreamReader(stream, encoding: Encoding.UTF8); + string rawOutput = reader.ReadToEnd(); + Assert.Equal("\"2024-10-01\"", rawOutput); + } + + [Fact] + public void WriteTimeOnly() + { + using MemoryStream stream = new MemoryStream(); + IJsonWriter jsonWriter = CreateJsonWriter(stream, isIeee754Compatible: false, Encoding.UTF8); + jsonWriter.WriteValue(new TimeOnly(4, 3, 2, 1)); + jsonWriter.Flush(); + + stream.Seek(0, SeekOrigin.Begin); + + using StreamReader reader = new StreamReader(stream, encoding: Encoding.UTF8); + string rawOutput = reader.ReadToEnd(); + Assert.Equal("\"04:03:02.0010000\"", rawOutput); + } + /// /// Normalizes the differences between JSON text encoded /// by Utf8JsonWriter and OData's JsonWriter, to make diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterTests.cs index 8b67385357..d33fb3fe12 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterTests.cs @@ -204,12 +204,24 @@ public void WritePrimitiveValueDate() this.VerifyWritePrimitiveValue(new Date(2014, 12, 31), "\"2014-12-31\""); } + [Fact] + public void WritePrimitiveValueDateOnly() + { + this.VerifyWritePrimitiveValue(new DateOnly(2024, 10, 1), "\"2024-10-01\""); + } + [Fact] public void WritePrimitiveValueTimeOfDay() { this.VerifyWritePrimitiveValue(new TimeOfDay(12, 30, 5, 10), "\"12:30:05.0100000\""); } + [Fact] + public void WritePrimitiveValueTimeOnly() + { + this.VerifyWritePrimitiveValue(new TimeOnly(4, 3, 2, 1), "\"04:03:02.0010000\""); + } + [Fact] public void WriteRawValueWritesValue() { diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonValueSerializerAsyncTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonValueSerializerAsyncTests.cs index 6172893efe..7d46845f5e 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonValueSerializerAsyncTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/ODataJsonValueSerializerAsyncTests.cs @@ -425,6 +425,36 @@ public async Task WritePrimitiveValueAsync_WritesExpectedValue() Assert.Equal("\"2014-09-17\"", result); } + [Fact] + public async Task WriteDateOnlyValueAsync_WritesExpectedValue() + { + var date = new DateOnly(2024, 9, 17); + var dateEdmTypeReference = EdmCoreModel.Instance.GetDate(false); + + var result = await SetupJsonValueSerializerAndRunTestAsync( + (jsonValueSerializer) => + { + return jsonValueSerializer.WritePrimitiveValueAsync(date, dateEdmTypeReference); + }); + + Assert.Equal("\"2024-09-17\"", result); + } + + [Fact] + public async Task WriteTimeOnlyValueAsync_WritesExpectedValue() + { + var timeOnly = new TimeOfDay(14, 9, 17, 2); + var dateEdmTypeReference = EdmCoreModel.Instance.GetTimeOfDay(false); + + var result = await SetupJsonValueSerializerAndRunTestAsync( + (jsonValueSerializer) => + { + return jsonValueSerializer.WritePrimitiveValueAsync(timeOnly, dateEdmTypeReference); + }); + + Assert.Equal("\"14:09:17.0020000\"", result); + } + [Fact] public async Task WriteUntypedValueAsync_ThrowsExceptionForRawValueNullOrEmpty() { diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/Metadata/EdmLibraryExtensionsTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/Metadata/EdmLibraryExtensionsTests.cs index 8bfde57050..5ae9a8cdf3 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/Metadata/EdmLibraryExtensionsTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/Metadata/EdmLibraryExtensionsTests.cs @@ -722,6 +722,11 @@ public void IsUserModelForCoreModelShouldBeFalse() [InlineData(typeof(byte))] [InlineData(typeof(bool))] [InlineData(typeof(float))] + [InlineData(typeof(Date))] + [InlineData(typeof(DateOnly))] + [InlineData(typeof(DateOnly?))] + [InlineData(typeof(TimeOnly))] + [InlineData(typeof(TimeOnly?))] [InlineData(typeof(Geography))] [InlineData(typeof(Geometry))] public void IsPrimitiveTypeForSupportedTypesShouldBeTrue(Type type) @@ -739,6 +744,22 @@ public void IsPrimitiveTypeForUnsupportedTypesShouldBeFalse(Type type) Assert.False(result); } + [Theory] + [InlineData(typeof(Date), EdmPrimitiveTypeKind.Date, false)] + [InlineData(typeof(Date?), EdmPrimitiveTypeKind.Date, true)] + [InlineData(typeof(TimeOfDay), EdmPrimitiveTypeKind.TimeOfDay, false)] + [InlineData(typeof(TimeOfDay?), EdmPrimitiveTypeKind.TimeOfDay, true)] + [InlineData(typeof(DateOnly), EdmPrimitiveTypeKind.Date, false)] + [InlineData(typeof(DateOnly?), EdmPrimitiveTypeKind.Date, true)] + [InlineData(typeof(TimeOnly), EdmPrimitiveTypeKind.TimeOfDay, false)] + [InlineData(typeof(TimeOnly?), EdmPrimitiveTypeKind.TimeOfDay, true)] + public void GetPrimitiveTypeReferenceForDateOnlyTimeOnlyShouldReturnCorrectEdmType(Type clrType, EdmPrimitiveTypeKind kind, bool nullable) + { + IEdmPrimitiveTypeReference primitiveTypeRef = EdmLibraryExtensions.GetPrimitiveTypeReference(clrType); + Assert.Equal(kind, primitiveTypeRef.PrimitiveKind()); + Assert.Equal(nullable, primitiveTypeRef.IsNullable); + } + [Theory] [InlineData(EdmPrimitiveTypeKind.Boolean, true, typeof(bool?))] [InlineData(EdmPrimitiveTypeKind.Boolean, false, typeof(bool))] diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ODataMessageReaderTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ODataMessageReaderTests.cs index 4aa5e52c4b..061a3efbba 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ODataMessageReaderTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ODataMessageReaderTests.cs @@ -106,6 +106,7 @@ public void ReadValueOfTimeOfDayShouldWork() ODataMessageReader reader = new ODataMessageReader(responseMessage, new ODataMessageReaderSettings(), new EdmModel()); var result = reader.ReadValue(new EdmTypeDefinitionReference(new EdmTypeDefinition("NS", "TimeOfDayValue", EdmPrimitiveTypeKind.TimeOfDay), true)); Assert.Equal(new TimeOfDay(12, 30, 4, 998), result); + Assert.Equal(new TimeOnly(12, 30, 4, 998), (TimeOnly)(TimeOfDay)result); // need to cast from object to 'TimeOfDay' first then cast to 'TimeOnly' } [Fact] diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Evaluation/KeyGenerationPinningTest.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Evaluation/KeyGenerationPinningTest.cs index 4af3a4bb2a..819bff0518 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Evaluation/KeyGenerationPinningTest.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Evaluation/KeyGenerationPinningTest.cs @@ -105,6 +105,10 @@ public void GiantPinningTest() RunPinningTest(builder, DateTimeOffset.MaxValue, DateTimeOffset.MinValue, XmlConvert.ToDateTimeOffset("2012-11-16T10:54:13.5422534-08:00"), XmlConvert.ToDateTimeOffset("2012-11-16T18:54:13.5422534Z")); RunPinningTest(builder, Date.MinValue, new Date(2014, 12, 31), Date.MaxValue); RunPinningTest(builder, TimeOfDay.MinValue, new TimeOfDay(12, 20, 4, 123), new TimeOfDay(TimeOfDay.MaxTickValue)); + + RunPinningTest(builder, DateOnly.MinValue, new DateOnly(2024, 10, 1), DateOnly.MaxValue); + RunPinningTest(builder, TimeOnly.MinValue, new TimeOnly(4, 3, 2, 1), TimeOnly.MaxValue); + RunPinningTest(builder, TimeSpan.MaxValue, TimeSpan.MinValue, TimeSpan.FromDays(1.5)); RunPinningTest(builder, Guid.Empty, Guid.Parse("b467459e-1eb5-4598-8a63-2c40c6a2590c")); RunPinningTest(builder, "", " \t \r\n", ".,();", "\r\n", "\r\n\r\n\r\n\r\n", "\r", "\n", "\n\r", "a\x0302e\x0327\x0627\x0654\x0655", "a surrogate pair: \xd800\xdc00", "left to right \x05d0\x05d1 \x05ea\x05e9 english", "\x1\x2\x3\x4\x5\x20"); @@ -176,6 +180,16 @@ public void GiantPinningTest() (23%3A59%3A59.9999999) (prop0=00%3A00%3A00.0000000,prop1=12%3A20%3A04.1230000,prop2=23%3A59%3A59.9999999) +(0001-01-01) +(2024-10-01) +(9999-12-31) +(prop0=0001-01-01,prop1=2024-10-01,prop2=9999-12-31) + +(00%3A00%3A00.0000000) +(04%3A03%3A02.0010000) +(23%3A59%3A59.9999999) +(prop0=00%3A00%3A00.0000000,prop1=04%3A03%3A02.0010000,prop2=23%3A59%3A59.9999999) + (duration'P10675199DT2H48M5.4775807S') (duration'-P10675199DT2H48M5.4775808S') (duration'P1DT12H') diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Roundtrip/Json/BinaryValueEncodingTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Roundtrip/Json/BinaryValueEncodingTests.cs index 07e4237ba5..3bc1219661 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Roundtrip/Json/BinaryValueEncodingTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/Roundtrip/Json/BinaryValueEncodingTests.cs @@ -71,6 +71,28 @@ private static void VerifyBinaryEncoding(byte[] byteArray, string encodedByteArr Assert.True(KeyAsSegmentsLiteralParser.TryParseLiteral(byteArray.GetType(), Uri.UnescapeDataString(keyAsSegmentFormattedByteArray), out keyAsSegmentParsedByteArray)); Assert.Equal((byte[])keyAsSegmentParsedByteArray, byteArray); } + + [Fact] + public void LiteralFormatterFormatDateOnlyLiteral() + { + DateOnly dateOnly = new DateOnly(2024, 10, 1); + string defaultFormattedDateOnly = DefaultLiteralFormatter.Format(dateOnly); + string keyAsSegmentFormattedDateOnly = KeyAsSegmentsLiteralFormatter.Format(dateOnly); + + Assert.Equal("2024-10-01", defaultFormattedDateOnly); + Assert.Equal(defaultFormattedDateOnly, keyAsSegmentFormattedDateOnly); + } + + [Fact] + public void LiteralFormatterFormatTimeOnlyLiteral() + { + TimeOnly timeOnly = new TimeOnly(4, 10, 1, 9); + string defaultFormattedTimeOnly = DefaultLiteralFormatter.Format(timeOnly); + string keyAsSegmentFormattedTimeOnly = KeyAsSegmentsLiteralFormatter.Format(timeOnly); + + Assert.Equal("04%3A10%3A01.0090000", defaultFormattedTimeOnly); + Assert.Equal(defaultFormattedTimeOnly, keyAsSegmentFormattedTimeOnly); + } } } From fb8086f9d233f0152f943e78082b99be4697d10c Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Mon, 7 Oct 2024 11:47:10 -0700 Subject: [PATCH 2/2] Resolved the comments. --- .../Serialization/PrimitiveType.cs | 6 +++--- .../Serialization/PrimitiveXmlConverter.cs | 4 ++-- .../Evaluation/LiteralFormatter.cs | 3 +-- .../Json/JsonWriterExtensions.Async.cs | 6 ++++-- src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs | 11 ++++++++--- .../ODataPayloadValueConverter.cs | 4 ---- src/Microsoft.OData.Core/ODataRawValueUtils.cs | 2 +- .../Uri/ODataUriConversionUtils.cs | 4 ++-- src/PlatformHelper.cs | 4 ++-- .../Json/JsonWriterBaseTests.cs | 2 +- 10 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.OData.Client/Serialization/PrimitiveType.cs b/src/Microsoft.OData.Client/Serialization/PrimitiveType.cs index 05c8016c2f..732858ef78 100644 --- a/src/Microsoft.OData.Client/Serialization/PrimitiveType.cs +++ b/src/Microsoft.OData.Client/Serialization/PrimitiveType.cs @@ -370,10 +370,10 @@ private static void InitializeTypes() RegisterKnownType(typeof(Date), XmlConstants.EdmDateTypeName, EdmPrimitiveTypeKind.Date, new DateTypeConverter(), true); RegisterKnownType(typeof(TimeOfDay), XmlConstants.EdmTimeOfDayTypeName, EdmPrimitiveTypeKind.TimeOfDay, new TimeOfDayConverter(), true); - RegisterKnownType(typeof(DateOnly), XmlConstants.EdmDateTypeName, EdmPrimitiveTypeKind.Date, new DateOnlyTypeConverter(), true); - RegisterKnownType(typeof(TimeOnly), XmlConstants.EdmTimeOfDayTypeName, EdmPrimitiveTypeKind.TimeOfDay, new TimeOnlyConverter(), true); - // Following are known types are mapped to existing Edm type + RegisterKnownType(typeof(DateOnly), XmlConstants.EdmDateTypeName, EdmPrimitiveTypeKind.Date, new DateOnlyTypeConverter(), false); + RegisterKnownType(typeof(TimeOnly), XmlConstants.EdmTimeOfDayTypeName, EdmPrimitiveTypeKind.TimeOfDay, new TimeOnlyConverter(), false); + RegisterKnownType(typeof(Char), XmlConstants.EdmStringTypeName, EdmPrimitiveTypeKind.String, new CharTypeConverter(), false); RegisterKnownType(typeof(Char[]), XmlConstants.EdmStringTypeName, EdmPrimitiveTypeKind.String, new CharArrayTypeConverter(), false); RegisterKnownType(typeof(Type), XmlConstants.EdmStringTypeName, EdmPrimitiveTypeKind.String, new ClrTypeConverter(), false); diff --git a/src/Microsoft.OData.Client/Serialization/PrimitiveXmlConverter.cs b/src/Microsoft.OData.Client/Serialization/PrimitiveXmlConverter.cs index 76217c5f72..3de551a364 100644 --- a/src/Microsoft.OData.Client/Serialization/PrimitiveXmlConverter.cs +++ b/src/Microsoft.OData.Client/Serialization/PrimitiveXmlConverter.cs @@ -859,7 +859,7 @@ internal override string ToString(object instance) } /// - /// Convert between primitive types Edm.Date and string + /// Convert between primitive types Edm.Date (using DateOnly) and string /// internal sealed class DateOnlyTypeConverter : PrimitiveTypeConverter { @@ -911,7 +911,7 @@ internal override string ToString(object instance) } /// - /// Convert between primitive types Edm.TimeOfDay and string + /// Convert between primitive types Edm.TimeOfDay (using TimeOnly) and string /// internal sealed class TimeOnlyConverter : PrimitiveTypeConverter { diff --git a/src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs b/src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs index c7605e8c64..1760944cc8 100644 --- a/src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs +++ b/src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs @@ -21,20 +21,19 @@ namespace Microsoft.OData.Evaluation using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; + using System.Globalization; using System.Text; using System.Linq; using System.Xml; #if ODATA_CORE using Microsoft.OData.Edm; using Microsoft.Spatial; - using System.Globalization; #else using System.Xml.Linq; using Microsoft.OData; using Microsoft.OData.Edm; using Microsoft.Spatial; using ExpressionConstants = XmlConstants; - using System.Globalization; #endif /// diff --git a/src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs b/src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs index d979b83bf1..00e544f096 100644 --- a/src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs +++ b/src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs @@ -137,7 +137,8 @@ internal static Task WritePrimitiveValueAsync(this IJsonWriter jsonWriter, objec if (value is DateOnly dateOnly) { - return jsonWriter.WriteValueAsync(dateOnly); // will implicitly to call 'Date' version + // will call 'WriteValueAsync(Date)' version implicitly + return jsonWriter.WriteValueAsync(dateOnly); } if (value is TimeOfDay) @@ -147,7 +148,8 @@ internal static Task WritePrimitiveValueAsync(this IJsonWriter jsonWriter, objec if (value is TimeOnly timeOnly) { - return jsonWriter.WriteValueAsync(timeOnly); // will implicitly to call 'TimeOfDay' version + // will call 'WriteValueAsync(TimeOfDay)' version implicitly + return jsonWriter.WriteValueAsync(timeOnly); } return TaskUtils.GetFaultedTask( diff --git a/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs b/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs index c12cd6a223..df0b8e2214 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs @@ -121,9 +121,14 @@ internal static void ODataValueToString(StringBuilder sb, ODataValue value) valueAsString = JsonValueUtils.GetEscapedJsonString(valueAsString); sb.Append('"').Append(valueAsString).Append('"'); } - else if (valueAsObject is byte[] || valueAsObject is DateTimeOffset || valueAsObject is Guid - || valueAsObject is TimeSpan || valueAsObject is Date || valueAsObject is TimeOfDay - || valueAsObject is DateOnly || valueAsObject is TimeOnly) + else if (valueAsObject is byte[] + || valueAsObject is DateTimeOffset + || valueAsObject is Guid + || valueAsObject is TimeSpan + || valueAsObject is Date + || valueAsObject is TimeOfDay + || valueAsObject is DateOnly + || valueAsObject is TimeOnly) { sb.Append('"').Append(valueAsString).Append('"'); } diff --git a/src/Microsoft.OData.Core/ODataPayloadValueConverter.cs b/src/Microsoft.OData.Core/ODataPayloadValueConverter.cs index be3e5b53e0..c0aeea9694 100644 --- a/src/Microsoft.OData.Core/ODataPayloadValueConverter.cs +++ b/src/Microsoft.OData.Core/ODataPayloadValueConverter.cs @@ -162,25 +162,21 @@ private static object ConvertStringValue(string stringValue, Type targetType) return EdmValueParser.ParseDuration(stringValue); } - // Date if (targetType == typeof(Date)) { return PlatformHelper.ConvertStringToDate(stringValue); } - // Time if (targetType == typeof(TimeOfDay)) { return PlatformHelper.ConvertStringToTimeOfDay(stringValue); } - // DateOnly if (targetType == typeof(DateOnly)) { return PlatformHelper.ConvertStringToDateOnly(stringValue); } - // TimeOnly if (targetType == typeof(TimeOnly)) { return PlatformHelper.ConvertStringToTimeOnly(stringValue); diff --git a/src/Microsoft.OData.Core/ODataRawValueUtils.cs b/src/Microsoft.OData.Core/ODataRawValueUtils.cs index 36bcec0360..af56776d38 100644 --- a/src/Microsoft.OData.Core/ODataRawValueUtils.cs +++ b/src/Microsoft.OData.Core/ODataRawValueUtils.cs @@ -123,7 +123,7 @@ internal static bool TryConvertPrimitiveToString(object value, out string result return true; } - if ( value is DateOnly dateOnly) + if (value is DateOnly dateOnly) { result = ODataRawValueConverter.ToString(dateOnly); return true; diff --git a/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs b/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs index 928ec6f31e..eef925352f 100644 --- a/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs +++ b/src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs @@ -509,13 +509,13 @@ internal static object CoerceTemporalType(object primitiveValue, IEdmPrimitiveTy if (primitiveValue is Date) { var dateValue = (Date)primitiveValue; - return new DateTimeOffset(dateValue.Year, dateValue.Month, dateValue.Day, 0, 0, 0, new TimeSpan(0)); + return new DateTimeOffset(dateValue.Year, dateValue.Month, dateValue.Day, 0, 0, 0, TimeSpan.Zero); } if (primitiveValue is DateOnly dateOnly) { var dateValue = (Date)dateOnly; - return new DateTimeOffset(dateValue.Year, dateValue.Month, dateValue.Day, 0, 0, 0, new TimeSpan(0)); + return new DateTimeOffset(dateValue.Year, dateValue.Month, dateValue.Day, 0, 0, 0, TimeSpan.Zero); } break; diff --git a/src/PlatformHelper.cs b/src/PlatformHelper.cs index f40dd2d0fe..c5a39395bf 100644 --- a/src/PlatformHelper.cs +++ b/src/PlatformHelper.cs @@ -236,7 +236,7 @@ internal static Date ConvertStringToDate(string text) } /// - /// Converts a string to a DateOnly. + /// Converts a string to a . /// /// String to be converted. /// DateOnly value @@ -252,7 +252,7 @@ internal static DateOnly ConvertStringToDateOnly(string text) } /// - /// Converts a string to a TimeOfDay. + /// Converts a string to a . /// /// String to be converted. /// Time of the day diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterBaseTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterBaseTests.cs index be1cbccb51..8b1fbdff62 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterBaseTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/Json/JsonWriterBaseTests.cs @@ -1104,7 +1104,7 @@ public void WriteDateOnly() { using MemoryStream stream = new MemoryStream(); IJsonWriter jsonWriter = CreateJsonWriter(stream, isIeee754Compatible: false, Encoding.UTF8); - jsonWriter.WriteValue(new DateOnly(2024,10,1)); + jsonWriter.WriteValue(new DateOnly(2024, 10, 1)); jsonWriter.Flush(); stream.Seek(0, SeekOrigin.Begin);