From b2b5fdbbb405c19b433bd4a052cfe69818b3c9fa Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Wed, 19 Jan 2022 19:09:33 -0800 Subject: [PATCH 1/3] Support DateOnly and TimeOnly --- .../Common/TypeHelper.cs | 13 + .../Edm/DefaultODataTypeMapper.cs | 7 + .../Edm/EdmPrimitiveHelper.cs | 22 + .../Extensions/SerializableErrorExtensions.cs | 3 + .../Serialization/ODataPrimitiveSerializer.cs | 17 + .../Microsoft.AspNetCore.OData.csproj | 2 +- .../Query/ClrCanonicalFunctions.cs | 19 + .../Expressions/ExpressionBinderHelper.cs | 49 ++- .../QueryBinder.SingleValueFunctionCall.cs | 27 +- .../Query/Expressions/QueryBinder.cs | 6 + .../Routing/ODataRouteDebugMiddleware.cs | 2 + .../DateAndTimeOfDayWithEfTest.cs | 399 ++++++++++++++++++ ...icrosoft.AspNetCore.OData.E2E.Tests.csproj | 10 +- .../Microsoft.AspNetCore.OData.Tests.csproj | 2 +- 14 files changed, 569 insertions(+), 9 deletions(-) create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/DateOnlyTimeOnly/DateAndTimeOfDayWithEfTest.cs diff --git a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs index f68e7a9c5..283630220 100644 --- a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs @@ -90,6 +90,19 @@ public static bool IsDateTime(Type clrType) return Type.GetTypeCode(underlyingTypeOrSelf) == TypeCode.DateTime; } +#if NET6_0 + internal static bool IsDateOnly(Type clrType) + { + Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); + return underlyingTypeOrSelf == typeof(DateOnly); + } + + internal static bool IsTimeOnly(Type clrType) + { + Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); + return underlyingTypeOrSelf == typeof(TimeOnly); + } +#endif /// /// Determine if a type is a TimeSpan. /// diff --git a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs index e76855eaa..c4c897e40 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs @@ -111,6 +111,13 @@ static DefaultODataTypeMapper() BuildTypeMapping(EdmPrimitiveTypeKind.String, isStandard: false); BuildTypeMapping(EdmPrimitiveTypeKind.DateTimeOffset, isStandard: false); BuildTypeMapping(EdmPrimitiveTypeKind.DateTimeOffset, isStandard: false); + +#if NET6_0 + BuildTypeMapping(EdmPrimitiveTypeKind.Date, isStandard: false); + BuildTypeMapping(EdmPrimitiveTypeKind.Date, isStandard: false); + BuildTypeMapping(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false); + BuildTypeMapping(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false); +#endif } #endregion diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs index 710652a8d..9c963f583 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs @@ -137,6 +137,28 @@ public static object ConvertPrimitiveValue(object value, Type type, TimeZoneInfo throw new ValidationException(Error.Format(SRResources.PropertyMustBeBoolean)); } +#if NET6_0 + else if (type == typeof(DateOnly)) + { + if (value is Date) + { + Date dt = (Date)value; + return new DateOnly(dt.Year, dt.Month, dt.Day); + } + + throw new ValidationException(Error.Format(SRResources.PropertyMustBeDateTimeOffsetOrDate)); + } + else if (type == typeof(TimeOnly)) + { + if (value is TimeOfDay) + { + TimeOfDay tod = (TimeOfDay)value; + return new TimeOnly(tod.Hours, tod.Minutes, tod.Seconds, (int)tod.Milliseconds); + } + + throw new ValidationException(Error.Format(SRResources.PropertyMustBeTimeOfDay)); + } +#endif else { if (TypeHelper.TryGetInstance(type, value, out var result)) diff --git a/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs index c1b41d364..e4879f893 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs @@ -8,6 +8,8 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Text; using Microsoft.AspNetCore.Mvc; @@ -102,6 +104,7 @@ private static ODataInnerError ToODataInnerError(this Dictionary // Convert the model state errors in to a string (for debugging only). // This should be improved once ODataError allows more details. + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "")] private static string ConvertModelStateErrors(this IReadOnlyDictionary errors) { StringBuilder builder = new StringBuilder(); diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs index e69be624a..876c3dd2f 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs @@ -144,6 +144,23 @@ internal static object ConvertPrimitiveValue(object value, IEdmPrimitiveTypeRefe return tod; } +#if NET6_0 + // Since ODL doesn't support "DateOnly" and "TimeOnly", we have to use Date and TimeOfDay defined in ODL as a bridge. + if (primitiveType != null && primitiveType.IsDate() && TypeHelper.IsDateOnly(type)) + { + DateOnly dateOnly = (DateOnly)value; + Date dt = new Date(dateOnly.Year, dateOnly.Month, dateOnly.Day); + return dt; + } + + if (primitiveType != null && primitiveType.IsTimeOfDay() && TypeHelper.IsTimeOnly(type)) + { + TimeOnly timeOnly = (TimeOnly)value; + TimeOfDay tod = new TimeOfDay(timeOnly.Hour, timeOnly.Minute, timeOnly.Second, timeOnly.Millisecond); + return tod; + } +#endif + return ConvertUnsupportedPrimitives(value, timeZoneInfo); } diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj index e771f51ed..75dc650d7 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 Microsoft.AspNetCore.OData $(OutputPath)$(AssemblyName).xml true diff --git a/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs b/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs index e9a0318f5..d0864e4ad 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs @@ -121,6 +121,25 @@ internal class ClrCanonicalFunctions new KeyValuePair(MillisecondFunctionName, typeof(TimeOfDay).GetProperty("Milliseconds")), }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); +#if NET6_0 + // DateOnly properties + public static readonly Dictionary DateOnlyProperties = new[] + { + new KeyValuePair(YearFunctionName, typeof(DateOnly).GetProperty("Year")), + new KeyValuePair(MonthFunctionName, typeof(DateOnly).GetProperty("Month")), + new KeyValuePair(DayFunctionName, typeof(DateOnly).GetProperty("Day")), + }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + // TimeOnly + public static readonly Dictionary TimeOnlyProperties = new[] + { + new KeyValuePair(HourFunctionName, typeof(TimeOnly).GetProperty("Hour")), + new KeyValuePair(MinuteFunctionName, typeof(TimeOnly).GetProperty("Minute")), + new KeyValuePair(SecondFunctionName, typeof(TimeOnly).GetProperty("Second")), + new KeyValuePair(MillisecondFunctionName, typeof(TimeOnly).GetProperty("Millisecond")), + }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); +#endif + // TimeSpan properties public static readonly Dictionary TimeSpanProperties = new[] { diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs index bd0b29dcb..a55d4ccea 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs @@ -96,6 +96,21 @@ public static Expression CreateBinaryExpression(BinaryOperatorKind binaryOperato right = CreateTimeBinaryExpression(right, querySettings); } +#if NET6_0 + if ((IsType(leftUnderlyingType) && IsDate(rightUnderlyingType)) || + (IsDate(leftUnderlyingType) && IsType(rightUnderlyingType))) + { + left = CreateDateBinaryExpression(left, querySettings); + right = CreateDateBinaryExpression(right, querySettings); + } + else if((IsType(leftUnderlyingType) && IsTimeOfDay(rightUnderlyingType)) || + (IsTimeOfDay(leftUnderlyingType) && IsType(rightUnderlyingType))) + { + left = CreateTimeBinaryExpression(left, querySettings); + right = CreateTimeBinaryExpression(right, querySettings); + } +#endif + if (left.Type != right.Type) { // one of them must be nullable and the other is not. @@ -381,6 +396,16 @@ private static Expression GetProperty(Expression source, string propertyName, OD { return MakePropertyAccess(ClrCanonicalFunctions.TimeSpanProperties[propertyName], source, querySettings); } +#if NET6_0 + else if (IsType(source.Type)) + { + return MakePropertyAccess(ClrCanonicalFunctions.DateOnlyProperties[propertyName], source, querySettings); + } + else if (IsType(source.Type)) + { + return MakePropertyAccess(ClrCanonicalFunctions.TimeOnlyProperties[propertyName], source, querySettings); + } +#endif return source; } @@ -407,15 +432,19 @@ private static Expression CreateTimeBinaryExpression(Expression source, ODataQue { source = ConvertToDateTimeRelatedConstExpression(source); + long ticksPerHour = 36000000000L; + long ticksPerMinute = 600000000L; + long ticksPerSecond = 10000000L; + // Hour, Minute, Second, Millisecond Expression hour = GetProperty(source, ClrCanonicalFunctions.HourFunctionName, querySettings); Expression minute = GetProperty(source, ClrCanonicalFunctions.MinuteFunctionName, querySettings); Expression second = GetProperty(source, ClrCanonicalFunctions.SecondFunctionName, querySettings); Expression milliSecond = GetProperty(source, ClrCanonicalFunctions.MillisecondFunctionName, querySettings); - Expression hourTicks = Expression.Multiply(Expression.Convert(hour, typeof(long)), Expression.Constant(TimeOfDay.TicksPerHour)); - Expression minuteTicks = Expression.Multiply(Expression.Convert(minute, typeof(long)), Expression.Constant(TimeOfDay.TicksPerMinute)); - Expression secondTicks = Expression.Multiply(Expression.Convert(second, typeof(long)), Expression.Constant(TimeOfDay.TicksPerSecond)); + Expression hourTicks = Expression.Multiply(Expression.Convert(hour, typeof(long)), Expression.Constant(ticksPerHour, typeof(long))); + Expression minuteTicks = Expression.Multiply(Expression.Convert(minute, typeof(long)), Expression.Constant(ticksPerMinute, typeof(long))); + Expression secondTicks = Expression.Multiply(Expression.Convert(second, typeof(long)), Expression.Constant(ticksPerSecond, typeof(long))); // return (hour * TicksPerHour + minute * TicksPerMinute + second * TicksPerSecond + millisecond) Expression result = Expression.Add(hourTicks, Expression.Add(minuteTicks, Expression.Add(secondTicks, Expression.Convert(milliSecond, typeof(long))))); @@ -451,6 +480,20 @@ private static Expression ConvertToDateTimeRelatedConstExpression(Expression sou { return Expression.Constant(timeOfDay.Value, typeof(TimeOfDay)); } + +#if NET6_0 + var dateOnly = parameterizedConstantValue as DateOnly?; + if (dateOnly != null) + { + return Expression.Constant(dateOnly.Value, typeof(DateOnly)); + } + + var timeOnly = parameterizedConstantValue as TimeOnly?; + if (timeOnly != null) + { + return Expression.Constant(timeOnly.Value, typeof(TimeOnly)); + } +#endif } return source; diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs index ee54130c1..494429ea9 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs @@ -348,7 +348,11 @@ protected virtual Expression BindDateRelatedProperty(SingleValueFunctionCallNode CheckArgumentNull(node, context); Expression[] arguments = BindArguments(node.Parameters, context); - Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateRelated(arguments[0].Type)); + Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsDateRelated(arguments[0].Type) +#if NET6_0 + || ExpressionBinderHelper.IsType(arguments[0].Type) +#endif + )); // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. Expression parameter = arguments[0]; @@ -359,6 +363,13 @@ protected virtual Expression BindDateRelatedProperty(SingleValueFunctionCallNode Contract.Assert(ClrCanonicalFunctions.DateProperties.ContainsKey(node.Name)); property = ClrCanonicalFunctions.DateProperties[node.Name]; } +#if NET6_0 + else if (ExpressionBinderHelper.IsType(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.DateOnlyProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.DateOnlyProperties[node.Name]; + } +#endif else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) { Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); @@ -384,7 +395,12 @@ protected virtual Expression BindTimeRelatedProperty(SingleValueFunctionCallNode CheckArgumentNull(node, context); Expression[] arguments = BindArguments(node.Parameters, context); - Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type))); + + Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type) +#if NET6_0 + || ExpressionBinderHelper.IsType(arguments[0].Type) +#endif + )); // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. Expression parameter = arguments[0]; @@ -395,6 +411,13 @@ protected virtual Expression BindTimeRelatedProperty(SingleValueFunctionCallNode Contract.Assert(ClrCanonicalFunctions.TimeOfDayProperties.ContainsKey(node.Name)); property = ClrCanonicalFunctions.TimeOfDayProperties[node.Name]; } +#if NET6_0 + else if (ExpressionBinderHelper.IsType(parameter.Type)) + { + Contract.Assert(ClrCanonicalFunctions.TimeOnlyProperties.ContainsKey(node.Name)); + property = ClrCanonicalFunctions.TimeOnlyProperties[node.Name]; + } +#endif else if (ExpressionBinderHelper.IsDateTime(parameter.Type)) { Contract.Assert(ClrCanonicalFunctions.DateTimeProperties.ContainsKey(node.Name)); diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs index 8788e756d..c4f171a03 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs @@ -1216,6 +1216,12 @@ internal static Expression ConvertNonStandardPrimitives(Expression source, Query // we handle enum conversions ourselves convertedExpression = source; } +#if NET6_0 + else if (TypeHelper.IsDateOnly(sourceType) || TypeHelper.IsTimeOnly(sourceType)) + { + convertedExpression = source; + } +#endif else { switch (Type.GetTypeCode(sourceType)) diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs index 87987d7d8..64c5f5aa3 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Net.Mime; using System.Text; @@ -163,6 +164,7 @@ internal static bool AcceptsJson(IHeaderDictionary headers) return result; } + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "")] private static void AppendRoute(StringBuilder builder, EndpointRouteInfo routeInfo) { builder.Append(""); diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateOnlyTimeOnly/DateAndTimeOfDayWithEfTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateOnlyTimeOnly/DateAndTimeOfDayWithEfTest.cs new file mode 100644 index 000000000..f5929d5ed --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateOnlyTimeOnly/DateAndTimeOfDayWithEfTest.cs @@ -0,0 +1,399 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +#if NET6_0 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Deltas; +using Microsoft.AspNetCore.OData.E2E.Tests.Extensions; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.DateOnlyTimeOnly +{ + public class DateOnlyAndTimeOnlyWithEfTest : WebApiTestBase + { + public DateOnlyAndTimeOnlyWithEfTest(WebApiTestFixture fixture) + :base(fixture) + { + } + + protected static void UpdateConfigureServices(IServiceCollection services) + { + string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Integrated Security=True;Initial Catalog=DateOnlyAndTimeOnlyModelContext8"; + services.AddDbContext(opt => opt.UseLazyLoadingProxies().UseSqlServer(connectionString)); + + services.ConfigureControllers(typeof(MetadataController), typeof(DateOnlyTimeOnlyModelsController)); + + services.AddControllers().AddOData(opt => opt.Count().Filter().OrderBy().Expand().SetMaxTop(null).Select() + .AddRouteComponents("odata", BuildEdmModel())); + } + + [Fact] + public async Task MetadataDocument_IncludesDateOnlyAndTimeOnlyProperties() + { + // Arrange + string Uri = "odata/$metadata"; + string expected = "\r\n" + +"\r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +" \r\n" + +""; + + // Remove indentation + expected = Regex.Replace(expected, @"\r\n\s*<", @"<"); + HttpClient client = CreateClient(); + + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(expected, await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task CanQueryEntitySet_WithDateOnlyAndTimeOnlyProperties() + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + + Assert.Equal(5, result["value"].Count()); + + // test one for each entity + Assert.Equal("2012-03-07", result["value"][0]["EndDay"]); + Assert.Equal(JValue.CreateNull(), result["value"][1]["PublishDay"]); + Assert.Equal("03:13:06.0080000", result["value"][2]["ResumeTime"]); + Assert.Equal(JValue.CreateNull(), result["value"][3]["EndTime"]); + Assert.Equal("00:05:03.0050000", result["value"][4]["CreatedTime"]); + } + + [Fact] + public async Task CanQuerySingleEntity_WithDateOnlyAndTimeOnlyProperties() + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels(2)"; + + string expect = @"{ + ""@odata.context"": ""http://localhost/odata/$metadata#DateOnlyTimeOnlyModels/$entity"", + ""@odata.type"": ""#Microsoft.AspNetCore.OData.E2E.Tests.DateOnlyTimeOnly.DateOnyTimeOnlyModel"", + ""@odata.id"": ""http://localhost/odata/DateOnlyTimeOnlyModels(2)"", + ""@odata.editLink"": ""DateOnlyTimeOnlyModels(2)"", + ""Id"": 2, + ""Birthday@odata.type"": ""#Date"", + ""Birthday"": ""2012-03-07"", + ""PublishDay"": null, + ""EndDay@odata.type"": ""#Date"", + ""EndDay"": ""2013-04-08"", + ""CreatedTime@odata.type"": ""#TimeOfDay"", + ""CreatedTime"": ""00:02:03.0050000"", + ""EndTime"": null, + ""ResumeTime@odata.type"": ""#TimeOfDay"", + ""ResumeTime"": ""02:12:05.0070000"" +}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json;odata.metadata=full")); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(JObject.Parse(expect), result); + } + + [Fact] + public async Task CanSelect_OnDateOnlyAndTimeOnlyProperties() + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels(3)?$select=Birthday,PublishDay,CreatedTime,ResumeTime"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal("2013-04-08", result["Birthday"]); + Assert.Equal("2018-09-18", result["PublishDay"]); + Assert.Equal("00:03:03.0050000", result["CreatedTime"]); + Assert.Equal("03:13:06.0080000", result["ResumeTime"]); + } + + [Theory] + [InlineData("?$filter=year(Birthday) eq 2015", "5")] + [InlineData("?$filter=month(PublishDay) eq 11", "1")] + [InlineData("?$filter=day(EndDay) ne 09", "1,2,4,5")] + [InlineData("?$filter=Birthday gt 2013-04-08", "4,5")] + [InlineData("?$filter=PublishDay eq null", "2,4")] // the following four cases are for nullable + [InlineData("?$filter=PublishDay eq 2018-09-18", "3")] + [InlineData("?$filter=PublishDay ne 2018-09-18", "1,5")] + [InlineData("?$filter=PublishDay lt 2019-12-31", "1,3")] + [InlineData("?$filter=EndTime ne null", "1,3,5")] + [InlineData("?$filter=CreatedTime eq 00:01:03.0050000", "1")] + [InlineData("?$filter=hour(EndTime) eq 01", "1")] + [InlineData("?$filter=minute(EndTime) eq 15", "5")] + [InlineData("?$filter=second(EndTime) eq 06", "3")] + [InlineData("?$filter=EndTime eq null", "2,4")] + [InlineData("?$filter=EndTime ge 00:03:05.0790000", "1,3,5")] + public async Task CanFilter_OnDateOnlyAndTimeOnlyProperties(string filter, string expect) + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels" + filter; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + string payload = await response.Content.ReadAsStringAsync(); + Assert.True(response.IsSuccessStatusCode); + + JObject result = await response.Content.ReadAsObject(); + Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); + } + + [Theory] + [InlineData("?$orderby=Birthday", "1,2,3,4,5")] + [InlineData("?$orderby=Birthday desc", "5,4,3,2,1")] + [InlineData("?$orderby=PublishDay", "2,4,1,3,5")] + [InlineData("?$orderby=PublishDay desc", "5,3,1,2,4")] + [InlineData("?$orderby=CreatedTime", "1,2,3,4,5")] + [InlineData("?$orderby=CreatedTime desc", "5,4,3,2,1")] + public async Task CanOrderBy_OnDateOnlyAndTimeOnlyProperties(string orderby, string expect) + { + // Arrange + string Uri = "odata/DateOnlyTimeOnlyModels" + orderby; + var request = new HttpRequestMessage(HttpMethod.Get, Uri); + HttpClient client = CreateClient(); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + + var result = JObject.Parse(await response.Content.ReadAsStringAsync()); + Assert.Equal(5, result["value"].Count()); + + Assert.Equal(expect, string.Join(",", result["value"].Select(e => e["Id"].ToString()))); + } + + [Fact] + public async Task PostEntity_WithDateOnlyAndTimeOnlyProperties() + { + // Arrange + const string Payload = "{" + + "\"Id\":99," + + "\"Birthday\":\"2099-01-01\"," + + "\"CreatedTime\":\"14:13:15.1790000\"," + + "\"EndDay\":\"1990-12-22\"}"; + + string Uri = "odata/DateOnlyTimeOnlyModels"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, Uri); + + request.Content = new StringContent(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = Payload.Length; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + } + + [Fact] + public async Task PutEntity_WithDateOnlyAndTimeOnlyProperties() + { + // Arrange + const string Payload = "{" + + "\"Birthday\":\"2199-01-02\"," + + "\"CreatedTime\":\"14:13:15.1790000\"}"; + + string Uri = "odata/DateOnlyTimeOnlyModels(3)"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, Uri); + + request.Content = new StringContent(Payload); + request.Content.Headers.ContentType = MediaTypeWithQualityHeaderValue.Parse("application/json"); + request.Content.Headers.ContentLength = Payload.Length; + HttpClient client = CreateClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(request); + + // Assert + Assert.True(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + } + + private static IEdmModel BuildEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("DateOnlyTimeOnlyModels"); + return builder.GetEdmModel(); + } + } + + public class DateOnlyTimeOnlyModelsController : ODataController + { + // private DateAndOnlyTimeOnlyModelContext _db; + private static IList _dateTimes = Enumerable.Range(1, 5).Select(i => + new DateOnyTimeOnlyModel + { + Id = i, + Birthday = new DateOnly(2010 + i, 1 + i, 5 + i), + + PublishDay = i % 2 == 0 ? null : new DateOnly(2015 + i, 12 - i, 15 + i), + + EndDay = new DateOnly(2010 + i + 1, 2 + i, 6 + i), + + CreatedTime = new TimeOnly(0, i, 3, 5), + + EndTime = i % 2 == 0 ? null : new TimeOnly(i, 10 + i, 3 + i, 5 + i), + + ResumeTime = new TimeOnly(i, 10 + i, 3 + i, 5 + i) + + }).ToList(); + + [EnableQuery] + public IActionResult Get() + { + return Ok(_dateTimes); + } + + [EnableQuery] + public IActionResult Get(int key) + { + DateOnyTimeOnlyModel dtm = _dateTimes.FirstOrDefault(e => e.Id == key); + if (dtm == null) + { + return NotFound(); + } + + return Ok(dtm); + } + + public IActionResult Post([FromBody]DateOnyTimeOnlyModel dt) + { + Assert.NotNull(dt); + + Assert.Equal(99, dt.Id); + Assert.Equal(new DateOnly(2099, 1, 1), dt.Birthday); + Assert.Equal(new TimeOnly(14, 13, 15, 179), dt.CreatedTime); + Assert.Equal(new DateOnly(1990, 12, 22), dt.EndDay); + + return Created(dt); + } + + public IActionResult Put(int key, [FromBody]Delta dt) + { + Assert.Equal(new[] { "Birthday", "CreatedTime" }, dt.GetChangedPropertyNames()); + + // Birthday + object value; + bool success = dt.TryGetPropertyValue("Birthday", out value); + Assert.True(success); + DateOnly dateOnly = Assert.IsType(value); + Assert.Equal(new DateOnly(2199, 1, 2), dateOnly); + + // CreatedTime + success = dt.TryGetPropertyValue("CreatedTime", out value); + Assert.True(success); + TimeOnly timeOnly = Assert.IsType(value); + Assert.Equal(new TimeOnly(14, 13, 15, 179), timeOnly); + return Updated(dt); + } + } + + // EF Core 6 doesn't support DateOnly and TimeOnly yet + public class DateAndOnlyTimeOnlyModelContext : DbContext + { + public DateAndOnlyTimeOnlyModelContext(DbContextOptions options) + : base(options) + { + } + + public DbSet DateTimes { get; set; } + + //protected override void OnModelCreating(ModelBuilder modelBuilder) + //{ + // modelBuilder.Entity().Property(c => c.EndDay).HasColumnType("date"); + // modelBuilder.Entity().Property(c => c.DeliverDay).HasColumnType("date"); + //} + } + + public class DateOnyTimeOnlyModel + { + public int Id { get; set; } + + public DateOnly Birthday { get; set; } + + public DateOnly? PublishDay { get; set; } + + public DateOnly EndDay { get; set; } + + public TimeOnly CreatedTime { get; set; } + + public TimeOnly? EndTime { get; set; } + + public TimeOnly ResumeTime { get; set; } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/Microsoft.AspNetCore.OData.E2E.Tests.csproj b/test/Microsoft.AspNetCore.OData.E2E.Tests/Microsoft.AspNetCore.OData.E2E.Tests.csproj index 5a9a9db31..37a58ce8c 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/Microsoft.AspNetCore.OData.E2E.Tests.csproj +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/Microsoft.AspNetCore.OData.E2E.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 Microsoft.AspNetCore.OData.E2E.Tests Microsoft.AspNetCore.OData.E2E.Tests @@ -38,7 +38,13 @@ - + + + + + + + diff --git a/test/Microsoft.AspNetCore.OData.Tests/Microsoft.AspNetCore.OData.Tests.csproj b/test/Microsoft.AspNetCore.OData.Tests/Microsoft.AspNetCore.OData.Tests.csproj index 02a7a265f..2f00e20f3 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Microsoft.AspNetCore.OData.Tests.csproj +++ b/test/Microsoft.AspNetCore.OData.Tests/Microsoft.AspNetCore.OData.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 Microsoft.AspNetCore.OData.Tests Microsoft.AspNetCore.OData.Tests From 85324e42ddfc5a22a2a2f97b3a2535e0358e686a Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Wed, 19 Jan 2022 19:23:33 -0800 Subject: [PATCH 2/3] Update the nuget nuspec to include .NET 6 target framework --- src/Microsoft.AspNetCore.OData.Nightly.nuspec | 11 ++++++++++- src/Microsoft.AspNetCore.OData.Release.nuspec | 13 +++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData.Nightly.nuspec b/src/Microsoft.AspNetCore.OData.Nightly.nuspec index ecb9ceb5e..b8044fbbb 100644 --- a/src/Microsoft.AspNetCore.OData.Nightly.nuspec +++ b/src/Microsoft.AspNetCore.OData.Nightly.nuspec @@ -2,7 +2,7 @@ Microsoft.AspNetCore.OData - Microsoft ASP.NET Core 3.x and 5.x for OData v4.0 + Microsoft ASP.NET Core 3.x, 5.x and 6.x for OData v4.0 $VersionFullSemantic$-Nightly$NightlyBuildVersion$ OData (.NET Foundation) © .NET Foundation and Contributors. All rights reserved. @@ -28,6 +28,12 @@ + + + + + + @@ -37,6 +43,9 @@ + + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData.Release.nuspec b/src/Microsoft.AspNetCore.OData.Release.nuspec index 03f233104..bff4cb866 100644 --- a/src/Microsoft.AspNetCore.OData.Release.nuspec +++ b/src/Microsoft.AspNetCore.OData.Release.nuspec @@ -2,7 +2,7 @@ Microsoft.AspNetCore.OData - Microsoft ASP.NET Core 3.x and 5.x for OData v4.0 + Microsoft ASP.NET Core 3.x, 5.x and 6.x for OData v4.0 $VersionNuGetSemantic$ OData (.NET Foundation) © .NET Foundation and Contributors. All rights reserved. @@ -22,7 +22,13 @@ - + + + + + + + @@ -37,6 +43,9 @@ + + + \ No newline at end of file From 090624a148a2899de9b3a0a03dd1f38e441c18c9 Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Thu, 20 Jan 2022 17:43:28 -0800 Subject: [PATCH 3/3] Resolve review comment. Thanks @Juliano Leal Goncalves --- .../Common/TypeHelper.cs | 15 ++++- .../Edm/DefaultODataTypeMapper.cs | 4 +- .../Edm/EdmPrimitiveHelper.cs | 6 +- .../Extensions/SerializableErrorExtensions.cs | 2 +- .../Serialization/ODataPrimitiveSerializer.cs | 9 ++- .../Microsoft.AspNetCore.OData.xml | 14 +++++ .../Query/ClrCanonicalFunctions.cs | 24 ++++---- .../Expressions/ExpressionBinderHelper.cs | 55 +++++++++++++------ .../QueryBinder.SingleValueFunctionCall.cs | 16 ++---- .../Routing/ODataRouteDebugMiddleware.cs | 2 +- 10 files changed, 90 insertions(+), 57 deletions(-) diff --git a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs index 283630220..5970ca142 100644 --- a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs @@ -91,18 +91,29 @@ public static bool IsDateTime(Type clrType) } #if NET6_0 - internal static bool IsDateOnly(Type clrType) + /// + /// Determine if a type is a . + /// + /// The type to test. + /// True if the type is a DateOnly; false otherwise. + public static bool IsDateOnly(this Type clrType) { Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); return underlyingTypeOrSelf == typeof(DateOnly); } - internal static bool IsTimeOnly(Type clrType) + /// + /// Determine if a type is a . + /// + /// The type to test. + /// True if the type is a TimeOnly; false otherwise. + public static bool IsTimeOnly(this Type clrType) { Type underlyingTypeOrSelf = GetUnderlyingTypeOrSelf(clrType); return underlyingTypeOrSelf == typeof(TimeOnly); } #endif + /// /// Determine if a type is a TimeSpan. /// diff --git a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs index c4c897e40..c740cc7fd 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs @@ -113,10 +113,10 @@ static DefaultODataTypeMapper() BuildTypeMapping(EdmPrimitiveTypeKind.DateTimeOffset, isStandard: false); #if NET6_0 - BuildTypeMapping(EdmPrimitiveTypeKind.Date, isStandard: false); BuildTypeMapping(EdmPrimitiveTypeKind.Date, isStandard: false); - BuildTypeMapping(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false); + BuildTypeMapping(EdmPrimitiveTypeKind.Date, isStandard: false); BuildTypeMapping(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false); + BuildTypeMapping(EdmPrimitiveTypeKind.TimeOfDay, isStandard: false); #endif } #endregion diff --git a/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs b/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs index 9c963f583..4226f0a08 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/EdmPrimitiveHelper.cs @@ -140,9 +140,8 @@ public static object ConvertPrimitiveValue(object value, Type type, TimeZoneInfo #if NET6_0 else if (type == typeof(DateOnly)) { - if (value is Date) + if (value is Date dt) { - Date dt = (Date)value; return new DateOnly(dt.Year, dt.Month, dt.Day); } @@ -150,9 +149,8 @@ public static object ConvertPrimitiveValue(object value, Type type, TimeZoneInfo } else if (type == typeof(TimeOnly)) { - if (value is TimeOfDay) + if (value is TimeOfDay tod) { - TimeOfDay tod = (TimeOfDay)value; return new TimeOnly(tod.Hours, tod.Minutes, tod.Seconds, (int)tod.Milliseconds); } diff --git a/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs index e4879f893..d56e142e6 100644 --- a/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/Extensions/SerializableErrorExtensions.cs @@ -104,7 +104,7 @@ private static ODataInnerError ToODataInnerError(this Dictionary // Convert the model state errors in to a string (for debugging only). // This should be improved once ODataError allows more details. - [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "")] + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "The default format provider is fine here.")] private static string ConvertModelStateErrors(this IReadOnlyDictionary errors) { StringBuilder builder = new StringBuilder(); diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs index 876c3dd2f..f9956f51b 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataPrimitiveSerializer.cs @@ -145,19 +145,18 @@ internal static object ConvertPrimitiveValue(object value, IEdmPrimitiveTypeRefe } #if NET6_0 - // Since ODL doesn't support "DateOnly" and "TimeOnly", we have to use Date and TimeOfDay defined in ODL as a bridge. + // Since ODL doesn't support "DateOnly", we have to use Date defined in ODL. if (primitiveType != null && primitiveType.IsDate() && TypeHelper.IsDateOnly(type)) { DateOnly dateOnly = (DateOnly)value; - Date dt = new Date(dateOnly.Year, dateOnly.Month, dateOnly.Day); - return dt; + return new Date(dateOnly.Year, dateOnly.Month, dateOnly.Day); } + // Since ODL doesn't support "TimeOnly", we have to use TimeOfDay defined in ODL. if (primitiveType != null && primitiveType.IsTimeOfDay() && TypeHelper.IsTimeOnly(type)) { TimeOnly timeOnly = (TimeOnly)value; - TimeOfDay tod = new TimeOfDay(timeOnly.Hour, timeOnly.Minute, timeOnly.Second, timeOnly.Millisecond); - return tod; + return new TimeOfDay(timeOnly.Hour, timeOnly.Minute, timeOnly.Second, timeOnly.Millisecond); } #endif diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 54a943a58..dd6357a7c 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -1089,6 +1089,20 @@ The type to test. True if the type is a DateTime; false otherwise. + + + Determine if a type is a . + + The type to test. + True if the type is a DateOnly; false otherwise. + + + + Determine if a type is a . + + The type to test. + True if the type is a TimeOnly; false otherwise. + Determine if a type is a TimeSpan. diff --git a/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs b/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs index d0864e4ad..3f44db2c7 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ClrCanonicalFunctions.cs @@ -123,21 +123,21 @@ internal class ClrCanonicalFunctions #if NET6_0 // DateOnly properties - public static readonly Dictionary DateOnlyProperties = new[] + public static readonly Dictionary DateOnlyProperties = new Dictionary { - new KeyValuePair(YearFunctionName, typeof(DateOnly).GetProperty("Year")), - new KeyValuePair(MonthFunctionName, typeof(DateOnly).GetProperty("Month")), - new KeyValuePair(DayFunctionName, typeof(DateOnly).GetProperty("Day")), - }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + { YearFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Year)) }, + { MonthFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Month)) }, + { DayFunctionName, typeof(DateOnly).GetProperty(nameof(DateOnly.Day)) } + }; - // TimeOnly - public static readonly Dictionary TimeOnlyProperties = new[] + // TimeOnly properties + public static readonly Dictionary TimeOnlyProperties = new Dictionary { - new KeyValuePair(HourFunctionName, typeof(TimeOnly).GetProperty("Hour")), - new KeyValuePair(MinuteFunctionName, typeof(TimeOnly).GetProperty("Minute")), - new KeyValuePair(SecondFunctionName, typeof(TimeOnly).GetProperty("Second")), - new KeyValuePair(MillisecondFunctionName, typeof(TimeOnly).GetProperty("Millisecond")), - }.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + { HourFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Hour)) }, + { MinuteFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Minute)) }, + { SecondFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Second)) }, + { MillisecondFunctionName, typeof(TimeOnly).GetProperty(nameof(TimeOnly.Millisecond)) } + }; #endif // TimeSpan properties diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs index a55d4ccea..7fc76d359 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderHelper.cs @@ -432,19 +432,15 @@ private static Expression CreateTimeBinaryExpression(Expression source, ODataQue { source = ConvertToDateTimeRelatedConstExpression(source); - long ticksPerHour = 36000000000L; - long ticksPerMinute = 600000000L; - long ticksPerSecond = 10000000L; - // Hour, Minute, Second, Millisecond Expression hour = GetProperty(source, ClrCanonicalFunctions.HourFunctionName, querySettings); Expression minute = GetProperty(source, ClrCanonicalFunctions.MinuteFunctionName, querySettings); Expression second = GetProperty(source, ClrCanonicalFunctions.SecondFunctionName, querySettings); Expression milliSecond = GetProperty(source, ClrCanonicalFunctions.MillisecondFunctionName, querySettings); - Expression hourTicks = Expression.Multiply(Expression.Convert(hour, typeof(long)), Expression.Constant(ticksPerHour, typeof(long))); - Expression minuteTicks = Expression.Multiply(Expression.Convert(minute, typeof(long)), Expression.Constant(ticksPerMinute, typeof(long))); - Expression secondTicks = Expression.Multiply(Expression.Convert(second, typeof(long)), Expression.Constant(ticksPerSecond, typeof(long))); + Expression hourTicks = Expression.Multiply(Expression.Convert(hour, typeof(long)), Expression.Constant(TimeSpan.TicksPerHour, typeof(long))); + Expression minuteTicks = Expression.Multiply(Expression.Convert(minute, typeof(long)), Expression.Constant(TimeSpan.TicksPerMinute, typeof(long))); + Expression secondTicks = Expression.Multiply(Expression.Convert(second, typeof(long)), Expression.Constant(TimeSpan.TicksPerSecond, typeof(long))); // return (hour * TicksPerHour + minute * TicksPerMinute + second * TicksPerSecond + millisecond) Expression result = Expression.Add(hourTicks, Expression.Add(minuteTicks, Expression.Add(secondTicks, Expression.Convert(milliSecond, typeof(long))))); @@ -482,16 +478,14 @@ private static Expression ConvertToDateTimeRelatedConstExpression(Expression sou } #if NET6_0 - var dateOnly = parameterizedConstantValue as DateOnly?; - if (dateOnly != null) + if (parameterizedConstantValue is DateOnly dateOnly) { - return Expression.Constant(dateOnly.Value, typeof(DateOnly)); + return Expression.Constant(dateOnly, typeof(DateOnly)); } - var timeOnly = parameterizedConstantValue as TimeOnly?; - if (timeOnly != null) + else if (parameterizedConstantValue is TimeOnly timeOnly) { - return Expression.Constant(timeOnly.Value, typeof(TimeOnly)); + return Expression.Constant(timeOnly, typeof(TimeOnly)); } #endif } @@ -511,21 +505,34 @@ public static bool IsDoubleOrDecimal(Type type) public static bool IsDateAndTimeRelated(Type type) { - return IsType(type) || - IsType(type) || - IsType(type) || - IsType(type) || - IsType(type); + return IsType(type) + || IsType(type) + || IsType(type) + || IsType(type) + || IsType(type) +#if NET6_0 + || IsType(type) + || IsType(type) +#endif + ; } public static bool IsDateRelated(Type type) { +#if NET6_0 + return IsType(type) || IsType(type) || IsType(type) || IsType(type); +#else return IsType(type) || IsType(type) || IsType(type); +#endif } public static bool IsTimeRelated(Type type) { +#if NET6_0 + return IsType(type) || IsType(type) || IsType(type) || IsType(type) || IsType(type); +#else return IsType(type) || IsType(type) || IsType(type) || IsType(type); +#endif } public static bool IsDateOrOffset(Type type) @@ -553,6 +560,18 @@ public static bool IsDate(Type type) return IsType(type); } +#if NET6_0 + public static bool IsDateOnly(this Type type) + { + return IsType(type); + } + + public static bool IsTimeOnly(this Type type) + { + return IsType(type); + } +#endif + public static bool IsInteger(Type type) { return IsType(type) || IsType(type) || IsType(type); diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs index 494429ea9..9c63e8e5d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs @@ -348,11 +348,7 @@ protected virtual Expression BindDateRelatedProperty(SingleValueFunctionCallNode CheckArgumentNull(node, context); Expression[] arguments = BindArguments(node.Parameters, context); - Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsDateRelated(arguments[0].Type) -#if NET6_0 - || ExpressionBinderHelper.IsType(arguments[0].Type) -#endif - )); + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateRelated(arguments[0].Type)); // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. Expression parameter = arguments[0]; @@ -364,7 +360,7 @@ protected virtual Expression BindDateRelatedProperty(SingleValueFunctionCallNode property = ClrCanonicalFunctions.DateProperties[node.Name]; } #if NET6_0 - else if (ExpressionBinderHelper.IsType(parameter.Type)) + else if (parameter.Type.IsDateOnly()) { Contract.Assert(ClrCanonicalFunctions.DateOnlyProperties.ContainsKey(node.Name)); property = ClrCanonicalFunctions.DateOnlyProperties[node.Name]; @@ -396,11 +392,7 @@ protected virtual Expression BindTimeRelatedProperty(SingleValueFunctionCallNode Expression[] arguments = BindArguments(node.Parameters, context); - Contract.Assert(arguments.Length == 1 && (ExpressionBinderHelper.IsTimeRelated(arguments[0].Type) -#if NET6_0 - || ExpressionBinderHelper.IsType(arguments[0].Type) -#endif - )); + Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsTimeRelated(arguments[0].Type)); // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. Expression parameter = arguments[0]; @@ -412,7 +404,7 @@ protected virtual Expression BindTimeRelatedProperty(SingleValueFunctionCallNode property = ClrCanonicalFunctions.TimeOfDayProperties[node.Name]; } #if NET6_0 - else if (ExpressionBinderHelper.IsType(parameter.Type)) + else if (parameter.Type.IsTimeOnly()) { Contract.Assert(ClrCanonicalFunctions.TimeOnlyProperties.ContainsKey(node.Name)); property = ClrCanonicalFunctions.TimeOnlyProperties[node.Name]; diff --git a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs index 64c5f5aa3..704383297 100644 --- a/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs +++ b/src/Microsoft.AspNetCore.OData/Routing/ODataRouteDebugMiddleware.cs @@ -164,7 +164,7 @@ internal static bool AcceptsJson(IHeaderDictionary headers) return result; } - [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "")] + [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "The default format provider is fine here.")] private static void AppendRoute(StringBuilder builder, EndpointRouteInfo routeInfo) { builder.Append("");