diff --git a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs index 4be2d89a..2783c161 100644 --- a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.OData.Deltas; using Microsoft.AspNetCore.OData.Edm; +using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Query.Wrapper; using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; @@ -419,6 +420,13 @@ internal static bool TryGetInstance(Type type, object value, out object instance return false; } + internal static bool IsClassicEFQueryProvider(Type queryProviderType) + { + var providerNS = queryProviderType.Namespace; + return providerNS == HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF6 + || providerNS == HandleNullPropagationOptionHelper.EntityFrameworkQueryProviderNamespace; + } + private static Type GetInnerGenericType(Type interfaceType) { // Getting the type T definition if the returning type implements IEnumerable diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs index e616cb6a..9f912e60 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/ExpressionBinderBase.cs @@ -84,6 +84,11 @@ internal ExpressionBinderBase(IEdmModel model, ODataQuerySettings querySettings) QuerySettings = querySettings; Model = model; } + + /// + /// If queryable the query is being applied to is EF6 + /// + protected bool ClassicEF { get; private set; } #endregion #region Abstract properties and methods @@ -390,10 +395,18 @@ private Expression BindDate(SingleValueFunctionCallNode node) // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); - - // EF doesn't support new Date(int, int, int), also doesn't support other property access, for example DateTime.Date. - // Therefore, we just return the source (DateTime or DateTimeOffset). - return arguments[0]; + + // EF6 and earlier don't support translating the Date property, so just return the original value + if (ClassicEF) + { + return arguments[0]; + } + + Type type = Nullable.GetUnderlyingType(arguments[0].Type) ?? arguments[0].Type; + PropertyInfo property = type.GetProperty(nameof(DateTime.Date)); + + Expression propertyAccessExpr = ExpressionBinderHelper.MakePropertyAccess(property, arguments[0], QuerySettings); + return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(propertyAccessExpr, arguments, QuerySettings); } private Expression BindNow(SingleValueFunctionCallNode node) @@ -403,7 +416,7 @@ private Expression BindNow(SingleValueFunctionCallNode node) // Function Now() does not take any arguments. Expression[] arguments = BindArguments(node.Parameters); Contract.Assert(arguments.Length == 0); - + return Expression.Property(null, typeof(DateTimeOffset), "UtcNow"); } @@ -415,10 +428,18 @@ private Expression BindTime(SingleValueFunctionCallNode node) // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); + + // EF6 and earlier don't support translating the TimeOfDay property, so just return the original value + if (ClassicEF) + { + return arguments[0]; + } - // EF doesn't support new TimeOfDay(int, int, int, int), also doesn't support other property access, for example DateTimeOffset.DateTime. - // Therefore, we just return the source (DateTime or DateTimeOffset). - return arguments[0]; + Type type = Nullable.GetUnderlyingType(arguments[0].Type) ?? arguments[0].Type; + PropertyInfo property = type.GetProperty(nameof(DateTime.TimeOfDay)); + + Expression propertyAccessExpr = ExpressionBinderHelper.MakePropertyAccess(property, arguments[0], QuerySettings); + return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(propertyAccessExpr, arguments, QuerySettings); } private Expression BindFractionalSeconds(SingleValueFunctionCallNode node) @@ -981,6 +1002,15 @@ protected Expression GetFlattenedPropertyExpression(string propertyPath) throw new ODataException(Error.Format(SRResources.PropertyOrPathWasRemovedFromContext, propertyPath)); } + + /// + /// Initialize the state of the binder using the queryable to apply to + /// + /// Queryable the query is being applied to + protected void InitializeQuery(IQueryable query) + { + ClassicEF = IsClassicEF(query); + } #endregion #region Internal methods @@ -1336,5 +1366,15 @@ internal Expression BindCastToEnumType(Type sourceType, Type targetClrType, Quer } } } + + /// + /// Checks IQueryable provider for need of EF6 optimization + /// + /// + /// True if EF6 optimization are needed. + internal virtual bool IsClassicEF(IQueryable query) + { + return TypeHelper.IsClassicEFQueryProvider(query.Provider.GetType()); + } #endregion } diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs index 3af97de0..f5b93641 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.SingleValueFunctionCall.cs @@ -699,9 +699,17 @@ protected virtual Expression BindDate(SingleValueFunctionCallNode node, QueryBin // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); - // EF doesn't support new Date(int, int, int), also doesn't support other property access, for example DateTime.Date. - // Therefore, we just return the source (DateTime or DateTimeOffset). - return arguments[0]; + // EF6 and earlier don't support translating the Date property, so just return the original value + if (context.IsClassicEF) + { + return arguments[0]; + } + + Type type = Nullable.GetUnderlyingType(arguments[0].Type) ?? arguments[0].Type; + PropertyInfo property = type.GetProperty(nameof(DateTime.Date)); + + Expression propertyAccessExpr = ExpressionBinderHelper.MakePropertyAccess(property, arguments[0], context.QuerySettings); + return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(propertyAccessExpr, arguments, context.QuerySettings); } /// @@ -718,10 +726,18 @@ protected virtual Expression BindTime(SingleValueFunctionCallNode node, QueryBin // We should support DateTime & DateTimeOffset even though DateTime is not part of OData v4 Spec. Contract.Assert(arguments.Length == 1 && ExpressionBinderHelper.IsDateOrOffset(arguments[0].Type)); + + // EF6 and earlier don't support translating the TimeOfDay property, so just return the original value + if (context.IsClassicEF) + { + return arguments[0]; + } + + Type type = Nullable.GetUnderlyingType(arguments[0].Type) ?? arguments[0].Type; + PropertyInfo property = type.GetProperty(nameof(DateTime.TimeOfDay)); - // EF doesn't support new TimeOfDay(int, int, int, int), also doesn't support other property access, for example DateTimeOffset.DateTime. - // Therefore, we just return the source (DateTime or DateTimeOffset). - return arguments[0]; + Expression propertyAccessExpr = ExpressionBinderHelper.MakePropertyAccess(property, arguments[0], context.QuerySettings); + return ExpressionBinderHelper.CreateFunctionCallWithNullPropagation(propertyAccessExpr, arguments, context.QuerySettings); } /// diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinderContext.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinderContext.cs index ea03fe6e..5326193d 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinderContext.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinderContext.cs @@ -10,6 +10,7 @@ using System.Diagnostics.Contracts; using System.Linq; using System.Linq.Expressions; +using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Query.Wrapper; using Microsoft.OData; @@ -84,6 +85,20 @@ public QueryBinderContext(IEdmModel model, ODataQuerySettings querySettings, Typ // Categories?$expand=Products($filter=OrderItems/any(oi:oi/UnitPrice ne UnitPrice) } + /// + /// Initializes a new instance of the class. + /// + /// The Edm model. + /// The query setting. + /// The current element CLR type in this context (scope). + /// The query provider for the queryable being applied to. + public QueryBinderContext(IEdmModel model, ODataQuerySettings querySettings, Type clrType, + IQueryProvider queryProvider) + : this(model, querySettings, clrType) + { + IsClassicEF = TypeHelper.IsClassicEFQueryProvider(queryProvider.GetType()); + } + /// /// Initializes a new instance of the class. /// @@ -186,6 +201,11 @@ public QueryBinderContext(QueryBinderContext context, ODataQuerySettings querySe /// Basically for $compute in $select and $expand /// public Expression Source { get; set; } + + /// + /// If the queryable provider is EF6 + /// + public bool IsClassicEF { get; init; } /// /// Gets the parameter using parameter name. diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/TransformationBinderBase.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/TransformationBinderBase.cs index eeb07a68..9ac970da 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/TransformationBinderBase.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/TransformationBinderBase.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using Microsoft.AspNetCore.OData.Common; using Microsoft.AspNetCore.OData.Query.Wrapper; using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; @@ -29,9 +30,7 @@ internal TransformationBinderBase(ODataQuerySettings settings, IAssemblyResolver protected Type ElementType { get { return this.LambdaParameter.Type; } } protected ParameterExpression LambdaParameter { get; set; } - - protected bool ClassicEF { get; private set; } - + /// /// Gets CLR type returned from the query. /// @@ -39,24 +38,12 @@ public Type ResultClrType { get; protected set; } - - /// - /// Checks IQueryable provider for need of EF6 optimization - /// - /// - /// True if EF6 optimization are needed. - internal virtual bool IsClassicEF(IQueryable query) - { - var providerNS = query.Provider.GetType().Namespace; - return (providerNS == HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF6 - || providerNS == HandleNullPropagationOptionHelper.EntityFrameworkQueryProviderNamespace); - } - + protected void PreprocessQuery(IQueryable query) { Contract.Assert(query != null); - this.ClassicEF = IsClassicEF(query); + InitializeQuery(query); this.BaseQuery = query; EnsureFlattenedPropertyContainer(this.LambdaParameter); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/ApplyQueryOptions.cs b/src/Microsoft.AspNetCore.OData/Query/Query/ApplyQueryOptions.cs index 39a08449..c887779c 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/ApplyQueryOptions.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/ApplyQueryOptions.cs @@ -165,7 +165,7 @@ public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) var filterTransformation = transformation as FilterTransformationNode; IFilterBinder binder = Context.GetFilterBinder(); - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, ResultClrType); + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, ResultClrType, query.Provider); query = binder.ApplyBind(query, filterTransformation.FilterClause, binderContext); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/FilterQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/FilterQueryOption.cs index 21599e3e..f265b8d0 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/FilterQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/FilterQueryOption.cs @@ -161,7 +161,7 @@ public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) FilterClause filterClause = FilterClause; Contract.Assert(filterClause != null); - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType, query.Provider); if (Compute != null) { diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/OrderByQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/OrderByQueryOption.cs index 94fb5675..1081fba8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/OrderByQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/OrderByQueryOption.cs @@ -262,7 +262,7 @@ private IOrderedQueryable ApplyToCore(IQueryable query, ODataQuerySettings query bool orderByItSeen = false; IOrderByBinder binder = Context.GetOrderByBinder(); - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType, query.Provider); if (Compute != null) { binderContext.AddComputedProperties(Compute.ComputeClause.ComputedItems); diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/SearchQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/SearchQueryOption.cs index 53e1f5f0..711d028e 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/SearchQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/SearchQueryOption.cs @@ -148,7 +148,7 @@ public IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySettings) throw Error.NotSupported(SRResources.ApplyToOnUntypedQueryOption, "ApplyTo"); } - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType); + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, querySettings, Context.ElementClrType, query.Provider); return binder.ApplyBind(query, SearchClause, binderContext); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Query/SelectExpandQueryOption.cs b/src/Microsoft.AspNetCore.OData/Query/Query/SelectExpandQueryOption.cs index 551b5c09..f9ac2ec8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Query/SelectExpandQueryOption.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Query/SelectExpandQueryOption.cs @@ -211,7 +211,7 @@ public IQueryable ApplyTo(IQueryable queryable, ODataQuerySettings settings) ISelectExpandBinder binder = Context.GetSelectExpandBinder(); - QueryBinderContext binderContext = new QueryBinderContext(Context.Model, settings, Context.ElementClrType) + QueryBinderContext binderContext = new QueryBinderContext(Context.Model, settings, Context.ElementClrType, queryable.Provider) { NavigationSource = Context.NavigationSource, }; diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayController.cs index 2d6401a6..59e7d2e4 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayController.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayController.cs @@ -50,6 +50,11 @@ private static void InitCustomers() NullableOffsets = new [] { dto.AddMonths(e), (DateTimeOffset?)null, dto.AddDays(e) }, NullableDates = new [] { (Date)dto.AddYears(e).Date, (Date?)null, (Date)dto.AddMonths(e).Date }, NullableTimeOfDays = new [] { (TimeOfDay)dto.AddHours(e).TimeOfDay, (TimeOfDay?)null, (TimeOfDay)dto.AddMinutes(e).TimeOfDay }, + + SameDayDateTime = dto.AddHours(e).DateTime, + SameDayNullableDateTime = e % 2 == 0 ? null : dto.AddHours(e).DateTime, + SameDayOffset = dto.AddHours(6 - e), + SameDayNullableOffset = e % 3 == 0 ? null : dto.AddHours(e), }).ToList(); } diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayEdmModel.cs index 13b65ff2..60ce935b 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayEdmModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayEdmModel.cs @@ -41,6 +41,11 @@ public static IEdmModel GetExplicitModel() customerType.CollectionProperty(c => c.NullableDates); customerType.CollectionProperty(c => c.NullableTimeOfDays); + customerType.Property(c => c.SameDayDateTime); + customerType.Property(c => c.SameDayNullableDateTime); + customerType.Property(c => c.SameDayOffset); + customerType.Property(c => c.SameDayNullableOffset); + var customers = builder.EntitySet("DCustomers"); // customers.HasIdLink(link, true); // customers.HasEditLink(link, true); diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayModel.cs index 503a0dd0..66614656 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayModel.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayModel.cs @@ -39,6 +39,11 @@ public class DCustomer public IList NullableOffsets { get; set; } public IList NullableDates { get; set; } public IList NullableTimeOfDays { get; set; } + + public DateTime SameDayDateTime { get; set; } + public DateTime? SameDayNullableDateTime { get; set; } + public DateTimeOffset SameDayOffset { get; set; } + public DateTimeOffset? SameDayNullableOffset { get; set; } } public class EfCustomer diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayTest.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayTest.cs index 50b408f5..be040512 100644 --- a/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayTest.cs +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/DateAndTimeOfDay/DateAndTimeOfDayTest.cs @@ -273,6 +273,18 @@ public static TheoryDataSet OrderByData new[] {"$orderby=NullableTimeOfDay", "3 > 1 > 2 > 4 > 5"}, new[] {"$orderby=NullableTimeOfDay desc", "5 > 4 > 2 > 1 > 3"}, + + new[] {"$orderby=date(SameDayDateTime), Id desc", "5 > 4 > 3 > 2 > 1"}, + new[] {"$orderby=date(SameDayNullableDateTime), Id desc", "4 > 2 > 5 > 3 > 1"}, + + new[] {"$orderby=date(SameDayDateTimeOffset), Id desc", "5 > 4 > 3 > 2 > 1"}, + new[] {"$orderby=date(SameDayNullableDateTimeOffset), Id desc", "3 > 5 > 4 > 2 > 1"}, + + new[] {"$orderby=time(SameDayDateTime)", "1 > 2 > 3 > 4 > 5"}, + new[] {"$orderby=time(SameDayNullableDateTime)", "2 > 4 > 1 > 3 > 5"}, + + new[] {"$orderby=time(SameDayDateTimeOffset)", "5 > 4 > 3 > 2 > 1"}, + new[] {"$orderby=time(SameDayNullableDateTimeOffset)", "3 > 1 > 2 > 4 > 5"}, }; TheoryDataSet data = new TheoryDataSet(); foreach (string mode in modes) @@ -559,6 +571,10 @@ public static TheoryDataSet OrderByDataEf new[] {"$orderby=NullableOffset", "3 > 1 > 2 > 4 > 5"}, new[] {"$orderby=NullableOffset desc", "5 > 4 > 2 > 1 > 3"}, + + new[] {"$orderby=date(NullableDateTime), Id desc", "4 > 2 > 3 > 1 > 5"}, + + new[] {"$orderby=time(DateTime)", "1 > 2 > 3 > 4 > 5"}, }; TheoryDataSet data = new TheoryDataSet(); foreach (string[] order in orders)