diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs index 34140b9caa7..78b7d5a9651 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs @@ -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; @@ -46,8 +47,10 @@ private readonly Stack _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. /// - public CosmosProjectionBindingExpressionVisitor([NotNull] CosmosSqlTranslatingExpressionVisitor sqlTranslator) + public CosmosProjectionBindingExpressionVisitor( + [NotNull] IModel model, [NotNull] CosmosSqlTranslatingExpressionVisitor sqlTranslator) { + _model = model; _sqlTranslator = sqlTranslator; } @@ -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) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index a8a9f02c1fa..98952cb22f2 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -49,7 +49,7 @@ public CosmosQueryableMethodTranslatingExpressionVisitor( sqlExpressionFactory, memberTranslatorProvider, methodCallTranslatorProvider); - _projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_sqlTranslator); + _projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_model, _sqlTranslator); } /// @@ -65,7 +65,7 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( _model = parentVisitor._model; _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; _sqlTranslator = parentVisitor._sqlTranslator; - _projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_sqlTranslator); + _projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_model, _sqlTranslator); } /// diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index 0d1cc9bf911..2b772681843 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -128,6 +128,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; @@ -164,6 +169,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; diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index eb76a9ec990..336d4617a04 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -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 @@ -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); @@ -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)); @@ -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); @@ -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) diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index b9cce3e92ab..f533dd4e66f 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -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; @@ -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); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 1cfedd3f776..7ddd029b2be 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -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); } diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 3c03c11b4e8..b4d433d6fef 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -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(_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) diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs index 5ea15cce951..45550ba80d3 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs @@ -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) { diff --git a/src/EFCore/Extensions/Internal/EFPropertyExtensions.cs b/src/EFCore/Extensions/Internal/EFPropertyExtensions.cs index 1d241600ffa..67223b0b928 100644 --- a/src/EFCore/Extensions/Internal/EFPropertyExtensions.cs +++ b/src/EFCore/Extensions/Internal/EFPropertyExtensions.cs @@ -24,51 +24,6 @@ namespace Microsoft.EntityFrameworkCore.Internal // ReSharper disable once InconsistentNaming public static class EFPropertyExtensions { - /// - /// 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. - /// - 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; - } - - /// - /// 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. - /// - public static bool IsEFIndexer([NotNull] this MethodCallExpression methodCallExpression) - => IsEFIndexer(methodCallExpression.Method); - - /// - /// 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. - /// - 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; - /// /// 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 diff --git a/src/EFCore/Extensions/Internal/TypeExtensions.cs b/src/EFCore/Extensions/Internal/TypeExtensions.cs index e3ccd12eb16..a2fa302a057 100644 --- a/src/EFCore/Extensions/Internal/TypeExtensions.cs +++ b/src/EFCore/Extensions/Internal/TypeExtensions.cs @@ -213,5 +213,20 @@ public static bool IsQueryableType([NotNull] this Type type) return type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryable<>)); } + + /// + /// 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. + /// + public static PropertyInfo FindIndexerProperty([NotNull] this Type type) + { + var defaultPropertyAttribute = type.GetCustomAttributes().FirstOrDefault(); + + return defaultPropertyAttribute == null + ? null + : type.GetRuntimeProperties().FirstOrDefault(pi => pi.Name == defaultPropertyAttribute.MemberName && pi.IsIndexerProperty()); + } } } diff --git a/src/EFCore/Extensions/ModelExtensions.cs b/src/EFCore/Extensions/ModelExtensions.cs index 1927cbbf9a2..4c7346d739b 100644 --- a/src/EFCore/Extensions/ModelExtensions.cs +++ b/src/EFCore/Extensions/ModelExtensions.cs @@ -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; @@ -150,5 +151,17 @@ public static PropertyAccessMode GetPropertyAccessMode([NotNull] this IModel mod /// The model to get the version for. public static string GetProductVersion([NotNull] this IModel model) => model[CoreAnnotationNames.ProductVersion] as string; + + /// + /// Gets a value indicating whether the given MethodInfo reprensent an indexer access. + /// + /// The model to use. + /// The MethodInfo to check for. + public static bool IsIndexerMethod([NotNull] this IModel model, [NotNull] MethodInfo methodInfo) + => !methodInfo.IsStatic + && methodInfo.IsSpecialName + && methodInfo.DeclaringType is Type declaringType + && model.AsModel().FindIndexerPropertyInfo(declaringType) is PropertyInfo indexerProperty + && (methodInfo == indexerProperty.GetMethod || methodInfo == indexerProperty.SetMethod); } } diff --git a/src/EFCore/Infrastructure/ExpressionExtensions.cs b/src/EFCore/Infrastructure/ExpressionExtensions.cs index 0ab30f77459..48d5a8b970e 100644 --- a/src/EFCore/Infrastructure/ExpressionExtensions.cs +++ b/src/EFCore/Infrastructure/ExpressionExtensions.cs @@ -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; @@ -107,6 +108,33 @@ public static bool TryGetEFPropertyArguments( return false; } + /// + /// 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. + /// + /// The method-call expression for indexer. + /// The model to use. + /// The extracted entity access expression. + /// The accessed property name. + /// True if the method-call was for indexer; false otherwise. + 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; + } + /// /// /// Gets the represented by a simple property-access expression. diff --git a/src/EFCore/Infrastructure/MethodInfoExtensions.cs b/src/EFCore/Infrastructure/MethodInfoExtensions.cs index f84dba2836c..bf22a4f22ad 100644 --- a/src/EFCore/Infrastructure/MethodInfoExtensions.cs +++ b/src/EFCore/Infrastructure/MethodInfoExtensions.cs @@ -1,8 +1,10 @@ // 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.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Infrastructure { @@ -31,5 +33,17 @@ public static bool IsEFPropertyMethod([CanBeNull] this MethodInfo methodInfo) || methodInfo?.IsGenericMethod == true && methodInfo.Name == nameof(EF.Property) && methodInfo.DeclaringType?.FullName == _efTypeName; + + /// + /// Returns true if the given method represents a indexer property access. + /// + /// The method. + /// True if the method is ; false otherwise. + public static bool IsIndexerMethod([NotNull] this MethodInfo methodInfo) + => !methodInfo.IsStatic + && methodInfo.IsSpecialName + && methodInfo.DeclaringType is Type declaringType + && declaringType.FindIndexerProperty() is PropertyInfo indexerProperty + && (methodInfo == indexerProperty.GetMethod || methodInfo == indexerProperty.SetMethod); } } diff --git a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs index 8b7952ed017..b4191b7eeec 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs @@ -106,7 +106,7 @@ Expression CreateMemberAccess(Expression parameter) { return propertyBase?.IsIndexerProperty() == true ? Expression.MakeIndex( - entityParameter, (PropertyInfo)memberInfo, new List() { Expression.Constant(propertyBase.Name) }) + parameter, (PropertyInfo)memberInfo, new List() { Expression.Constant(propertyBase.Name) }) : (Expression)Expression.MakeMemberAccess(parameter, memberInfo); } } diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 34d2c4d247e..551b81528dc 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; @@ -36,6 +37,9 @@ public class Model : ConventionAnnotatable, IMutableModel, IConventionModel private readonly SortedDictionary _entityTypes = new SortedDictionary(StringComparer.Ordinal); + private readonly ConcurrentDictionary _indexerPropertyInfoMap + = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _clrTypeNameMap = new ConcurrentDictionary(); @@ -841,6 +845,15 @@ public virtual IModel MakeReadonly() return this; } + /// + /// 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. + /// + public virtual PropertyInfo FindIndexerPropertyInfo([NotNull] Type type) + => _indexerPropertyInfoMap.GetOrAdd(type, type.FindIndexerProperty()); + /// /// 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 diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index 8fc0c074eff..5ecc722f07b 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -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; @@ -67,7 +68,7 @@ private static Func CreateCurrentValueGetter CreateCurrentValueGetter() { Expression.Constant(propertyBase.Name) }) + : (Expression)Expression.MakeMemberAccess(parameter, memberInfo); + } } private static Func CreateOriginalValueGetter(IProperty property) diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index c7998cad9a9..17c4484d8cd 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Threading; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Internal @@ -185,7 +186,7 @@ public virtual PropertyInfo FindIndexerPropertyInfo() if (!_indexerPropertyInitialized) { - var indexerPropertyInfo = GetRuntimeProperties().Values.FirstOrDefault(pi => pi.IsIndexerProperty()); + var indexerPropertyInfo = ClrType.FindIndexerProperty(); Interlocked.CompareExchange(ref _indexerPropertyInfo, indexerPropertyInfo, null); _indexerPropertyInitialized = true; diff --git a/src/EFCore/Query/EntityMaterializerSource.cs b/src/EFCore/Query/EntityMaterializerSource.cs index bcbada88316..84b25be969c 100644 --- a/src/EFCore/Query/EntityMaterializerSource.cs +++ b/src/EFCore/Query/EntityMaterializerSource.cs @@ -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.Constant(property.Name) }), + value) + : Expression.MakeMemberAccess(parameter, memberInfo).Assign(value); + } } private ConcurrentDictionary> Materializers diff --git a/src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs b/src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs index 9b646dda11d..cb3c0ac4ab1 100644 --- a/src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/EntityEqualityRewritingExpressionVisitor.cs @@ -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] }); @@ -288,6 +287,16 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp : newMethodCall; } + // Navigation via an indexer property + if (methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model, 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 @@ -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 dtoType) diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index f5dc4a95ad8..93aab9fbc7e 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -26,6 +26,7 @@ public ExpandingExpressionVisitor( { _navigationExpandingExpressionVisitor = navigationExpandingExpressionVisitor; _source = source; + Model = navigationExpandingExpressionVisitor._queryCompilationContext.Model; } public Expression Expand(Expression expression, bool applyIncludes = false) @@ -40,6 +41,8 @@ public Expression Expand(Expression expression, bool applyIncludes = false) return expression; } + protected IModel Model { get; } + protected override Expression VisitExtension(Expression expression) { Check.NotNull(expression, nameof(expression)); @@ -75,6 +78,13 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp ?? methodCallExpression.Update(null, new[] { source, methodCallExpression.Arguments[1] }); } + if (methodCallExpression.TryGetIndexerArguments(Model, out source, out navigationName)) + { + source = Visit(source); + return TryExpandNavigation(source, MemberIdentity.Create(navigationName)) + ?? methodCallExpression.Update(source, new[] { methodCallExpression.Arguments[0] }); + } + return base.VisitMethodCall(methodCallExpression); } @@ -382,6 +392,19 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return methodCallExpression; } + if (methodCallExpression.TryGetIndexerArguments(Model, out var source, out var propertyName)) + { + if (UnwrapEntityReference(source) is EntityReference entityReferece) + { + // If it is mapped property then, it would get converted to a column so we don't need to expand includes. + var property = entityReferece.EntityType.FindProperty(propertyName); + if (property != null) + { + return methodCallExpression; + } + } + } + return base.VisitMethodCall(methodCallExpression); } diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index a6ed534c7cd..75286fbda7a 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -55,7 +55,7 @@ public NavigationExpandingExpressionVisitor( { _queryCompilationContext = queryCompilationContext; _pendingSelectorExpandingExpressionVisitor = new PendingSelectorExpandingExpressionVisitor(this); - _subqueryMemberPushdownExpressionVisitor = new SubqueryMemberPushdownExpressionVisitor(); + _subqueryMemberPushdownExpressionVisitor = new SubqueryMemberPushdownExpressionVisitor(queryCompilationContext.Model); _reducingExpressionVisitor = new ReducingExpressionVisitor(); _entityReferenceOptionalMarkingExpressionVisitor = new EntityReferenceOptionalMarkingExpressionVisitor(); _enumerableToQueryableMethodConvertingExpressionVisitor = new EnumerableToQueryableMethodConvertingExpressionVisitor(); diff --git a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs index b767c0215f0..c83440b7cea 100644 --- a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs @@ -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; @@ -592,7 +592,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp if (_evaluatableExpressions.ContainsKey(methodCallExpression.Arguments[i])) { if (parameterInfos[i].GetCustomAttribute() != null - || methodCallExpression.Method.IsEFIndexer()) + || _model.IsIndexerMethod(methodCallExpression.Method)) { _evaluatableExpressions[methodCallExpression.Arguments[i]] = false; } diff --git a/src/EFCore/Query/Internal/SubqueryMemberPushdownExpressionVisitor.cs b/src/EFCore/Query/Internal/SubqueryMemberPushdownExpressionVisitor.cs index 4fa21505d5b..8d3469d61e1 100644 --- a/src/EFCore/Query/Internal/SubqueryMemberPushdownExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/SubqueryMemberPushdownExpressionVisitor.cs @@ -3,10 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query.Internal @@ -41,6 +42,13 @@ public class SubqueryMemberPushdownExpressionVisitor : ExpressionVisitor { QueryableMethods.LastOrDefaultWithPredicate, QueryableMethods.LastOrDefaultWithoutPredicate } }; + private readonly IModel _model; + + public SubqueryMemberPushdownExpressionVisitor([NotNull] IModel model) + { + _model = model; + } + protected override Expression VisitMember(MemberExpression memberExpression) { Check.NotNull(memberExpression, nameof(memberExpression)); @@ -97,6 +105,37 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } } + if (methodCallExpression.TryGetIndexerArguments(_model, out source, out _)) + { + source = Visit(source); + + if (source is MethodCallExpression innerMethodCall + && innerMethodCall.Method.IsGenericMethod + && _supportedMethods.Contains(innerMethodCall.Method.GetGenericMethodDefinition())) + { + return PushdownMember( + innerMethodCall, + (target, nullable) => + { + var propertyType = methodCallExpression.Type; + if (nullable && !propertyType.IsNullableType()) + { + propertyType = propertyType.MakeNullable(); + } + + var indexerExpression = Expression.Call( + target, + methodCallExpression.Method, + new[] { methodCallExpression.Arguments[0] }); + + return nullable && !indexerExpression.Type.IsNullableType() + ? Expression.Convert(indexerExpression, indexerExpression.Type.MakeNullable()) + : (Expression)indexerExpression; + }, + methodCallExpression.Type); + } + } + // Avoid pushing down a collection navigation which is followed by AsQueryable if (methodCallExpression.Method.IsGenericMethod && methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs index be5611d7106..fc284dc4d24 100644 --- a/src/EFCore/Query/QueryTranslationPreprocessor.cs +++ b/src/EFCore/Query/QueryTranslationPreprocessor.cs @@ -36,7 +36,7 @@ public virtual Expression Process([NotNull] Expression query) query = new GroupJoinFlatteningExpressionVisitor().Visit(query); query = new NullCheckRemovingExpressionVisitor().Visit(query); query = new EntityEqualityRewritingExpressionVisitor(_queryCompilationContext).Rewrite(query); - query = new SubqueryMemberPushdownExpressionVisitor().Visit(query); + query = new SubqueryMemberPushdownExpressionVisitor(_queryCompilationContext.Model).Visit(query); query = new NavigationExpandingExpressionVisitor(_queryCompilationContext, Dependencies.EvaluatableExpressionFilter).Expand( query); query = new FunctionPreprocessingExpressionVisitor().Visit(query); diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryFixtureBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryFixtureBase.cs index 44079039793..69a5e6c1c5b 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryFixtureBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryFixtureBase.cs @@ -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"]); } } }, @@ -243,7 +243,12 @@ protected virtual QueryAsserter CreateQueryAsserter( protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { - modelBuilder.Entity().HasKey(c => c.Name); + modelBuilder.Entity( + b => + { + b.HasKey(c => c.Name); + b.Metadata.AddIndexedProperty("Nation", typeof(string)); + }); modelBuilder.Entity( b => diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index ec885ef1b58..4e8264617ab 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -7407,6 +7407,94 @@ 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().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().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().Select(c => (string)c["Nation"])); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Can_OrderBy_indexed_properties(bool async) + { + return AssertQuery( + async, + ss => ss.Set().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().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().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() + join c2 in ss.Set() + on c1["Nation"] equals c2["Nation"] + select new { c1.Name, c2.Location })); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Projecting_index_property_ignores_include(bool isAsync) + { + return AssertQuery( + isAsync, + ss => from c in ss.Set().Include(c => c.BornGears).AsTracking() + select new { Nation = c["Nation"] }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Indexer_property_is_pushdown_into_subquery(bool isAsync) + { + return AssertQuery( + isAsync, + ss => ss.Set().Where(g => ss.Set().Where(c => c.Name == g.CityOfBirthName).FirstOrDefault()["Nation"] == null)); + } + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); protected virtual void ClearLog() diff --git a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/City.cs b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/City.cs index d5d90485113..5e18a9829c7 100644 --- a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/City.cs +++ b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/City.cs @@ -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 BornGears { get; set; } public List StationedGears { get; set; } diff --git a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs index 5ccaa8dbc6a..be5ca187b94 100644 --- a/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs +++ b/test/EFCore.Specification.Tests/TestModels/GearsOfWarModel/GearsOfWarData.cs @@ -151,15 +151,15 @@ public static IReadOnlyList 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" }; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index c1eeb069199..d4a6fb1a2e9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -7400,6 +7400,98 @@ FROM [Weapons] AS [w] ORDER BY [w0].[IsAutomatic]"); } + public override async Task Can_query_on_indexed_properties(bool async) + { + await base.Can_query_on_indexed_properties(async); + + AssertSql( + @"SELECT [c].[Name], [c].[Location], [c].[Nation] +FROM [Cities] AS [c] +WHERE [c].[Nation] = N'Tyrus'"); + } + + public override async Task Can_query_on_indexed_property_when_property_name_from_closure(bool async) + { + await base.Can_query_on_indexed_property_when_property_name_from_closure(async); + + AssertSql( + @"SELECT [c].[Name], [c].[Location], [c].[Nation] +FROM [Cities] AS [c] +WHERE [c].[Nation] = N'Tyrus'"); + } + + public override async Task Can_project_indexed_properties(bool async) + { + await base.Can_project_indexed_properties(async); + + AssertSql( + @"SELECT [c].[Nation] +FROM [Cities] AS [c]"); + } + + public override async Task Can_OrderBy_indexed_properties(bool async) + { + await base.Can_OrderBy_indexed_properties(async); + + AssertSql( + @"SELECT [c].[Name], [c].[Location], [c].[Nation] +FROM [Cities] AS [c] +WHERE [c].[Nation] IS NOT NULL +ORDER BY [c].[Nation], [c].[Name]"); + } + + public override async Task Can_group_by_indexed_property_on_query(bool isAsync) + { + await base.Can_group_by_indexed_property_on_query(isAsync); + + AssertSql( + @"SELECT COUNT(*) +FROM [Cities] AS [c] +GROUP BY [c].[Nation]"); + } + + public override async Task Can_group_by_converted_indexed_property_on_query(bool isAsync) + { + await base.Can_group_by_converted_indexed_property_on_query(isAsync); + + AssertSql( + @"SELECT COUNT(*) +FROM [Cities] AS [c] +GROUP BY [c].[Nation]"); + } + + public override async Task Can_join_on_indexed_property_on_query(bool isAsync) + { + await base.Can_join_on_indexed_property_on_query(isAsync); + + AssertSql( + @"SELECT [c].[Name], [c0].[Location] +FROM [Cities] AS [c] +INNER JOIN [Cities] AS [c0] ON [c].[Nation] = [c0].[Nation]"); + } + + public override async Task Projecting_index_property_ignores_include(bool isAsync) + { + await base.Projecting_index_property_ignores_include(isAsync); + + AssertSql( + @"SELECT [c].[Nation] +FROM [Cities] AS [c]"); + } + + public override async Task Indexer_property_is_pushdown_into_subquery(bool isAsync) + { + await base.Indexer_property_is_pushdown_into_subquery(isAsync); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Gear', N'Officer') AND ( + SELECT TOP(1) [c].[Nation] + FROM [Cities] AS [c] + WHERE [c].[Name] = [g].[CityOfBirthName]) IS NULL"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Tests/Metadata/Internal/PropertyAccessorsFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/PropertyAccessorsFactoryTest.cs index 7fbaf9a8cea..4e640e0edef 100644 --- a/test/EFCore.Tests/Metadata/Internal/PropertyAccessorsFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/PropertyAccessorsFactoryTest.cs @@ -14,6 +14,32 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal { public class PropertyAccessorsFactoryTest { + [ConditionalFact] + public void Can_use_PropertyAccessorsFactory_on_indexed_property() + { + IMutableModel model = new Model(); + var entityType = model.AddEntityType(typeof(IndexedClass)); + var id = entityType.AddProperty("Id", typeof(int)); + var propertyA = entityType.AddIndexedProperty("PropertyA", typeof(string)); + model.FinalizeModel(); + + var contextServices = InMemoryTestHelpers.Instance.CreateContextServices(model); + var stateManager = contextServices.GetRequiredService(); + var factory = contextServices.GetRequiredService(); + + var entity = new IndexedClass(); + var entry = factory.Create(stateManager, entityType, entity); + + var propertyAccessors = new PropertyAccessorsFactory().Create(propertyA); + Assert.Equal("ValueA", ((Func)propertyAccessors.CurrentValueGetter)(entry)); + Assert.Equal("ValueA", ((Func)propertyAccessors.OriginalValueGetter)(entry)); + Assert.Equal("ValueA", ((Func)propertyAccessors.PreStoreGeneratedCurrentValueGetter)(entry)); + Assert.Equal("ValueA", ((Func)propertyAccessors.RelationshipSnapshotGetter)(entry)); + + var valueBuffer = new ValueBuffer(new object[] { 1, "ValueA" }); + Assert.Equal("ValueA", propertyAccessors.ValueBufferGetter(valueBuffer)); + } + [ConditionalFact] public void Can_use_PropertyAccessorsFactory_on_non_indexed_property() {