Skip to content

Commit 7ad242d

Browse files
xuzhgStéphane Mitermite
authored and
Stéphane Mitermite
committed
Support DateOnly and TimeOnly (OData#450)
* Support DateOnly and TimeOnly * Update the nuget nuspec to include .NET 6 target framework * Resolve review comment. Thanks @juliano Leal Goncalves
1 parent b23b282 commit 7ad242d

17 files changed

+627
-16
lines changed

src/Microsoft.AspNetCore.OData.Nightly.nuspec

+10-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<package>
33
<metadata>
44
<id>Microsoft.AspNetCore.OData</id>
5-
<title>Microsoft ASP.NET Core 3.x and 5.x for OData v4.0</title>
5+
<title>Microsoft ASP.NET Core 3.x, 5.x and 6.x for OData v4.0</title>
66
<version>$VersionFullSemantic$-Nightly$NightlyBuildVersion$</version>
77
<authors>OData (.NET Foundation)</authors>
88
<copyright>&#169; .NET Foundation and Contributors. All rights reserved.</copyright>
@@ -28,6 +28,12 @@
2828
<dependency id="Microsoft.OData.Edm" version="$ODataLibPackageDependency$" />
2929
<dependency id="Microsoft.Spatial" version="$ODataLibPackageDependency$" />
3030
</group>
31+
<group targetFramework=".net6.0">
32+
<dependency id="Microsoft.OData.ModelBuilder" version="$ODataModelBuilderPackageDependency$" />
33+
<dependency id="Microsoft.OData.Core" version="$ODataLibPackageDependency$" />
34+
<dependency id="Microsoft.OData.Edm" version="$ODataLibPackageDependency$" />
35+
<dependency id="Microsoft.Spatial" version="$ODataLibPackageDependency$" />
36+
</group>
3137
</dependencies>
3238
</metadata>
3339
<files>
@@ -37,6 +43,9 @@
3743
<file src="$ProductRoot$\net5.0\Microsoft.AspNetCore.OData.dll" target="lib\net5.0" />
3844
<file src="$ProductRoot$\net5.0\Microsoft.AspNetCore.OData.xml" target="lib\net5.0" />
3945
<file src="$ProductRoot$\net5.0\Microsoft.AspNetCore.OData.pdb" target="lib\net5.0" />
46+
<file src="$ProductRoot$\net6.0\Microsoft.AspNetCore.OData.dll" target="lib\net6.0" />
47+
<file src="$ProductRoot$\net6.0\Microsoft.AspNetCore.OData.xml" target="lib\net6.0" />
48+
<file src="$ProductRoot$\net6.0\Microsoft.AspNetCore.OData.pdb" target="lib\net6.0" />
4049
<file src="$SourcesRoot$\images\odata.png" target="images\" />
4150
</files>
4251
</package>

src/Microsoft.AspNetCore.OData.Release.nuspec

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<package>
33
<metadata>
44
<id>Microsoft.AspNetCore.OData</id>
5-
<title>Microsoft ASP.NET Core 3.x and 5.x for OData v4.0</title>
5+
<title>Microsoft ASP.NET Core 3.x, 5.x and 6.x for OData v4.0</title>
66
<version>$VersionNuGetSemantic$</version>
77
<authors>OData (.NET Foundation)</authors>
88
<copyright>&#169; .NET Foundation and Contributors. All rights reserved.</copyright>
@@ -22,7 +22,13 @@
2222
<dependency id="Microsoft.OData.Edm" version="$ODataLibPackageDependency$" />
2323
<dependency id="Microsoft.Spatial" version="$ODataLibPackageDependency$" />
2424
</group>
25-
<group targetFramework=".net5.0">
25+
<group targetFramework=".net5.0">
26+
<dependency id="Microsoft.OData.ModelBuilder" version="$ODataModelBuilderPackageDependency$" />
27+
<dependency id="Microsoft.OData.Core" version="$ODataLibPackageDependency$" />
28+
<dependency id="Microsoft.OData.Edm" version="$ODataLibPackageDependency$" />
29+
<dependency id="Microsoft.Spatial" version="$ODataLibPackageDependency$" />
30+
</group>
31+
<group targetFramework=".net6.0">
2632
<dependency id="Microsoft.OData.ModelBuilder" version="$ODataModelBuilderPackageDependency$" />
2733
<dependency id="Microsoft.OData.Core" version="$ODataLibPackageDependency$" />
2834
<dependency id="Microsoft.OData.Edm" version="$ODataLibPackageDependency$" />
@@ -37,6 +43,9 @@
3743
<file src="$ProductRoot$\net5.0\Microsoft.AspNetCore.OData.dll" target="lib\net5.0" />
3844
<file src="$ProductRoot$\net5.0\Microsoft.AspNetCore.OData.xml" target="lib\net5.0" />
3945
<file src="$ProductRoot$\net5.0\Microsoft.AspNetCore.OData.pdb" target="lib\net5.0" />
46+
<file src="$ProductRoot$\net6.0\Microsoft.AspNetCore.OData.dll" target="lib\net6.0" />
47+
<file src="$ProductRoot$\net6.0\Microsoft.AspNetCore.OData.xml" target="lib\net6.0" />
48+
<file src="$ProductRoot$\net6.0\Microsoft.AspNetCore.OData.pdb" target="lib\net6.0" />
4049
<file src="$SourcesRoot$\images\odata.png" target="images\" />
4150
</files>
4251
</package>

src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs

+24
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,30 @@ public static bool IsDateTime(Type clrType)
9090
return Type.GetTypeCode(underlyingTypeOrSelf) == TypeCode.DateTime;
9191
}
9292

93+
#if NET6_0
94+
/// <summary>
95+
/// Determine if a type is a <see cref="DateOnly"/>.
96+
/// </summary>
97+
/// <param name="clrType">The type to test.</param>
98+
/// <returns>True if the type is a DateOnly; false otherwise.</returns>
99+
public static bool IsDateOnly(this Type clrType)
100+
{
101+
Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType);
102+
return underlyingTypeOrSelf == typeof(DateOnly);
103+
}
104+
105+
/// <summary>
106+
/// Determine if a type is a <see cref="TimeOnly"/>.
107+
/// </summary>
108+
/// <param name="clrType">The type to test.</param>
109+
/// <returns>True if the type is a TimeOnly; false otherwise.</returns>
110+
public static bool IsTimeOnly(this Type clrType)
111+
{
112+
Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType);
113+
return underlyingTypeOrSelf == typeof(TimeOnly);
114+
}
115+
#endif
116+
93117
/// <summary>
94118
/// Determine if a type is a TimeSpan.
95119
/// </summary>

src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs

+7
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ static DefaultODataTypeMapper()
111111
BuildTypeMapping<char>(EdmPrimitiveTypeKind.String, isStandard: false);
112112
BuildTypeMapping<DateTime?>(EdmPrimitiveTypeKind.DateTimeOffset, isStandard: false);
113113
BuildTypeMapping<DateTime>(EdmPrimitiveTypeKind.DateTimeOffset, isStandard: false);
114+
115+
#if NET6_0
116+
BuildTypeMapping<DateOnly?>(EdmPrimitiveTypeKind.Date, isStandard: false);
117+
BuildTypeMapping<DateOnly>(EdmPrimitiveTypeKind.Date, isStandard: false);
118+
BuildTypeMapping<TimeOnly?>(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false);
119+
BuildTypeMapping<TimeOnly>(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false);
120+
#endif
114121
}
115122
#endregion
116123

src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs

+20
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,26 @@ public static object ConvertPrimitiveValue(object value, Type type, TimeZoneInfo
137137

138138
throw new ValidationException(Error.Format(SRResources.PropertyMustBeBoolean));
139139
}
140+
#if NET6_0
141+
else if (type == typeof(DateOnly))
142+
{
143+
if (value is Date dt)
144+
{
145+
return new DateOnly(dt.Year, dt.Month, dt.Day);
146+
}
147+
148+
throw new ValidationException(Error.Format(SRResources.PropertyMustBeDateTimeOffsetOrDate));
149+
}
150+
else if (type == typeof(TimeOnly))
151+
{
152+
if (value is TimeOfDay tod)
153+
{
154+
return new TimeOnly(tod.Hours, tod.Minutes, tod.Seconds, (int)tod.Milliseconds);
155+
}
156+
157+
throw new ValidationException(Error.Format(SRResources.PropertyMustBeTimeOfDay));
158+
}
159+
#endif
140160
else
141161
{
142162
if (TypeHelper.TryGetInstance(type, value, out var result))

src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System;
99
using System.Collections.Generic;
1010
using System.ComponentModel;
11+
using System.Diagnostics.CodeAnalysis;
12+
using System.Globalization;
1113
using System.Linq;
1214
using System.Text;
1315
using Microsoft.AspNetCore.Mvc;
@@ -102,6 +104,7 @@ private static ODataInnerError ToODataInnerError(this Dictionary<string, object>
102104

103105
// Convert the model state errors in to a string (for debugging only).
104106
// This should be improved once ODataError allows more details.
107+
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "The default format provider is fine here.")]
105108
private static string ConvertModelStateErrors(this IReadOnlyDictionary<string, object> errors)
106109
{
107110
StringBuilder builder = new StringBuilder();

src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs

+16
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,22 @@ internal static object ConvertPrimitiveValue(object value, IEdmPrimitiveTypeRefe
144144
return tod;
145145
}
146146

147+
#if NET6_0
148+
// Since ODL doesn't support "DateOnly", we have to use Date defined in ODL.
149+
if (primitiveType != null && primitiveType.IsDate() && TypeHelper.IsDateOnly(type))
150+
{
151+
DateOnly dateOnly = (DateOnly)value;
152+
return new Date(dateOnly.Year, dateOnly.Month, dateOnly.Day);
153+
}
154+
155+
// Since ODL doesn't support "TimeOnly", we have to use TimeOfDay defined in ODL.
156+
if (primitiveType != null && primitiveType.IsTimeOfDay() && TypeHelper.IsTimeOnly(type))
157+
{
158+
TimeOnly timeOnly = (TimeOnly)value;
159+
return new TimeOfDay(timeOnly.Hour, timeOnly.Minute, timeOnly.Second, timeOnly.Millisecond);
160+
}
161+
#endif
162+
147163
return ConvertUnsupportedPrimitives(value, timeZoneInfo);
148164
}
149165

src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netcoreapp3.1;net5.0</TargetFrameworks>
4+
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
55
<RootNamespace>Microsoft.AspNetCore.OData</RootNamespace>
66
<DocumentationFile>$(OutputPath)$(AssemblyName).xml</DocumentationFile>
77
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>

src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml

+14
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,20 @@
10891089
<param name="clrType">The type to test.</param>
10901090
<returns>True if the type is a DateTime; false otherwise.</returns>
10911091
</member>
1092+
<member name="M:Microsoft.AspNetCore.OData.Common.TypeHelper.IsDateOnly(System.Type)">
1093+
<summary>
1094+
Determine if a type is a <see cref="T:System.DateOnly"/>.
1095+
</summary>
1096+
<param name="clrType">The type to test.</param>
1097+
<returns>True if the type is a DateOnly; false otherwise.</returns>
1098+
</member>
1099+
<member name="M:Microsoft.AspNetCore.OData.Common.TypeHelper.IsTimeOnly(System.Type)">
1100+
<summary>
1101+
Determine if a type is a <see cref="T:System.TimeOnly"/>.
1102+
</summary>
1103+
<param name="clrType">The type to test.</param>
1104+
<returns>True if the type is a TimeOnly; false otherwise.</returns>
1105+
</member>
10921106
<member name="M:Microsoft.AspNetCore.OData.Common.TypeHelper.IsTimeSpan(System.Type)">
10931107
<summary>
10941108
Determine if a type is a TimeSpan.

src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs

+19
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,25 @@ internal class ClrCanonicalFunctions
121121
new KeyValuePair<string, PropertyInfo>(MillisecondFunctionName, typeof(TimeOfDay).GetProperty("Milliseconds")),
122122
}.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
123123

124+
#if NET6_0
125+
// DateOnly properties
126+
public static readonly Dictionary<string, PropertyInfo> DateOnlyProperties = new Dictionary<string, PropertyInfo>
127+
{
128+
{ YearFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Year)) },
129+
{ MonthFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Month)) },
130+
{ DayFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Day)) }
131+
};
132+
133+
// TimeOnly properties
134+
public static readonly Dictionary<string, PropertyInfo> TimeOnlyProperties = new Dictionary<string, PropertyInfo>
135+
{
136+
{ HourFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Hour)) },
137+
{ MinuteFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Minute)) },
138+
{ SecondFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Second)) },
139+
{ MillisecondFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Millisecond)) }
140+
};
141+
#endif
142+
124143
// TimeSpan properties
125144
public static readonly Dictionary<string, PropertyInfo> TimeSpanProperties = new[]
126145
{

src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs

+70-8
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ public static Expression CreateBinaryExpression(BinaryOperatorKind binaryOperato
9696
right = CreateTimeBinaryExpression(right, querySettings);
9797
}
9898

99+
#if NET6_0
100+
if ((IsType<DateOnly>(leftUnderlyingType) && IsDate(rightUnderlyingType)) ||
101+
(IsDate(leftUnderlyingType) && IsType<DateOnly>(rightUnderlyingType)))
102+
{
103+
left = CreateDateBinaryExpression(left, querySettings);
104+
right = CreateDateBinaryExpression(right, querySettings);
105+
}
106+
else if((IsType<TimeOnly>(leftUnderlyingType) && IsTimeOfDay(rightUnderlyingType)) ||
107+
(IsTimeOfDay(leftUnderlyingType) && IsType<TimeOnly>(rightUnderlyingType)))
108+
{
109+
left = CreateTimeBinaryExpression(left, querySettings);
110+
right = CreateTimeBinaryExpression(right, querySettings);
111+
}
112+
#endif
113+
99114
if (left.Type != right.Type)
100115
{
101116
// one of them must be nullable and the other is not.
@@ -381,6 +396,16 @@ private static Expression GetProperty(Expression source, string propertyName, OD
381396
{
382397
return MakePropertyAccess(ClrCanonicalFunctions.TimeSpanProperties[propertyName], source, querySettings);
383398
}
399+
#if NET6_0
400+
else if (IsType<DateOnly>(source.Type))
401+
{
402+
return MakePropertyAccess(ClrCanonicalFunctions.DateOnlyProperties[propertyName], source, querySettings);
403+
}
404+
else if (IsType<TimeOnly>(source.Type))
405+
{
406+
return MakePropertyAccess(ClrCanonicalFunctions.TimeOnlyProperties[propertyName], source, querySettings);
407+
}
408+
#endif
384409

385410
return source;
386411
}
@@ -413,9 +438,9 @@ private static Expression CreateTimeBinaryExpression(Expression source, ODataQue
413438
Expression second = GetProperty(source, ClrCanonicalFunctions.SecondFunctionName, querySettings);
414439
Expression milliSecond = GetProperty(source, ClrCanonicalFunctions.MillisecondFunctionName, querySettings);
415440

416-
Expression hourTicks = Expression.Multiply(Expression.Convert(hour, typeof(long)), Expression.Constant(TimeOfDay.TicksPerHour));
417-
Expression minuteTicks = Expression.Multiply(Expression.Convert(minute, typeof(long)), Expression.Constant(TimeOfDay.TicksPerMinute));
418-
Expression secondTicks = Expression.Multiply(Expression.Convert(second, typeof(long)), Expression.Constant(TimeOfDay.TicksPerSecond));
441+
Expression hourTicks = Expression.Multiply(Expression.Convert(hour, typeof(long)), Expression.Constant(TimeSpan.TicksPerHour, typeof(long)));
442+
Expression minuteTicks = Expression.Multiply(Expression.Convert(minute, typeof(long)), Expression.Constant(TimeSpan.TicksPerMinute, typeof(long)));
443+
Expression secondTicks = Expression.Multiply(Expression.Convert(second, typeof(long)), Expression.Constant(TimeSpan.TicksPerSecond, typeof(long)));
419444

420445
// return (hour * TicksPerHour + minute * TicksPerMinute + second * TicksPerSecond + millisecond)
421446
Expression result = Expression.Add(hourTicks, Expression.Add(minuteTicks, Expression.Add(secondTicks, Expression.Convert(milliSecond, typeof(long)))));
@@ -451,6 +476,18 @@ private static Expression ConvertToDateTimeRelatedConstExpression(Expression sou
451476
{
452477
return Expression.Constant(timeOfDay.Value, typeof(TimeOfDay));
453478
}
479+
480+
#if NET6_0
481+
if (parameterizedConstantValue is DateOnly dateOnly)
482+
{
483+
return Expression.Constant(dateOnly, typeof(DateOnly));
484+
}
485+
486+
else if (parameterizedConstantValue is TimeOnly timeOnly)
487+
{
488+
return Expression.Constant(timeOnly, typeof(TimeOnly));
489+
}
490+
#endif
454491
}
455492

456493
return source;
@@ -468,21 +505,34 @@ public static bool IsDoubleOrDecimal(Type type)
468505

469506
public static bool IsDateAndTimeRelated(Type type)
470507
{
471-
return IsType<Date>(type) ||
472-
IsType<DateTime>(type) ||
473-
IsType<DateTimeOffset>(type) ||
474-
IsType<TimeOfDay>(type) ||
475-
IsType<TimeSpan>(type);
508+
return IsType<Date>(type)
509+
|| IsType<DateTime>(type)
510+
|| IsType<DateTimeOffset>(type)
511+
|| IsType<TimeOfDay>(type)
512+
|| IsType<TimeSpan>(type)
513+
#if NET6_0
514+
|| IsType<DateOnly>(type)
515+
|| IsType<TimeOnly>(type)
516+
#endif
517+
;
476518
}
477519

478520
public static bool IsDateRelated(Type type)
479521
{
522+
#if NET6_0
523+
return IsType<Date>(type) || IsType<DateTime>(type) || IsType<DateTimeOffset>(type) || IsType<DateOnly>(type);
524+
#else
480525
return IsType<Date>(type) || IsType<DateTime>(type) || IsType<DateTimeOffset>(type);
526+
#endif
481527
}
482528

483529
public static bool IsTimeRelated(Type type)
484530
{
531+
#if NET6_0
532+
return IsType<TimeOfDay>(type) || IsType<DateTime>(type) || IsType<DateTimeOffset>(type) || IsType<TimeSpan>(type) || IsType<TimeOnly>(type);
533+
#else
485534
return IsType<TimeOfDay>(type) || IsType<DateTime>(type) || IsType<DateTimeOffset>(type) || IsType<TimeSpan>(type);
535+
#endif
486536
}
487537

488538
public static bool IsDateOrOffset(Type type)
@@ -510,6 +560,18 @@ public static bool IsDate(Type type)
510560
return IsType<Date>(type);
511561
}
512562

563+
#if NET6_0
564+
public static bool IsDateOnly(this Type type)
565+
{
566+
return IsType<DateOnly>(type);
567+
}
568+
569+
public static bool IsTimeOnly(this Type type)
570+
{
571+
return IsType<TimeOnly>(type);
572+
}
573+
#endif
574+
513575
public static bool IsInteger(Type type)
514576
{
515577
return IsType<short>(type) || IsType<int>(type) || IsType<long>(type);

0 commit comments

Comments
 (0)