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 1, 2020
1 parent a8072e9 commit 8793087
Show file tree
Hide file tree
Showing 18 changed files with 294 additions and 70 deletions.
Expand Up @@ -377,6 +377,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
throw new InvalidOperationException("EF.Property called with wrong property name.");
}

// EF Indexer property
if (methodCallExpression.TryGetIndexerArguments(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 @@ -336,6 +336,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
throw new InvalidOperationException("EF.Property called with wrong property name.");
}

// EF Indexer property
if (methodCallExpression.TryGetIndexerArguments(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
25 changes: 25 additions & 0 deletions src/EFCore/Infrastructure/ExpressionExtensions.cs
Expand Up @@ -107,6 +107,31 @@ 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="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,
out Expression entityExpression,
out string propertyName)
{
if (methodCallExpression.Method.IsIndexerMethod()
&& 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
14 changes: 14 additions & 0 deletions src/EFCore/Infrastructure/MethodInfoExtensions.cs
@@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;

Expand Down Expand Up @@ -31,5 +33,17 @@ public static bool IsEFPropertyMethod([CanBeNull] this MethodInfo methodInfo)
|| methodInfo?.IsGenericMethod == true
&& methodInfo.Name == nameof(EF.Property)
&& methodInfo.DeclaringType?.FullName == _efTypeName;

/// <summary>
/// Returns true if the given method represents a indexer property access.
/// </summary>
/// <param name="methodInfo"> The method. </param>
/// <returns> True if the method is <see cref="EF.Property{TProperty}" />; false otherwise. </returns>
public static bool IsIndexerMethod([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;
}
}
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
11 changes: 10 additions & 1 deletion src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
Expand Down Expand Up @@ -67,7 +68,7 @@ private static PropertyAccessors CreateGeneric<TProperty>(IPropertyBase property
Expression.Property(entryParameter, "Entity"),
entityClrType);

currentValueExpression = Expression.MakeMemberAccess(
currentValueExpression = CreateMemberAccess(
convertedExpression,
propertyBase.GetMemberInfo(forMaterialization: false, forSet: false));

Expand Down Expand Up @@ -108,6 +109,14 @@ private static PropertyAccessors CreateGeneric<TProperty>(IPropertyBase property
currentValueExpression,
entryParameter)
.Compile();

Expression CreateMemberAccess(Expression parameter, MemberInfo memberInfo)
{
return propertyBase?.IsIndexerProperty() == true
? Expression.MakeIndex(
parameter, (PropertyInfo)memberInfo, new List<Expression>() { Expression.Constant(propertyBase.Name) })
: (Expression)Expression.MakeMemberAccess(parameter, memberInfo);
}
}

private static Func<InternalEntityEntry, TProperty> CreateOriginalValueGetter<TProperty>(IProperty property)
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/Metadata/Internal/TypeBase.cs
Expand Up @@ -185,7 +185,7 @@ public virtual PropertyInfo FindIndexerPropertyInfo()

if (!_indexerPropertyInitialized)
{
var indexerPropertyInfo = GetRuntimeProperties().Values.FirstOrDefault(pi => pi.IsIndexerProperty());
var indexerPropertyInfo = GetRuntimeProperties().Values.FirstOrDefault(PropertyInfoExtensions.IsIndexerProperty);

Interlocked.CompareExchange(ref _indexerPropertyInfo, indexerPropertyInfo, null);
_indexerPropertyInitialized = true;
Expand Down
19 changes: 12 additions & 7 deletions src/EFCore/Query/EntityMaterializerSource.cs
Expand Up @@ -133,17 +133,22 @@ var readValueExpression
property.GetIndex(),
property);

blockExpressions.Add(
Expression.MakeMemberAccess(
instanceVariable,
memberInfo).Assign(
readValueExpression));
blockExpressions.Add(CreateMemberAssignment(instanceVariable, memberInfo, property, readValueExpression));
}

blockExpressions.Add(instanceVariable);

return Expression.Block(
new[] { instanceVariable }, blockExpressions);
return Expression.Block(new[] { instanceVariable }, blockExpressions);

static Expression CreateMemberAssignment(Expression parameter, MemberInfo memberInfo, IPropertyBase property, Expression value)
{
return property.IsIndexerProperty()
? Expression.Assign(
Expression.MakeIndex(
parameter, (PropertyInfo)memberInfo, new List<Expression>() { Expression.Constant(property.Name) }),
value)
: Expression.MakeMemberAccess(parameter, memberInfo).Assign(value);
}
}

private ConcurrentDictionary<IEntityType, Func<MaterializationContext, object>> Materializers
Expand Down
Expand Up @@ -277,9 +277,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
?? methodCallExpression.Update(null, new[] { Unwrap(newLeft), Unwrap(newRight) });
}

// Navigation via EF.Property() or via an indexer property
if (methodCallExpression.TryGetEFPropertyArguments(out _, out var propertyName)
|| methodCallExpression.TryGetEFIndexerArguments(out _, out propertyName))
// Navigation via EF.Property()
if (methodCallExpression.TryGetEFPropertyArguments(out _, out var propertyName))
{
newSource = Visit(arguments[0]);
var newMethodCall = methodCallExpression.Update(null, new[] { Unwrap(newSource), arguments[1] });
Expand All @@ -288,6 +287,16 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
: newMethodCall;
}

// Navigation via an indexer property
if (methodCallExpression.TryGetIndexerArguments(out _, out propertyName))
{
newSource = Visit(methodCallExpression.Object);
var newMethodCall = methodCallExpression.Update(Unwrap(newSource), new[] { arguments[0] });
return newSource is EntityReferenceExpression entityWrapper
? entityWrapper.TraverseProperty(propertyName, newMethodCall)
: newMethodCall;
}

switch (method.Name)
{
// These are methods that require special handling
Expand Down Expand Up @@ -1071,7 +1080,8 @@ protected struct EntityOrDtoType
public static EntityOrDtoType FromEntityReferenceExpression(EntityReferenceExpression ere)
=> new EntityOrDtoType
{
EntityType = ere.IsEntityType ? ere.EntityType : null, DtoType = ere.IsDtoType ? ere.DtoType : null
EntityType = ere.IsEntityType ? ere.EntityType : null,
DtoType = ere.IsDtoType ? ere.DtoType : null
};

public static EntityOrDtoType FromDtoType(Dictionary<string, EntityOrDtoType> dtoType)
Expand Down
Expand Up @@ -9,7 +9,7 @@
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
Expand Down Expand Up @@ -592,7 +592,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
if (_evaluatableExpressions.ContainsKey(methodCallExpression.Arguments[i]))
{
if (parameterInfos[i].GetCustomAttribute<NotParameterizedAttribute>() != null
|| methodCallExpression.Method.IsEFIndexer())
|| methodCallExpression.Method.IsIndexerMethod())
{
_evaluatableExpressions[methodCallExpression.Arguments[i]] = false;
}
Expand Down
Expand Up @@ -44,7 +44,7 @@ protected GearsOfWarQueryFixtureBase()
{
Assert.Equal(e.Name, a.Name);
Assert.Equal(e.Location, a.Location);
Assert.Equal(e.Nation, a.Nation);
Assert.Equal(e["Nation"], a["Nation"]);
}
}
},
Expand Down Expand Up @@ -243,7 +243,12 @@ protected GearsOfWarQueryFixtureBase()

protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
modelBuilder.Entity<City>().HasKey(c => c.Name);
modelBuilder.Entity<City>(
b =>
{
b.HasKey(c => c.Name);
b.Metadata.AddIndexedProperty("Nation", typeof(string));
});

modelBuilder.Entity<Gear>(
b =>
Expand Down
71 changes: 70 additions & 1 deletion test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
Expand Up @@ -7397,7 +7397,7 @@ public virtual Task OrderBy_bool_coming_from_optional_navigation(bool async)
ss => ss.Set<Weapon>().Select(w => w.SynergyWith).OrderBy(g => MaybeScalar<bool>(g, () => g.IsAutomatic)),
assertOrder: true);
}

[ConditionalFact]
public virtual void Byte_array_filter_by_length_parameter_compiled()
{
Expand All @@ -7411,6 +7411,75 @@ public virtual void Byte_array_filter_by_length_parameter_compiled()
Assert.Equal(2, query(context, byteQueryParam));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Can_query_on_indexed_properties(bool async)
{
return AssertQuery(
async,
ss => ss.Set<City>().Where(c => (string)c["Nation"] == "Tyrus"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Can_query_on_indexed_property_when_property_name_from_closure(bool async)
{
var propertyName = "Nation";
return AssertQuery(
async,
ss => ss.Set<City>().Where(c => (string)c[propertyName] == "Tyrus"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Can_project_indexed_properties(bool async)
{
return AssertQuery(
async,
ss => ss.Set<City>().Select(c => (string)c["Nation"]));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Can_OrderBy_indexed_properties(bool async)
{
return AssertQuery(
async,
ss => ss.Set<City>().Where(c => (string)c["Nation"] != null).OrderBy(c => (string)c["Nation"]).ThenBy(c => c.Name),
assertOrder: true);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Can_group_by_indexed_property_on_query(bool isAsync)
{
return AssertQueryScalar(
isAsync,
ss => ss.Set<City>().GroupBy(c => c["Nation"]).Select(g => g.Count()));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Can_group_by_converted_indexed_property_on_query(bool isAsync)
{
return AssertQueryScalar(
isAsync,
ss => ss.Set<City>().GroupBy(c => (string)c["Nation"]).Select(g => g.Count()));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Can_join_on_indexed_property_on_query(bool isAsync)
{
return AssertQuery(
isAsync,
ss =>
(from c1 in ss.Set<City>()
join c2 in ss.Set<City>()
on c1["Nation"] equals c2["Nation"]
select new { c1.Name, c2.Location }));
}

protected GearsOfWarContext CreateContext() => Fixture.CreateContext();

protected virtual void ClearLog()
Expand Down

0 comments on commit 8793087

Please sign in to comment.