Skip to content
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

Fixes #2293: Enable DateOnly and TimeOnly #3078

Merged
merged 2 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion src/Microsoft.OData.Client/Serialization/PrimitiveType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.OData.Client
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -858,10 +858,36 @@ internal override string ToString(object instance)
}
}

/// <summary>
/// Convert between primitive types Edm.Date and string
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
internal sealed class DateOnlyTypeConverter : PrimitiveTypeConverter
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Create an instance of primitive type from a string representation
/// </summary>
/// <param name="text">The string representation</param>
/// <returns>An instance of primitive type</returns>
internal override object Parse(string text)
{
return PlatformHelper.ConvertStringToDateOnly(text);
}

/// <summary>
/// Convert an instance of primitive type to string
/// </summary>
/// <param name="instance">The instance</param>
/// <returns>The string representation of the instance</returns>
internal override string ToString(object instance)
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
{
return ((Date)(DateOnly)instance).ToString();
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// <summary>
/// Convert between primitive types Edm.TimeOfDay and string
/// </summary>
internal sealed class TimeOfDayConvert : PrimitiveTypeConverter
internal sealed class TimeOfDayConverter : PrimitiveTypeConverter
{
/// <summary>
/// Create an instance of primitive type from a string representation
Expand All @@ -883,4 +909,30 @@ internal override string ToString(object instance)
return ((TimeOfDay)instance).ToString();
}
}

/// <summary>
/// Convert between primitive types Edm.TimeOfDay and string
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
internal sealed class TimeOnlyConverter : PrimitiveTypeConverter
{
/// <summary>
/// Create an instance of primitive type from a string representation
/// </summary>
/// <param name="text">The string representation</param>
/// <returns>An instance of primitive type</returns>
internal override object Parse(string text)
{
return PlatformHelper.ConvertStringToTimeOnly(text);
}

/// <summary>
/// Convert an instance of primitive type to string
/// </summary>
/// <param name="instance">The instance</param>
/// <returns>The string representation of the instance</returns>
internal override string ToString(object instance)
{
return ((TimeOfDay)(TimeOnly)instance).ToString();
}
}
}
12 changes: 12 additions & 0 deletions src/Microsoft.OData.Core/Evaluation/EdmValueUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
xuzhg marked this conversation as resolved.
Show resolved Hide resolved

if (primitiveValue is DateTimeOffset)
{
IEdmTemporalTypeReference dateTimeOffsetType = (IEdmTemporalTypeReference)EnsurePrimitiveType(type, EdmPrimitiveTypeKind.DateTimeOffset);
Expand All @@ -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);
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
return new EdmTimeOfDayConstant(timeOfDayType, timeOnly);
}

if (primitiveValue is TimeSpan)
{
IEdmTemporalTypeReference timeType = (IEdmTemporalTypeReference)EnsurePrimitiveType(type, EdmPrimitiveTypeKind.Duration);
Expand Down
12 changes: 12 additions & 0 deletions src/Microsoft.OData.Core/Evaluation/LiteralFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ namespace Microsoft.OData.Evaluation
#if ODATA_CORE
using Microsoft.OData.Edm;
using Microsoft.Spatial;
using System.Globalization;
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
#else
using System.Xml.Linq;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.Spatial;
using ExpressionConstants = XmlConstants;
using System.Globalization;
#endif

/// <summary>
Expand Down Expand Up @@ -223,6 +225,11 @@ private static string FormatRawLiteral(object value)
return value.ToString();
}

if (value is DateOnly dateOnly)
{
return ((Date)dateOnly).ToString();
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@habbes habbes Oct 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether it's necessary to cast to Date here? Could we consider creating Date-to-string helper function that could be used to ensure consistency for both Date and DateOnly but could allow us to work with DateOnly without having to cast to Date? I think if DateOnly is used often or the preferred choice in the future, we should consider having first-class support for it and avoid the overhead of casting to Date when possible. If that's a lot of work, it could be done in a follow-up PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the next breaking change version (9 version), we should remove 'Date' 'TimeOfDay' and use 'DateOnly' and 'TimeOnly' types introduced in .NET.
In current 8 version, I try to avoid breaking change. And most important, we should make sure to get the same 'literal' string no matter whether customers are using 'Date' or 'DateOnly'.

DateOnly has several 'ToString()' overloads, the string literal varys along with the formatter provider. 'Cast' to Date then call 'ToString' on 'Date' is a 'center' method and make sure we get the same literal string no matter what type is. And to call 'ToString()' is a straightforward. There's no need to create a helper method to wrapper 'ToString()' again. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was curious about the potential overhead of casting to Edm.Date so I ran some tests to measure the ToString() scenario. To my surprise, casting to Edm.Date then call ToString() (i.e. ((Date)dateOnly).ToString() appears to be faster than dateOnly.ToString("yyyy-MM-dd", InvariantCulture).

This issue appears to be specific case of DateOnly.ToString(format, provider) overload which verifies that format is a valid date-only format, and this verification happens in O(n). You can bypass this overhead by converting the DateOnly to DateTime then calling DateTime.ToString("yyyy-MM-dd", InvariantCulture). This happens to be faster than casting to Edm.Date.

image

Here's the benchmark code.

This was meant to verify whether there's an actual overhead of casting DateOnly to Edm.Date and that we can avoid that perf overhead by handling DateOnly directly

}

if (value is DateTimeOffset)
{
return XmlConvert.ToString((DateTimeOffset)value);
Expand All @@ -233,6 +240,11 @@ private static string FormatRawLiteral(object value)
return value.ToString();
}

if (value is TimeOnly timeOnly)
{
return ((TimeOfDay)timeOnly).ToString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar concerns as https://github.com/OData/odata.net/pull/3078/files#r1792817270 on whether we can avoid casting to TimeOfDay and handle TimeOnly directly.

}

if (value is TimeSpan)
{
return EdmValueWriter.DurationAsXml((TimeSpan)value);
Expand Down
10 changes: 10 additions & 0 deletions src/Microsoft.OData.Core/Json/JsonWriterExtensions.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
}

if (value is TimeOfDay)
{
return jsonWriter.WriteValueAsync((TimeOfDay)value);
}

if (value is TimeOnly timeOnly)
{
return jsonWriter.WriteValueAsync(timeOnly); // will implicitly to call 'TimeOfDay' version
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
}

return TaskUtils.GetFaultedTask(
new ODataException(ODataErrorStrings.ODataJsonWriter_UnsupportedValueType(value.GetType().FullName)));
}
Expand Down
15 changes: 15 additions & 0 deletions src/Microsoft.OData.Core/Json/JsonWriterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
4 changes: 3 additions & 1 deletion src/Microsoft.OData.Core/Json/ODataJsonWriterUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
{
sb.Append('"').Append(valueAsString).Append('"');
}
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.OData.Core/Metadata/EdmLibraryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Comment on lines +143 to +147

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When passing primitive values like this (true, false) to arguments of a method, consider explicitly adding the argument name for readability.

I know this is like this for the rest of the class but consider that as a refactor outside of this PR too.

#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
Expand Down
12 changes: 12 additions & 0 deletions src/Microsoft.OData.Core/ODataPayloadValueConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ private static object ConvertStringValue(string stringValue, Type targetType)
return PlatformHelper.ConvertStringToTimeOfDay(stringValue);
}

// DateOnly
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
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))
{
Expand Down
12 changes: 12 additions & 0 deletions src/Microsoft.OData.Core/ODataRawValueUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,25 @@ internal static bool TryConvertPrimitiveToString(object value, out string result
return true;
}

if ( value is DateOnly dateOnly)
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
{
result = ODataRawValueConverter.ToString(dateOnly);
return true;
}

if (value is TimeOfDay)
{
// Edm.TimeOfDay
result = ODataRawValueConverter.ToString((TimeOfDay)value);
return true;
}

if (value is TimeOnly timeOnly)
{
result = ODataRawValueConverter.ToString(timeOnly);
return true;
}

result = null;
return false;
}
Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
return new DateTimeOffset(dateValue.Year, dateValue.Month, dateValue.Day, 0, 0, 0, new TimeSpan(0));
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
}

break;

case EdmPrimitiveTypeKind.Date:
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@

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
20 changes: 20 additions & 0 deletions src/Microsoft.OData.Edm/Schema/Date.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,26 @@ public static implicit operator Date(DateTime operand)
return new Date(operand.Year, operand.Month, operand.Day);
}

/// <summary>
/// Convert Date to Clr DateOnly
/// </summary>
/// <param name="operand">Date Value</param>
/// <returns>DateTime Value which represent the Date</returns>
public static implicit operator DateOnly(Date operand)
{
return DateOnly.FromDateTime(operand.dateTime);
}

/// <summary>
/// Convert Clr DateOnly to Date
/// </summary>
/// <param name="operand">DateOnly Value</param>
/// <returns>Date Value from DateOnly</returns>
public static implicit operator Date(DateOnly operand)
{
return new Date(operand.Year, operand.Month, operand.Day);
}

/// <summary>
/// Convert Date to String
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions src/Microsoft.OData.Edm/Schema/TimeOfDay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,26 @@ public static implicit operator TimeOfDay(TimeSpan timeSpan)
}
}

/// <summary>
/// Convert TimeOfDay to .Net TimeOnly
/// </summary>
/// <param name="time">TimeOfDay Value</param>
/// <returns>TimeOnly Value which represent the TimeOfDay</returns>
public static implicit operator TimeOnly(TimeOfDay time)
{
return TimeOnly.FromTimeSpan(time.timeSpan);
}

/// <summary>
/// Convert .Net Clr TimeOnly to TimeOfDay
/// </summary>
/// <param name="timeOnly">TimeOnly Value</param>
/// <returns>TimeOfDay Value from TimeOnly</returns>
public static implicit operator TimeOfDay(TimeOnly timeOnly)
{
return new TimeOfDay(timeOnly.Ticks);
}

/// <summary>
/// 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.
Expand Down
32 changes: 32 additions & 0 deletions src/PlatformHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,22 @@ internal static Date ConvertStringToDate(string text)
return date;
}

/// <summary>
/// Converts a string to a DateOnly.
/// </summary>
/// <param name="text">String to be converted.</param>
/// <returns>DateOnly value</returns>
xuzhg marked this conversation as resolved.
Show resolved Hide resolved
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;
}

/// <summary>
/// Converts a string to a TimeOfDay.
/// </summary>
Expand Down Expand Up @@ -267,6 +283,22 @@ internal static TimeOfDay ConvertStringToTimeOfDay(string text)

return timeOfDay;
}

/// <summary>
/// Converts a string to a TimeOnly.
/// </summary>
/// <param name="text">String to be converted.</param>
/// <returns>TimeOnly value</returns>
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

/// <summary>
Expand Down
Loading
Loading