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 ce6e1da commit 8f22672
Show file tree
Hide file tree
Showing 13 changed files with 108 additions and 62 deletions.
Expand Up @@ -336,6 +336,11 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
throw new InvalidOperationException("EF.Property called with wrong property name.");
}

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/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
3 changes: 1 addition & 2 deletions src/EFCore/Query/EntityMaterializerSource.cs
Expand Up @@ -142,8 +142,7 @@ var readValueExpression

blockExpressions.Add(instanceVariable);

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

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 @@ -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
11 changes: 10 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,15 @@ 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"));
}

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

protected virtual void ClearLog()
Expand Down
@@ -1,18 +1,41 @@
// 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.Collections.Generic;

namespace Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel
{
public class City
{
private string _nation;
// non-integer key with not conventional name
public string Name { get; set; }

public string Location { get; set; }

public string Nation { get; set; }
public object this[string name]
{
get
{
if (!string.Equals(name, "Nation", StringComparison.Ordinal))
{
throw new InvalidOperationException($"Indexed property with key {name} is not defined on {nameof(City)}.");
}

return _nation;
}

set
{
if (!string.Equals(name, "Nation", StringComparison.Ordinal))
{
throw new InvalidOperationException($"Indexed property with key {name} is not defined on {nameof(City)}.");
}

_nation = (string)value;
}
}

public List<Gear> BornGears { get; set; }
public List<Gear> StationedGears { get; set; }
Expand Down
Expand Up @@ -151,15 +151,15 @@ public static IReadOnlyList<City> CreateCities()
{
Location = "Jacinto's location",
Name = "Jacinto",
Nation = "Tyrus"
};
jacinto["Nation"] = "Tyrus";

var ephyra = new City
{
Location = "Ephyra's location",
Name = "Ephyra",
Nation = "Tyrus"
};
ephyra["Nation"] = "Tyrus";

var hanover = new City { Location = "Hanover's location", Name = "Hanover" };

Expand Down

0 comments on commit 8f22672

Please sign in to comment.