diff --git a/src/EFCore/Extensions/ConventionEntityTypeExtensions.cs b/src/EFCore/Extensions/ConventionEntityTypeExtensions.cs index 599b52d5354..15505ce55a9 100644 --- a/src/EFCore/Extensions/ConventionEntityTypeExtensions.cs +++ b/src/EFCore/Extensions/ConventionEntityTypeExtensions.cs @@ -7,6 +7,8 @@ using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -433,7 +435,9 @@ public static IConventionProperty FindProperty([NotNull] this IConventionEntityT Check.NotNull(entityType, nameof(entityType)); Check.NotNull(memberInfo, nameof(memberInfo)); - return entityType.FindProperty(memberInfo.GetSimpleMemberName()); + return (memberInfo as PropertyInfo)?.IsIndexerProperty() == true + ? null + : entityType.FindProperty(memberInfo.GetSimpleMemberName()); } /// @@ -504,6 +508,31 @@ public static IConventionProperty FindDeclaredProperty([NotNull] this IConventio bool setTypeConfigurationSource = true, bool fromDataAnnotation = false) => entityType.AddProperty(name, propertyType, null, setTypeConfigurationSource, fromDataAnnotation); + /// + /// Adds an indexed property to this entity type. + /// + /// The entity type to add the property to. + /// The name of the property to add. + /// The type of value the property will hold. + /// Indicates whether the type configuration source should be set. + /// Indicates whether the configuration was specified using a data annotation. + /// The newly created property. + public static IConventionProperty AddIndexedProperty( + [NotNull] this IConventionEntityType entityType, [NotNull] string name, [NotNull] Type propertyType, + bool setTypeConfigurationSource = true, bool fromDataAnnotation = false) + { + Check.NotNull(entityType, nameof(entityType)); + + var indexerPropertyInfo = entityType.FindIndexerPropertyInfo(); + if (indexerPropertyInfo == null) + { + throw new InvalidOperationException( + CoreStrings.NonIndexerEntityType(name, entityType.DisplayName(), typeof(string).ShortDisplayName())); + } + + return entityType.AddProperty(name, propertyType, indexerPropertyInfo, setTypeConfigurationSource, fromDataAnnotation); + } + /// /// Gets the index defined on the given property. Returns null if no index is defined. /// diff --git a/src/EFCore/Extensions/EntityTypeExtensions.cs b/src/EFCore/Extensions/EntityTypeExtensions.cs index 12e3ee60728..4fbadcd219c 100644 --- a/src/EFCore/Extensions/EntityTypeExtensions.cs +++ b/src/EFCore/Extensions/EntityTypeExtensions.cs @@ -539,7 +539,9 @@ public static IProperty FindProperty([NotNull] this IEntityType entityType, [Not Check.NotNull(entityType, nameof(entityType)); Check.NotNull(memberInfo, nameof(memberInfo)); - return entityType.FindProperty(memberInfo.GetSimpleMemberName()); + return (memberInfo as PropertyInfo)?.IsIndexerProperty() == true + ? null + : entityType.FindProperty(memberInfo.GetSimpleMemberName()); } /// diff --git a/src/EFCore/Extensions/MutableEntityTypeExtensions.cs b/src/EFCore/Extensions/MutableEntityTypeExtensions.cs index a7a19109e54..c891baba173 100644 --- a/src/EFCore/Extensions/MutableEntityTypeExtensions.cs +++ b/src/EFCore/Extensions/MutableEntityTypeExtensions.cs @@ -6,6 +6,8 @@ using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; @@ -402,7 +404,7 @@ public static IMutableProperty FindProperty([NotNull] this IMutableEntityType en Check.NotNull(entityType, nameof(entityType)); Check.NotNull(propertyInfo, nameof(propertyInfo)); - return entityType.FindProperty(propertyInfo.GetSimpleMemberName()); + return propertyInfo.IsIndexerProperty() ? null : entityType.FindProperty(propertyInfo.GetSimpleMemberName()); } /// @@ -463,6 +465,28 @@ public static IMutableProperty FindDeclaredProperty([NotNull] this IMutableEntit [NotNull] this IMutableEntityType entityType, [NotNull] string name, [NotNull] Type propertyType) => entityType.AddProperty(name, propertyType, null); + /// + /// Adds an indexed property to this entity type. + /// + /// The entity type to add the property to. + /// The name of the property to add. + /// The type of value the property will hold. + /// The newly created property. + public static IMutableProperty AddIndexedProperty( + [NotNull] this IMutableEntityType entityType, [NotNull] string name, [NotNull] Type propertyType) + { + Check.NotNull(entityType, nameof(entityType)); + + var indexerPropertyInfo = entityType.FindIndexerPropertyInfo(); + if (indexerPropertyInfo == null) + { + throw new InvalidOperationException( + CoreStrings.NonIndexerEntityType(name, entityType.DisplayName(), typeof(string).ShortDisplayName())); + } + + return entityType.AddProperty(name, propertyType, indexerPropertyInfo); + } + /// /// Gets the index defined on the given property. Returns null if no index is defined. /// diff --git a/src/EFCore/Extensions/PropertyBaseExtensions.cs b/src/EFCore/Extensions/PropertyBaseExtensions.cs index f60775cc779..3e049ea0730 100644 --- a/src/EFCore/Extensions/PropertyBaseExtensions.cs +++ b/src/EFCore/Extensions/PropertyBaseExtensions.cs @@ -82,6 +82,18 @@ public static string GetFieldName([NotNull] this IPropertyBase propertyBase) public static bool IsShadowProperty([NotNull] this IPropertyBase property) => Check.NotNull(property, nameof(property)).GetIdentifyingMemberInfo() == null; + /// + /// Gets a value indicating whether this is an indexer property. An indexer property is one that is accessed through + /// an indexer on the entity class. + /// + /// The property to check. + /// + /// True if the property is an indexer property, otherwise false. + /// + public static bool IsIndexerProperty([NotNull] this IPropertyBase property) + => Check.NotNull(property, nameof(property)).GetIdentifyingMemberInfo() is PropertyInfo propertyInfo + && propertyInfo == property.DeclaringType.FindIndexerPropertyInfo(); + /// /// /// Gets the being used for this property. diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index ecc38f320b8..576070f54e5 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -783,7 +783,7 @@ private bool Contains(IForeignKey inheritedFk, IForeignKey derivedFk) .GetDeclaredProperties() .Cast() .Concat(entityType.GetDeclaredNavigations()) - .Where(p => !p.IsShadowProperty())); + .Where(p => !p.IsShadowProperty() && !p.IsIndexerProperty())); var constructorBinding = (InstantiationBinding)entityType[CoreAnnotationNames.ConstructorBinding]; diff --git a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs index 27909e58e13..8b7952ed017 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs @@ -40,7 +40,7 @@ public override IClrPropertyGetter Create(IPropertyBase property) Expression readExpression; if (memberInfo.DeclaringType.IsAssignableFrom(typeof(TEntity))) { - readExpression = Expression.MakeMemberAccess(entityParameter, memberInfo); + readExpression = CreateMemberAccess(entityParameter); } else { @@ -57,7 +57,7 @@ public override IClrPropertyGetter Create(IPropertyBase property) Expression.Condition( Expression.ReferenceEqual(converted, Expression.Constant(null)), Expression.Default(memberInfo.GetMemberType()), - Expression.MakeMemberAccess(converted, memberInfo)) + CreateMemberAccess(converted)) }); } @@ -101,6 +101,14 @@ public override IClrPropertyGetter Create(IPropertyBase property) return new ClrPropertyGetter( Expression.Lambda>(readExpression, entityParameter).Compile(), Expression.Lambda>(hasDefaultValueExpression, entityParameter).Compile()); + + Expression CreateMemberAccess(Expression parameter) + { + return propertyBase?.IsIndexerProperty() == true + ? Expression.MakeIndex( + entityParameter, (PropertyInfo)memberInfo, new List() { Expression.Constant(propertyBase.Name) }) + : (Expression)Expression.MakeMemberAccess(parameter, memberInfo); + } } } } diff --git a/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs index 0cd7413fec0..f8a917bd8e3 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs @@ -45,8 +45,7 @@ public override IClrPropertySetter Create(IPropertyBase property) Expression writeExpression; if (memberInfo.DeclaringType.IsAssignableFrom(typeof(TEntity))) { - writeExpression = Expression.MakeMemberAccess(entityParameter, memberInfo) - .Assign(convertedParameter); + writeExpression = CreateMemberAssignment(entityParameter); } else { @@ -62,8 +61,7 @@ public override IClrPropertySetter Create(IPropertyBase property) Expression.TypeAs(entityParameter, memberInfo.DeclaringType)), Expression.IfThen( Expression.ReferenceNotEqual(converted, Expression.Constant(null)), - Expression.MakeMemberAccess(converted, memberInfo) - .Assign(convertedParameter)) + CreateMemberAssignment(converted)) }); } @@ -78,6 +76,16 @@ public override IClrPropertySetter Create(IPropertyBase property) && propertyType.UnwrapNullableType().IsEnum ? new NullableEnumClrPropertySetter(setter) : (IClrPropertySetter)new ClrPropertySetter(setter); + + Expression CreateMemberAssignment(Expression parameter) + { + return propertyBase?.IsIndexerProperty() == true + ? Expression.Assign( + Expression.MakeIndex( + entityParameter, (PropertyInfo)memberInfo, new List() { Expression.Constant(propertyBase.Name) }), + convertedParameter) + : Expression.MakeMemberAccess(parameter, memberInfo).Assign(convertedParameter); + } } } } diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index 81f540f5689..cf9bf2bea40 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -1599,7 +1599,7 @@ private Type ValidateClrMember(string name, MemberInfo memberInfo, bool throwOnN if (name != memberInfo.GetSimpleMemberName()) { - if ((memberInfo as PropertyInfo)?.IsEFIndexerProperty() != true) + if (memberInfo != FindIndexerPropertyInfo()) { if (throwOnNameMismatch) { @@ -2066,7 +2066,7 @@ public virtual Index RemoveIndex([NotNull] Index index) if (memberInfo != null && propertyType != memberInfo.GetMemberType() - && (memberInfo as PropertyInfo)?.IsEFIndexerProperty() != true) + && memberInfo != FindIndexerPropertyInfo()) { if (typeConfigurationSource != null) { diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index 6aabdf9f7ba..76f5a238ca9 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -682,7 +682,7 @@ public virtual bool CanAddProperty([NotNull] Type propertyType, [NotNull] string var memberInfo = Metadata.ClrType.GetMembersInHierarchy(name).FirstOrDefault(); if (memberInfo != null && propertyType != memberInfo.GetMemberType() - && (memberInfo as PropertyInfo)?.IsEFIndexerProperty() != true + && memberInfo != Metadata.FindIndexerPropertyInfo() && typeConfigurationSource != null) { return false; diff --git a/src/EFCore/Metadata/Internal/PropertyBase.cs b/src/EFCore/Metadata/Internal/PropertyBase.cs index 5e9a7e1e597..3a3f6f6b789 100644 --- a/src/EFCore/Metadata/Internal/PropertyBase.cs +++ b/src/EFCore/Metadata/Internal/PropertyBase.cs @@ -80,8 +80,10 @@ public abstract class PropertyBase : ConventionAnnotatable, IMutablePropertyBase /// public virtual FieldInfo FieldInfo { - [DebuggerStepThrough] get => _fieldInfo; - [DebuggerStepThrough] set => SetField(value, ConfigurationSource.Explicit); + [DebuggerStepThrough] + get => _fieldInfo; + [DebuggerStepThrough] + set => SetField(value, ConfigurationSource.Explicit); } /// diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index 49f917ce675..c7998cad9a9 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -25,6 +25,8 @@ public abstract class TypeBase : ConventionAnnotatable, IMutableTypeBase, IConve private readonly Dictionary _ignoredMembers = new Dictionary(StringComparer.Ordinal); + private bool _indexerPropertyInitialized; + private PropertyInfo _indexerPropertyInfo; private Dictionary _runtimeProperties; private Dictionary _runtimeFields; @@ -174,9 +176,23 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS /// 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 void SetPropertyAccessMode( - PropertyAccessMode? propertyAccessMode, ConfigurationSource configurationSource) - => this.SetOrRemoveAnnotation(CoreAnnotationNames.PropertyAccessMode, propertyAccessMode, configurationSource); + public virtual PropertyInfo FindIndexerPropertyInfo() + { + if (ClrType == null) + { + return null; + } + + if (!_indexerPropertyInitialized) + { + var indexerPropertyInfo = GetRuntimeProperties().Values.FirstOrDefault(pi => pi.IsIndexerProperty()); + + Interlocked.CompareExchange(ref _indexerPropertyInfo, indexerPropertyInfo, null); + _indexerPropertyInitialized = true; + } + + return _indexerPropertyInfo; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -184,9 +200,9 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS /// 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 void SetNavigationAccessMode( + public virtual void SetPropertyAccessMode( PropertyAccessMode? propertyAccessMode, ConfigurationSource configurationSource) - => this.SetOrRemoveAnnotation(CoreAnnotationNames.NavigationAccessMode, propertyAccessMode, configurationSource); + => this.SetOrRemoveAnnotation(CoreAnnotationNames.PropertyAccessMode, propertyAccessMode, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -194,12 +210,9 @@ public virtual void UpdateConfigurationSource(ConfigurationSource configurationS /// 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 void ClearCaches() - { - _runtimeProperties = null; - _runtimeFields = null; - Thread.MemoryBarrier(); - } + public virtual void SetNavigationAccessMode( + PropertyAccessMode? propertyAccessMode, ConfigurationSource configurationSource) + => this.SetOrRemoveAnnotation(CoreAnnotationNames.NavigationAccessMode, propertyAccessMode, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs b/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs index f6846cca13e..de7a0fb487d 100644 --- a/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs +++ b/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs @@ -51,20 +51,19 @@ public static bool HasClrType([NotNull] this ITypeBase type) /// 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 MemberInfo FindClrMember([NotNull] this TypeBase type, [NotNull] string name) - { - if (type.GetRuntimeProperties().TryGetValue(name, out var property)) - { - return property; - } - - if (type.GetRuntimeFields().TryGetValue(name, out var field)) - { - return field; - } + public static PropertyInfo FindIndexerPropertyInfo([NotNull] this ITypeBase type) + => (type as TypeBase).FindIndexerPropertyInfo(); - return null; - } + /// + /// 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 MemberInfo FindClrMember([NotNull] this TypeBase type, [NotNull] string name) + => type.GetRuntimeProperties().TryGetValue(name, out var property) + ? property + : (MemberInfo)(type.GetRuntimeFields().TryGetValue(name, out var field) ? field : null); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index d4a9adfad03..25169f966c4 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Reflection; @@ -2308,6 +2308,14 @@ public static string SkipNavigationNonCollection([CanBeNull] object navigation, GetString("SkipNavigationNonCollection", nameof(navigation), nameof(entityType)), navigation, entityType); + /// + /// Cannot add property '{property}' on entity type '{entity}' since there is no indexer on '{entity}' taking a single argument of type '{type}'. + /// + public static string NonIndexerEntityType([CanBeNull] object property, [CanBeNull] object entity, [CanBeNull] object type) + => string.Format( + GetString("NonIndexerEntityType", nameof(property), nameof(entity), nameof(type)), + property, entity, type); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 372b8d2ba90..54d2bba2744 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -1245,4 +1245,7 @@ The skip navigation '{navigation}' on entity type '{entityType}' is not a collection. Only collection skip navigation properties are currently supported. + + Cannot add property '{property}' on entity type '{entity}' since there is no indexer on '{entity}' taking a single argument of type '{type}'. + \ No newline at end of file diff --git a/src/Shared/PropertyInfoExtensions.cs b/src/Shared/PropertyInfoExtensions.cs index 267d4c9f6b2..69c8330c93f 100644 --- a/src/Shared/PropertyInfoExtensions.cs +++ b/src/Shared/PropertyInfoExtensions.cs @@ -22,7 +22,7 @@ public static bool IsCandidateProperty(this PropertyInfo propertyInfo, bool need && (!publicOnly || propertyInfo.GetMethod.IsPublic) && propertyInfo.GetIndexParameters().Length == 0; - public static bool IsEFIndexerProperty([NotNull] this PropertyInfo propertyInfo) + public static bool IsIndexerProperty([NotNull] this PropertyInfo propertyInfo) { if (propertyInfo.PropertyType == typeof(object)) { diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs index b87650fa6eb..63469236328 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.InMemory.Metadata.Conventions; @@ -82,6 +83,19 @@ public void Delegate_getter_is_returned_for_struct_property_info() new Customer { Id = 7, Fuel = new Fuel(1.0) })); } + [ConditionalFact] + public void Delegate_getter_is_returned_for_index_property() + { + var modelBuilder = new ModelBuilder(InMemoryConventionSetBuilder.Build()); + modelBuilder.Entity().Property(e => e.Id); + var propertyA = modelBuilder.Entity().Metadata.AddIndexedProperty("PropertyA", typeof(string)); + var propertyB = modelBuilder.Entity().Metadata.AddIndexedProperty("PropertyB", typeof(int)); + modelBuilder.FinalizeModel(); + + Assert.Equal("ValueA", new ClrPropertyGetterFactory().Create(propertyA).GetClrValue(new IndexedClass { Id = 7 })); + Assert.Equal(123, new ClrPropertyGetterFactory().Create(propertyB).GetClrValue(new IndexedClass { Id = 7 })); + } + private class Customer { internal int Id { get; set; } @@ -93,5 +107,17 @@ private struct Fuel public Fuel(double volume) => Volume = volume; public double Volume { get; } } + + private class IndexedClass + { + private readonly Dictionary _internalValues = new Dictionary + { + {"PropertyA", "ValueA" }, + {"PropertyB", 123 } + }; + + internal int Id { get; set; } + internal object this[string name] => _internalValues[name]; + } } } diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs index 79d9c78bb56..a8cc7a04c6a 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore.Infrastructure; using Xunit; @@ -229,6 +230,25 @@ public void Delegate_setter_throws_if_no_setter_found() () => new ClrPropertySetterFactory().Create(property)); } + [ConditionalFact] + public void Delegate_setter_can_set_index_properties() + { + var entityType = CreateModel().AddEntityType(typeof(IndexedClass)); + var propertyA = entityType.AddIndexedProperty("PropertyA", typeof(string)); + var propertyB = entityType.AddIndexedProperty("PropertyB", typeof(int)); + + var indexedClass = new IndexedClass { Id = 7 }; + + Assert.Equal("ValueA", indexedClass["PropertyA"]); + Assert.Equal(123, indexedClass["PropertyB"]); + + new ClrPropertySetterFactory().Create(propertyA).SetClrValue(indexedClass, "UpdatedValue"); + new ClrPropertySetterFactory().Create(propertyB).SetClrValue(indexedClass, 42); + + Assert.Equal("UpdatedValue", indexedClass["PropertyA"]); + Assert.Equal(42, indexedClass["PropertyB"]); + } + private IMutableModel CreateModel() => new Model(); @@ -275,6 +295,18 @@ private class BaseEntity public int NoSetterProperty { get; } } + private class IndexedClass + { + private readonly Dictionary _internalValues = new Dictionary + { + {"PropertyA", "ValueA" }, + {"PropertyB", 123 } + }; + + internal int Id { get; set; } + internal object this[string name] { get => _internalValues[name]; set => _internalValues[name] = value; } + } + #endregion } } diff --git a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs index 8e8ae36ab32..81c3758dc44 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs @@ -2008,6 +2008,72 @@ public void Adding_a_CLR_service_property_to_shadow_type_throws() Assert.Throws(() => entityType.AddServiceProperty(Order.CustomerIdProperty)).Message); } + [ConditionalFact] + public void Can_add_indexed_property() + { + var model = CreateModel(); + var mutatbleEntityType = model.AddEntityType(typeof(Customer)); + var mutableProperty = mutatbleEntityType.AddIndexedProperty("Nation", typeof(string)); + + Assert.False(mutableProperty.IsShadowProperty()); + Assert.True(mutableProperty.IsIndexerProperty()); + Assert.Equal("Nation", mutableProperty.Name); + Assert.Same(typeof(string), mutableProperty.ClrType); + Assert.Same(mutatbleEntityType, mutableProperty.DeclaringEntityType); + + Assert.True(new[] { mutableProperty }.SequenceEqual(mutatbleEntityType.GetProperties())); + + Assert.Same(mutableProperty, mutatbleEntityType.RemoveProperty("Nation")); + Assert.Empty(mutatbleEntityType.GetProperties()); + + var conventionEntityType = (IConventionEntityType)mutatbleEntityType; + var conventionProperty = conventionEntityType.AddIndexedProperty("Country", typeof(string)); + + Assert.False(conventionProperty.IsShadowProperty()); + Assert.True(conventionProperty.IsIndexerProperty()); + Assert.Equal("Country", conventionProperty.Name); + Assert.Same(typeof(string), conventionProperty.ClrType); + Assert.Same(mutatbleEntityType, conventionProperty.DeclaringEntityType); + + Assert.True(new[] { conventionProperty }.SequenceEqual(conventionEntityType.GetProperties())); + + Assert.Same(conventionProperty, conventionEntityType.RemoveProperty("Country")); + Assert.Empty(conventionEntityType.GetProperties()); + } + + [ConditionalFact] + public void FindProperty_return_null_when_passed_indexer_property_info() + { + var model = CreateModel(); + var entityType = model.AddEntityType(typeof(Customer)); + var property = entityType.AddIndexedProperty("Nation", typeof(string)); + var itemProperty = entityType.AddProperty("Item", typeof(string)); + var indexerPropertyInfo = typeof(Customer).GetRuntimeProperty("Item"); + Assert.NotNull(indexerPropertyInfo); + + Assert.Same(property, entityType.FindProperty("Nation")); + + Assert.Null(((IEntityType)entityType).FindProperty(indexerPropertyInfo)); + Assert.Null(entityType.FindProperty(indexerPropertyInfo)); + Assert.Null(((IConventionEntityType)entityType).FindProperty(indexerPropertyInfo)); + } + + [ConditionalFact] + public void AddIndexedProperty_throws_when_entitytype_does_not_have_indexer() + { + var model = CreateModel(); + var entityType = model.AddEntityType(typeof(Order)); + + Assert.Equal( + CoreStrings.NonIndexerEntityType("Nation", entityType.DisplayName(), typeof(string).ShortDisplayName()), + Assert.Throws(() => entityType.AddIndexedProperty("Nation", typeof(string))).Message); + + Assert.Equal( + CoreStrings.NonIndexerEntityType("Nation", entityType.DisplayName(), typeof(string).ShortDisplayName()), + Assert.Throws( + () => ((IConventionEntityType)entityType).AddIndexedProperty("Nation", typeof(string))).Message); + } + [ConditionalFact] public void Can_get_property_indexes() {