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 e5eb3e0
Show file tree
Hide file tree
Showing 29 changed files with 479 additions and 81 deletions.
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 @@ public class CosmosProjectionBindingExpressionVisitor : ExpressionVisitor
/// 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
Expand Up @@ -49,7 +49,7 @@ public class CosmosQueryableMethodTranslatingExpressionVisitor : QueryableMethod
sqlExpressionFactory,
memberTranslatorProvider,
methodCallTranslatorProvider);
_projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_sqlTranslator);
_projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_model, _sqlTranslator);
}

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

/// <summary>
Expand Down
Expand Up @@ -126,6 +126,11 @@ private bool TryBindMember(Expression source, MemberIdentity member, out Express
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
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
Expand Up @@ -31,7 +31,7 @@ public class InMemoryQueryableMethodTranslatingExpressionVisitor : QueryableMeth
[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
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
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
Expand Up @@ -140,9 +140,10 @@ public virtual Func<ISnapshot> CreateEmpty([NotNull] IEntityType entityType)
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
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
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
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
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 string Print([NotNull] this Expression expression, int? characterL
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
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

0 comments on commit e5eb3e0

Please sign in to comment.