Skip to content

Commit

Permalink
Query: Add indexed property support
Browse files Browse the repository at this point in the history
Part of #13610
Resolves #15799
  • Loading branch information
smitpatel committed Jan 4, 2020
1 parent 51ca166 commit 769a462
Show file tree
Hide file tree
Showing 29 changed files with 479 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal
public class CosmosProjectionBindingExpressionVisitor : ExpressionVisitor
{
private readonly CosmosSqlTranslatingExpressionVisitor _sqlTranslator;
private readonly IModel _model;
private SelectExpression _selectExpression;
private bool _clientEval;

Expand All @@ -46,8 +47,10 @@ private readonly Stack<INavigation> _includedNavigations
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public CosmosProjectionBindingExpressionVisitor([NotNull] CosmosSqlTranslatingExpressionVisitor sqlTranslator)
public CosmosProjectionBindingExpressionVisitor(
[NotNull] IModel model, [NotNull] CosmosSqlTranslatingExpressionVisitor sqlTranslator)
{
_model = model;
_sqlTranslator = sqlTranslator;
}

Expand Down Expand Up @@ -260,7 +263,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
{
Check.NotNull(methodCallExpression, nameof(methodCallExpression));

if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var memberName))
if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var memberName)
|| methodCallExpression.TryGetIndexerArguments(_model, out source, out memberName))
{
if (!_clientEval)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public CosmosQueryableMethodTranslatingExpressionVisitor(
sqlExpressionFactory,
memberTranslatorProvider,
methodCallTranslatorProvider);
_projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_sqlTranslator);
_projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_model, _sqlTranslator);
}

/// <summary>
Expand All @@ -65,7 +65,7 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor(
_model = parentVisitor._model;
_sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
_sqlTranslator = parentVisitor._sqlTranslator;
_projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_sqlTranslator);
_projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_model, _sqlTranslator);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ when methodCallExpression.TryGetEFPropertyArguments(out var innerSource, out var
TryBindMember(innerSource, MemberIdentity.Create(innerPropertyName), out visitedExpression);
break;

case MethodCallExpression methodCallExpression
when methodCallExpression.TryGetIndexerArguments(_model, out var innerSource, out var innerPropertyName):
TryBindMember(innerSource, MemberIdentity.Create(innerPropertyName), out visitedExpression);
break;

default:
visitedExpression = null;
break;
Expand Down Expand Up @@ -162,6 +167,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
: null;
}

// EF Indexer property
if (methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName))
{
return TryBindMember(source, MemberIdentity.Create(propertyName), out var result) ? result : null;
}

if (TranslationFailed(methodCallExpression.Object, Visit(methodCallExpression.Object), out var sqlObject))
{
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ public class InMemoryExpressionTranslatingExpressionVisitor : ExpressionVisitor

private readonly QueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor;
private readonly EntityProjectionFindingExpressionVisitor _entityProjectionFindingExpressionVisitor;
private readonly IModel _model;

public InMemoryExpressionTranslatingExpressionVisitor(
[NotNull] QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
[NotNull] QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor,
[NotNull] IModel model)
{
_queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor;
_entityProjectionFindingExpressionVisitor = new EntityProjectionFindingExpressionVisitor();
_model = model;
}

private sealed class EntityProjectionFindingExpressionVisitor : ExpressionVisitor
Expand Down Expand Up @@ -63,8 +66,14 @@ public override Expression Visit(Expression expression)

private sealed class PropertyFindingExpressionVisitor : ExpressionVisitor
{
private readonly IModel _model;
private IProperty _property;

public PropertyFindingExpressionVisitor(IModel model)
{
_model = model;
}

public IProperty Find(Expression expression)
{
Visit(expression);
Expand All @@ -85,9 +94,10 @@ protected override Expression VisitMember(MemberExpression memberExpression)

protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
if (methodCallExpression.TryGetEFPropertyArguments(out var _, out var propertyName))
if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var propertyName)
|| methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName))
{
var entityType = FindEntityType(methodCallExpression.Object);
var entityType = FindEntityType(source);
if (entityType != null)
{
_property = GetProperty(entityType, MemberIdentity.Create(propertyName));
Expand Down Expand Up @@ -151,7 +161,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
newRight = ConvertToNullable(newRight);
}

var propertyFindingExpressionVisitor = new PropertyFindingExpressionVisitor();
var propertyFindingExpressionVisitor = new PropertyFindingExpressionVisitor(_model);
var property = propertyFindingExpressionVisitor.Find(binaryExpression.Left)
?? propertyFindingExpressionVisitor.Find(binaryExpression.Right);

Expand Down Expand Up @@ -377,6 +387,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
throw new InvalidOperationException("EF.Property called with wrong property name.");
}

// EF Indexer property
if (methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName))
{
return TryBindMember(source, MemberIdentity.Create(propertyName), methodCallExpression.Type, out var result) ? result : null;
}

// GroupBy Aggregate case
if (methodCallExpression.Object == null
&& methodCallExpression.Method.DeclaringType == typeof(Enumerable)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public InMemoryQueryableMethodTranslatingExpressionVisitor(
[NotNull] IModel model)
: base(dependencies, subquery: false)
{
_expressionTranslator = new InMemoryExpressionTranslatingExpressionVisitor(this);
_expressionTranslator = new InMemoryExpressionTranslatingExpressionVisitor(this, model);
_weakEntityExpandingExpressionVisitor = new WeakEntityExpandingExpressionVisitor(_expressionTranslator);
_projectionBindingExpressionVisitor = new InMemoryProjectionBindingExpressionVisitor(this, _expressionTranslator);
_model = model;
Expand Down Expand Up @@ -994,6 +994,14 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
?? methodCallExpression.Update(null, new[] { source, methodCallExpression.Arguments[1] });
}

if (methodCallExpression.TryGetEFPropertyArguments(out source, out navigationName))
{
source = Visit(source);

return TryExpand(source, MemberIdentity.Create(navigationName))
?? methodCallExpression.Update(source, new[] { methodCallExpression.Arguments[0] });
}

return base.VisitMethodCall(methodCallExpression);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,14 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
?? methodCallExpression.Update(null, new[] { source, methodCallExpression.Arguments[1] });
}

if (methodCallExpression.TryGetEFPropertyArguments(out source, out navigationName))
{
source = Visit(source);

return TryExpand(source, MemberIdentity.Create(navigationName))
?? methodCallExpression.Update(source, new[] { methodCallExpression.Arguments[1] });
}

return base.VisitMethodCall(methodCallExpression);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
throw new InvalidOperationException("EF.Property called with wrong property name.");
}

// EF Indexer property
if (methodCallExpression.TryGetIndexerArguments(_model, out source, out propertyName))
{
return TryBindMember(source, MemberIdentity.Create(propertyName), out var result) ? result : null;
}

// GroupBy Aggregate case
if (methodCallExpression.Object == null
&& methodCallExpression.Method.DeclaringType == typeof(Enumerable)
Expand Down
7 changes: 4 additions & 3 deletions src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,10 @@ protected virtual Expression CreateSnapshotExpression(
continue;
}

var memberAccess = (Expression)Expression.MakeMemberAccess(
entityVariable,
propertyBase.GetMemberInfo(forMaterialization: false, forSet: false));
var memberInfo = propertyBase.GetMemberInfo(forMaterialization: false, forSet: false);
var memberAccess = propertyBase.IsIndexerProperty()
? Expression.MakeIndex(entityVariable, (PropertyInfo)memberInfo, new[] { Expression.Constant(propertyBase.Name) })
: (Expression)Expression.MakeMemberAccess(entityVariable, memberInfo);

if (memberAccess.Type != propertyBase.ClrType)
{
Expand Down
45 changes: 0 additions & 45 deletions src/EFCore/Extensions/Internal/EFPropertyExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,51 +24,6 @@ namespace Microsoft.EntityFrameworkCore.Internal
// ReSharper disable once InconsistentNaming
public static class EFPropertyExtensions
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static bool TryGetEFIndexerArguments(
[NotNull] this MethodCallExpression methodCallExpression,
[CanBeNull] out Expression entityExpression,
[CanBeNull] out string propertyName)
{
if (IsEFIndexer(methodCallExpression)
&& methodCallExpression.Arguments[0] is ConstantExpression propertyNameExpression)
{
entityExpression = methodCallExpression.Object;
propertyName = (string)propertyNameExpression.Value;
return true;
}

(entityExpression, propertyName) = (null, null);
return false;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static bool IsEFIndexer([NotNull] this MethodCallExpression methodCallExpression)
=> IsEFIndexer(methodCallExpression.Method);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static bool IsEFIndexer([NotNull] this MethodInfo methodInfo)
=> !methodInfo.IsStatic
&& "get_Item".Equals(methodInfo.Name, StringComparison.Ordinal)
&& typeof(object) == methodInfo.ReturnType
&& methodInfo.GetParameters()?.Count() == 1
&& typeof(string) == methodInfo.GetParameters().First().ParameterType;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
15 changes: 15 additions & 0 deletions src/EFCore/Extensions/Internal/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,5 +213,20 @@ public static bool IsQueryableType([NotNull] this Type type)

return type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryable<>));
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static PropertyInfo FindIndexerProperty([NotNull] this Type type)
{
var defaultPropertyAttribute = type.GetCustomAttributes<DefaultMemberAttribute>().FirstOrDefault();

return defaultPropertyAttribute == null
? null
: type.GetRuntimeProperties().FirstOrDefault(pi => pi.Name == defaultPropertyAttribute.MemberName && pi.IsIndexerProperty());
}
}
}
12 changes: 12 additions & 0 deletions src/EFCore/Extensions/ModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
Expand Down Expand Up @@ -150,5 +151,16 @@ public static PropertyAccessMode GetPropertyAccessMode([NotNull] this IModel mod
/// <param name="model"> The model to get the version for. </param>
public static string GetProductVersion([NotNull] this IModel model)
=> model[CoreAnnotationNames.ProductVersion] as string;

/// <summary>
/// Gets a value indicating whether the given MethodInfo reprensent an indexer access.
/// </summary>
/// <param name="model"> The model to use. </param>
/// <param name="methodInfo"> The MethodInfo to check for. </param>
public static bool IsIndexerMethod([NotNull] this IModel model, [NotNull] MethodInfo methodInfo)
=> !methodInfo.IsStatic
&& methodInfo.IsSpecialName
&& model.AsModel().FindIndexerPropertyInfo(methodInfo.DeclaringType) is PropertyInfo indexerProperty
&& (methodInfo == indexerProperty.GetMethod || methodInfo == indexerProperty.SetMethod);
}
}
28 changes: 28 additions & 0 deletions src/EFCore/Infrastructure/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Utilities;

Expand Down Expand Up @@ -107,6 +108,33 @@ public static bool TryGetEFPropertyArguments(
return false;
}

/// <summary>
/// If the given a method-call expression represents a call to indexer on the entity, then this
/// method extracts the entity expression and property name.
/// </summary>
/// <param name="methodCallExpression"> The method-call expression for indexer. </param>
/// <param name="model"> The model to use. </param>
/// <param name="entityExpression"> The extracted entity access expression. </param>
/// <param name="propertyName"> The accessed property name. </param>
/// <returns> True if the method-call was for indexer; false otherwise. </returns>
public static bool TryGetIndexerArguments(
[NotNull] this MethodCallExpression methodCallExpression,
[NotNull] IModel model,
out Expression entityExpression,
out string propertyName)
{
if (model.IsIndexerMethod(methodCallExpression.Method)
&& methodCallExpression.Arguments[0] is ConstantExpression propertyNameExpression)
{
entityExpression = methodCallExpression.Object;
propertyName = (string)propertyNameExpression.Value;
return true;
}

(entityExpression, propertyName) = (null, null);
return false;
}

/// <summary>
/// <para>
/// Gets the <see cref="PropertyInfo" /> represented by a simple property-access expression.
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Expression CreateMemberAccess(Expression parameter)
{
return propertyBase?.IsIndexerProperty() == true
? Expression.MakeIndex(
entityParameter, (PropertyInfo)memberInfo, new List<Expression>() { Expression.Constant(propertyBase.Name) })
parameter, (PropertyInfo)memberInfo, new List<Expression>() { Expression.Constant(propertyBase.Name) })
: (Expression)Expression.MakeMemberAccess(parameter, memberInfo);
}
}
Expand Down
Loading

0 comments on commit 769a462

Please sign in to comment.