Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ internal ExpressionBinderBase(IEdmModel model, ODataQuerySettings querySettings)
QuerySettings = querySettings;
Model = model;
}

/// <summary>
/// If queryable the query is being applied to is EF6
/// </summary>
protected bool ClassicEF { get; private set; }
#endregion

#region Abstract properties and methods
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

The new QueryBinder and QueryBinderContext were designed to remove the tight coupling with IQueryable that was present in ExpressionBinderBase and the older Binders.
Can you add a Custom FilterBinder and override this method with your custom implementation? That's a cleaner solution.
Reference https://devblogs.microsoft.com/odata/customizing-filter-for-spatial-data-in-asp-net-core-odata-8/#customizing-filterbinder

Copy link
Author

Choose a reason for hiding this comment

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

That would be the ideal solution, however that doesn't work for all operators. Aggregation and compute operations still use ExpressionBinderBase so can't be customised (unless that has changed since this PR was opened?)

{
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);
Copy link
Member

@WanjohiSammy WanjohiSammy Feb 6, 2025

Choose a reason for hiding this comment

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

Thanks for this fix.

Just to confirm..
Will this change break for users still using older versions of Entity Framework other than EF6? Why not add a condition to ensure it supports older versions of EF.

Copy link
Author

Choose a reason for hiding this comment

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

@WanjohiSammy Added handling for EF6 (just falls back to the previous behaviour of returning the source).

This required moving the IsClassicEF method and ClassicEF properties from TransformationBinderBase up to ExpressionBinderBase.

}

private Expression BindNow(SingleValueFunctionCallNode node)
Expand All @@ -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");
}

Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

Same here:
Older versions might not support new features from EF6. Adding a condition ensures compatibility.

Copy link
Author

Choose a reason for hiding this comment

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

}

private Expression BindFractionalSeconds(SingleValueFunctionCallNode node)
Expand Down Expand Up @@ -981,6 +1002,15 @@ protected Expression GetFlattenedPropertyExpression(string propertyPath)

throw new ODataException(Error.Format(SRResources.PropertyOrPathWasRemovedFromContext, propertyPath));
}

/// <summary>
/// Initialize the state of the binder using the queryable to apply to
/// </summary>
/// <param name="query">Queryable the query is being applied to</param>
protected void InitializeQuery(IQueryable query)
{
ClassicEF = IsClassicEF(query);
}
#endregion

#region Internal methods
Expand Down Expand Up @@ -1336,5 +1366,15 @@ internal Expression BindCastToEnumType(Type sourceType, Type targetClrType, Quer
}
}
}

/// <summary>
/// Checks IQueryable provider for need of EF6 optimization
/// </summary>
/// <param name="query"></param>
/// <returns>True if EF6 optimization are needed.</returns>
internal virtual bool IsClassicEF(IQueryable query)
{
return TypeHelper.IsClassicEFQueryProvider(query.Provider.GetType());
}
#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Author

Choose a reason for hiding this comment

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

Is it possible to tell if the query provider is EF6 from here?

The only place I can find that checks if it is EF6 is in TransformationBinderBase, and it uses the type of IQueryable.Provider to do so.

That would probably work for the translation in ExpressionBinderBase since TransformationBinderBase derives from it, but QueryBinder isn't exposed to the queryable at any point?

internal virtual bool IsClassicEF(IQueryable query)
{
var providerNS = query.Provider.GetType().Namespace;
return (providerNS == HandleNullPropagationOptionHelper.ObjectContextQueryProviderNamespaceEF6
|| providerNS == HandleNullPropagationOptionHelper.EntityFrameworkQueryProviderNamespace);
}

Copy link
Author

Choose a reason for hiding this comment

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

@WanjohiSammy Added handling for EF6 here too, same as Resolved as in https://github.com/OData/AspNetCoreOData/pull/1407/files#r1944232224.

As previously mentioned QueryBinder wasn't exposed to the queryable at any point, to get around this an IsClassicEF property has been added to QueryBinderContext and a new constructor for it accepting IQueryProvider added.

This has been added as an additional constructor to preserve the public API, the new one could be changed to be internal if wanted but leaving it as public probably makes the most sense. The new constructor should now be in use everywhere that requires it.

}

/// <summary>
Expand All @@ -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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -84,6 +85,20 @@ public QueryBinderContext(IEdmModel model, ODataQuerySettings querySettings, Typ
// Categories?$expand=Products($filter=OrderItems/any(oi:oi/UnitPrice ne UnitPrice)
}

/// <summary>
/// Initializes a new instance of the <see cref="QueryBinderContext" /> class.
/// </summary>
/// <param name="model">The Edm model.</param>
/// <param name="querySettings">The query setting.</param>
/// <param name="clrType">The current element CLR type in this context (scope).</param>
/// <param name="queryProvider">The query provider for the queryable being applied to.</param>
public QueryBinderContext(IEdmModel model, ODataQuerySettings querySettings, Type clrType,
IQueryProvider queryProvider)
: this(model, querySettings, clrType)
{
IsClassicEF = TypeHelper.IsClassicEFQueryProvider(queryProvider.GetType());
}

/// <summary>
/// Initializes a new instance of the <see cref="QueryBinderContext" /> class.
/// </summary>
Expand Down Expand Up @@ -186,6 +201,11 @@ public QueryBinderContext(QueryBinderContext context, ODataQuerySettings querySe
/// Basically for $compute in $select and $expand
/// </summary>
public Expression Source { get; set; }

/// <summary>
/// If the queryable provider is EF6
/// </summary>
public bool IsClassicEF { get; init; }

/// <summary>
/// Gets the parameter using parameter name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,34 +30,20 @@ internal TransformationBinderBase(ODataQuerySettings settings, IAssemblyResolver
protected Type ElementType { get { return this.LambdaParameter.Type; } }

protected ParameterExpression LambdaParameter { get; set; }

protected bool ClassicEF { get; private set; }


/// <summary>
/// Gets CLR type returned from the query.
/// </summary>
public Type ResultClrType
{
get; protected set;
}

/// <summary>
/// Checks IQueryable provider for need of EF6 optimization
/// </summary>
/// <param name="query"></param>
/// <returns>True if EF6 optimization are needed.</returns>
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DCustomer>("DCustomers");
// customers.HasIdLink(link, true);
// customers.HasEditLink(link, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ public class DCustomer
public IList<DateTimeOffset?> NullableOffsets { get; set; }
public IList<Date?> NullableDates { get; set; }
public IList<TimeOfDay?> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,18 @@ public static TheoryDataSet<string, string, string> 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"},
Copy link
Member

Choose a reason for hiding this comment

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

Are you using EF here?

Copy link
Author

Choose a reason for hiding this comment

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

These tests are for the in-memory queryable provider, there are a couple of tests for EF too below


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<string, string, string> data = new TheoryDataSet<string, string, string>();
foreach (string mode in modes)
Expand Down Expand Up @@ -559,6 +571,10 @@ public static TheoryDataSet<string, string> 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<string, string> data = new TheoryDataSet<string, string>();
foreach (string[] order in orders)
Expand Down