diff --git a/src/EFCore/ChangeTracking/Internal/ISharedTypeEntityFinder.cs b/src/EFCore/ChangeTracking/Internal/ISharedTypeEntityFinder.cs new file mode 100644 index 00000000000..bd296d448b7 --- /dev/null +++ b/src/EFCore/ChangeTracking/Internal/ISharedTypeEntityFinder.cs @@ -0,0 +1,22 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public interface ISharedTypeEntityFinder + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + /// the shared-type EntityType corresponding to this instance if found in the model, or null if not. + IEntityType FindSharedTypeEntityType([NotNull] object entityInstance); + } +} diff --git a/src/EFCore/ChangeTracking/Internal/SelfDescribingIndexPropertyEntityFinder.cs b/src/EFCore/ChangeTracking/Internal/SelfDescribingIndexPropertyEntityFinder.cs new file mode 100644 index 00000000000..48d372285e4 --- /dev/null +++ b/src/EFCore/ChangeTracking/Internal/SelfDescribingIndexPropertyEntityFinder.cs @@ -0,0 +1,83 @@ +// 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; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class SelfDescribingIndexPropertyEntityFinder : ISharedTypeEntityFinder + { + public const string DefaultEntityTypeNamePropertyName = "__EntityTypeName__"; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public SelfDescribingIndexPropertyEntityFinder([NotNull] IModel model) + { + Check.NotNull(model, nameof(model)); + + Model = model; + EntityTypeNamePropertyName = DefaultEntityTypeNamePropertyName; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual string EntityTypeNamePropertyName { get; [NotNull]set; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual IModel Model { get; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + /// the shared-type EntityType corresponding to this instance if found in the model, or null if not. + public virtual IEntityType FindSharedTypeEntityType(object entityInstance) + { + Check.NotNull(entityInstance, nameof(entityInstance)); + + var efIndexerPropInfo = entityInstance.GetType() + .GetRuntimeProperties().FirstOrDefault(p => p.IsEFIndexerProperty()); + if (efIndexerPropInfo == null) + { + return null; + } + + string entityTypeName = null; + try + { + entityTypeName = efIndexerPropInfo + .GetValue(entityInstance, new[] { EntityTypeNamePropertyName }) as string; + } + catch + { + // exceptions indicate the indexer does not have the + // EntityTypeNamePropertyName key etc. + } + + var entityType = entityTypeName == null + ? null + : Model.FindEntityType(entityTypeName); + + return entityType != null && entityType.IsSharedType ? entityType : null; + } + } +} diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 8a5ae9254f2..25ca951d42c 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -47,6 +47,7 @@ public class StateManager : IStateManager private readonly IModel _model; private readonly IDatabase _database; private readonly IConcurrencyDetector _concurrencyDetector; + private readonly ISharedTypeEntityFinder _sharedTypeEntityFinder; /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -72,6 +73,7 @@ public StateManager([NotNull] StateManagerDependencies dependencies) UpdateLogger = dependencies.UpdateLogger; _changeTrackingLogger = dependencies.ChangeTrackingLogger; + _sharedTypeEntityFinder = dependencies.SharedTypeEntityFinder; } /// @@ -155,7 +157,8 @@ public virtual InternalEntityEntry GetOrCreateEntry(object entity) { _trackingQueryMode = TrackingQueryMode.Multiple; - var entityType = _model.FindRuntimeEntityType(entity.GetType()); + var entityType = _sharedTypeEntityFinder.FindSharedTypeEntityType(entity) + ?? _model.FindRuntimeEntityType(entity.GetType()); if (entityType == null) { if (_model.HasEntityTypeWithDefiningNavigation(entity.GetType())) diff --git a/src/EFCore/ChangeTracking/Internal/StateManagerDependencies.cs b/src/EFCore/ChangeTracking/Internal/StateManagerDependencies.cs index 7311724918d..cdc587cb6b8 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManagerDependencies.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManagerDependencies.cs @@ -60,7 +60,8 @@ public sealed class StateManagerDependencies [NotNull] IEntityMaterializerSource entityMaterializerSource, [NotNull] ILoggingOptions loggingOptions, [NotNull] IDiagnosticsLogger updateLogger, - [NotNull] IDiagnosticsLogger changeTrackingLogger) + [NotNull] IDiagnosticsLogger changeTrackingLogger, + [NotNull] ISharedTypeEntityFinder sharedEntityTypeFinder) { InternalEntityEntryFactory = internalEntityEntryFactory; InternalEntityEntrySubscriber = internalEntityEntrySubscriber; @@ -76,6 +77,7 @@ public sealed class StateManagerDependencies LoggingOptions = loggingOptions; UpdateLogger = updateLogger; ChangeTrackingLogger = changeTrackingLogger; + SharedTypeEntityFinder = sharedEntityTypeFinder; } /// @@ -162,6 +164,12 @@ public sealed class StateManagerDependencies /// public IDiagnosticsLogger ChangeTrackingLogger { get; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public ISharedTypeEntityFinder SharedTypeEntityFinder { get; } + /// /// Clones this dependency parameter object with one service replaced. /// @@ -182,7 +190,8 @@ public StateManagerDependencies With([NotNull] IInternalEntityEntryFactory inter EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -204,7 +213,8 @@ public StateManagerDependencies With([NotNull] IInternalEntityEntrySubscriber in EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -226,7 +236,8 @@ public StateManagerDependencies With([NotNull] IInternalEntityEntryNotifier inte EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -248,7 +259,8 @@ public StateManagerDependencies With([NotNull] ValueGenerationManager valueGener EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -270,7 +282,8 @@ public StateManagerDependencies With([NotNull] IModel model) EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -292,7 +305,8 @@ public StateManagerDependencies With([NotNull] IDatabase database) EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -314,7 +328,8 @@ public StateManagerDependencies With([NotNull] IConcurrencyDetector concurrencyD EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -336,7 +351,8 @@ public StateManagerDependencies With([NotNull] ICurrentDbContext currentContext) EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -358,7 +374,8 @@ public StateManagerDependencies With([NotNull] IEntityFinderSource entityFinderS EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -380,7 +397,8 @@ public StateManagerDependencies With([NotNull] IDbSetSource setSource) EntityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -402,7 +420,8 @@ public StateManagerDependencies With([NotNull] IEntityMaterializerSource entityM entityMaterializerSource, LoggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -424,7 +443,8 @@ public StateManagerDependencies With([NotNull] ILoggingOptions loggingOptions) EntityMaterializerSource, loggingOptions, UpdateLogger, - ChangeTrackingLogger); + ChangeTrackingLogger, + SharedTypeEntityFinder); /// /// Clones this dependency parameter object with one service replaced. @@ -446,7 +466,8 @@ public StateManagerDependencies With([NotNull] IDiagnosticsLogger /// Clones this dependency parameter object with one service replaced. @@ -468,6 +489,31 @@ public StateManagerDependencies With([NotNull] IDiagnosticsLogger + /// Clones this dependency parameter object with one service replaced. + /// + /// A replacement for the current dependency of this type. + /// A new parameter object with the given service replaced. + public StateManagerDependencies With([NotNull] ISharedTypeEntityFinder sharedTypeEntityFinder) + => new StateManagerDependencies( + InternalEntityEntryFactory, + InternalEntityEntrySubscriber, + InternalEntityEntryNotifier, + ValueGenerationManager, + Model, + Database, + ConcurrencyDetector, + CurrentContext, + EntityFinderSource, + SetSource, + EntityMaterializerSource, + LoggingOptions, + UpdateLogger, + ChangeTrackingLogger, + sharedTypeEntityFinder); + } } diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index cb41a4a6af8..c60395cdd26 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -54,6 +54,7 @@ public class DbContext : IDbContextPoolable { private IDictionary _sets; + private IDictionary _sharedTypeSets; private IDictionary _queries; private readonly DbContextOptions _options; @@ -215,6 +216,28 @@ object IDbSetCache.GetOrAddSet(IDbSetSource source, Type type) return set; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + object IDbSetCache.GetOrAddSharedTypeSet(IDbSetSource source, string entityTypeName, Type clrType) + { + CheckDisposed(); + + if (_sharedTypeSets == null) + { + _sharedTypeSets = new Dictionary(); + } + + if (!_sharedTypeSets.TryGetValue(entityTypeName, out var set)) + { + set = source.CreateSharedTypeSet(this, entityTypeName, clrType); + _sharedTypeSets[entityTypeName] = set; + } + + return set; + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -246,6 +269,16 @@ public virtual DbSet Set() where TEntity : class => (DbSet)((IDbSetCache)this).GetOrAddSet(DbContextDependencies.SetSource, typeof(TEntity)); + /// + /// Creates a that can be used to query and save instances of . + /// + /// The type of entity for which a set should be returned. + /// The name of the entity type as defined by . + /// A set for the given entity type. + public virtual DbSet SharedTypeSet(string entityTypeName) + where TEntity : class + => (DbSet)((IDbSetCache)this).GetOrAddSharedTypeSet(DbContextDependencies.SetSource, entityTypeName, typeof(TEntity)); + /// /// Creates a that can be used to query instances of . /// @@ -628,6 +661,17 @@ var resettableServices } } + if (_sharedTypeSets != null) + { + foreach (var set in _sharedTypeSets.Values) + { + if (set is IResettableService resettable) + { + resettable.ResetState(); + } + } + } + if (_queries != null) { foreach (var query in _queries.Values) diff --git a/src/EFCore/DbSet`.cs b/src/EFCore/DbSet`.cs index 33309d3eb7d..390e1906999 100644 --- a/src/EFCore/DbSet`.cs +++ b/src/EFCore/DbSet`.cs @@ -35,7 +35,7 @@ namespace Microsoft.EntityFrameworkCore /// /// objects are usually obtained from a /// property on a derived or from the - /// method. + /// or methods. /// /// /// The type of entity being operated on by this set. diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index fcb488d8d5f..a98591d16f0 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -106,6 +106,7 @@ public class EntityFrameworkServicesBuilder { typeof(IKeyPropagator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(INavigationFixer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(ILocalViewListener), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(ISharedTypeEntityFinder), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IStateManager), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(Func), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IConcurrencyDetector), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -232,6 +233,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); TryAdd(); TryAdd(); TryAdd(); diff --git a/src/EFCore/Internal/DbSetSource.cs b/src/EFCore/Internal/DbSetSource.cs index d9012aceef1..c1dc7a2ef6a 100644 --- a/src/EFCore/Internal/DbSetSource.cs +++ b/src/EFCore/Internal/DbSetSource.cs @@ -17,41 +17,56 @@ public class DbSetSource : IDbSetSource, IDbQuerySource private static readonly MethodInfo _genericCreateSet = typeof(DbSetSource).GetTypeInfo().GetDeclaredMethod(nameof(CreateSetFactory)); + private static readonly MethodInfo _genericCreateSharedTypeSet + = typeof(DbSetSource).GetTypeInfo().GetDeclaredMethod(nameof(CreateSharedTypeSetFactory)); + private static readonly MethodInfo _genericCreateQuery = typeof(DbSetSource).GetTypeInfo().GetDeclaredMethod(nameof(CreateQueryFactory)); - private readonly ConcurrentDictionary> _cache - = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _cache + = new ConcurrentDictionary>(); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public virtual object Create(DbContext context, Type type) - => CreateCore(context, type, _genericCreateSet); + => CreateCore(context, type.DisplayName(), type, _genericCreateSet); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual object CreateSharedTypeSet(DbContext context, string entityTypeName, Type type) + => CreateCore(context, entityTypeName, type, _genericCreateSharedTypeSet); /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// public virtual object CreateQuery(DbContext context, Type type) - => CreateCore(context, type, _genericCreateQuery); + => CreateCore(context, type.DisplayName(), type, _genericCreateQuery); - private object CreateCore(DbContext context, Type type, MethodInfo createMethod) + private object CreateCore(DbContext context, string entityTypeName, Type type, MethodInfo createMethod) => _cache.GetOrAdd( - type, - t => (Func)createMethod - .MakeGenericMethod(t) - .Invoke(null, null))(context); + entityTypeName, + name => (Func)createMethod + .MakeGenericMethod(type) + .Invoke(null, null))(context, entityTypeName); + + [UsedImplicitly] + private static Func CreateSetFactory() + where TEntity : class + => (ctx, _) => new InternalDbSet(ctx); [UsedImplicitly] - private static Func CreateSetFactory() + private static Func CreateSharedTypeSetFactory() where TEntity : class - => c => new InternalDbSet(c); + => (ctx, entityTypeName) => new InternalSharedTypeDbSet(ctx, entityTypeName); [UsedImplicitly] - private static Func> CreateQueryFactory() + private static Func> CreateQueryFactory() where TQuery : class - => c => new InternalDbQuery(c); + => (ctx, _) => new InternalDbQuery(ctx); } } diff --git a/src/EFCore/Internal/EntityFinder.cs b/src/EFCore/Internal/EntityFinder.cs index b2c7dcef3b4..02951fba091 100644 --- a/src/EFCore/Internal/EntityFinder.cs +++ b/src/EFCore/Internal/EntityFinder.cs @@ -292,7 +292,9 @@ private IQueryable BuildQueryRoot(IEntityType entityType) { var definingEntityType = entityType.DefiningEntityType; return definingEntityType == null - ? (IQueryable)_setCache.GetOrAddSet(_setSource, entityType.ClrType) + ? entityType.IsSharedType + ? (IQueryable)_setCache.GetOrAddSharedTypeSet(_setSource, entityType.Name, entityType.ClrType) + : (IQueryable)_setCache.GetOrAddSet(_setSource, entityType.ClrType) : BuildQueryRoot(definingEntityType, entityType); } diff --git a/src/EFCore/Internal/IDbSetCache.cs b/src/EFCore/Internal/IDbSetCache.cs index da40030c7ef..60532d433c4 100644 --- a/src/EFCore/Internal/IDbSetCache.cs +++ b/src/EFCore/Internal/IDbSetCache.cs @@ -17,5 +17,11 @@ public interface IDbSetCache /// directly from your code. This API may change or be removed in future releases. /// object GetOrAddSet([NotNull] IDbSetSource source, [NotNull] Type type); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + object GetOrAddSharedTypeSet([NotNull] IDbSetSource source, [NotNull] string entityTypeName, [NotNull] Type type); } } diff --git a/src/EFCore/Internal/IDbSetSource.cs b/src/EFCore/Internal/IDbSetSource.cs index 593a9094b28..41846722fcd 100644 --- a/src/EFCore/Internal/IDbSetSource.cs +++ b/src/EFCore/Internal/IDbSetSource.cs @@ -17,5 +17,11 @@ public interface IDbSetSource /// directly from your code. This API may change or be removed in future releases. /// object Create([NotNull] DbContext context, [NotNull] Type type); + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + object CreateSharedTypeSet([NotNull] DbContext context, [NotNull] string entityTypeName, [NotNull] Type type); } } diff --git a/src/EFCore/Internal/InternalDbSet.cs b/src/EFCore/Internal/InternalDbSet.cs index be7aa49568e..3e2a6fa916b 100644 --- a/src/EFCore/Internal/InternalDbSet.cs +++ b/src/EFCore/Internal/InternalDbSet.cs @@ -25,8 +25,8 @@ public class InternalDbSet : DbSet, IQueryable, IAsyncEnumerableAccessor, IInfrastructure, IResettableService where TEntity : class { - private readonly DbContext _context; - private IEntityType _entityType; + protected readonly DbContext _context; + protected IEntityType _entityType; private EntityQueryable _entityQueryable; private LocalView _localView; @@ -43,7 +43,7 @@ public InternalDbSet([NotNull] DbContext context) _context = context; } - private IEntityType EntityType + protected virtual IEntityType EntityType { get { diff --git a/src/EFCore/Internal/InternalSharedTypeDbSet.cs b/src/EFCore/Internal/InternalSharedTypeDbSet.cs new file mode 100644 index 00000000000..11523bb3a2c --- /dev/null +++ b/src/EFCore/Internal/InternalSharedTypeDbSet.cs @@ -0,0 +1,53 @@ +// 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 JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class InternalSharedTypeDbSet : InternalDbSet + where TEntity : class + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public InternalSharedTypeDbSet([NotNull] DbContext context, [NotNull] string entityTypeName) + :base(context) + { + Check.NotNull(context, nameof(context)); + + EntityTypeName = entityTypeName; + } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual string EntityTypeName { get; } + + protected override IEntityType EntityType + { + get + { + if (_entityType == null) + { + _entityType = _context.Model.FindEntityType(EntityTypeName); + if (_entityType == null) + { + throw new InvalidOperationException(CoreStrings.InvalidSharedTypeSet(EntityTypeName)); + } + } + + return _entityType; + } + } + } +} diff --git a/src/EFCore/Metadata/IEntityType.cs b/src/EFCore/Metadata/IEntityType.cs index 6ff8ad7a65f..d2fb3a5f61b 100644 --- a/src/EFCore/Metadata/IEntityType.cs +++ b/src/EFCore/Metadata/IEntityType.cs @@ -43,6 +43,12 @@ public interface IEntityType : ITypeBase /// true if the entity type is a query type; otherwise false. bool IsQueryType { get; } + /// + /// Gets whether this entity type can share its ClrType with other entities. + /// + /// true if the entity type can share its ClrType with other entities; otherwise false. + bool IsSharedType { get; } + /// /// /// Gets primary key for this entity. Returns null if no primary key is defined. diff --git a/src/EFCore/Metadata/IMutableEntityType.cs b/src/EFCore/Metadata/IMutableEntityType.cs index 5fd7afaadda..3c27e3757e9 100644 --- a/src/EFCore/Metadata/IMutableEntityType.cs +++ b/src/EFCore/Metadata/IMutableEntityType.cs @@ -41,6 +41,12 @@ public interface IMutableEntityType : IEntityType, IMutableTypeBase /// true if the entity type is a query type; otherwise false. new bool IsQueryType { get; set; } + /// + /// Gets or sets whether this entity type can share its ClrType with other entities. + /// + /// true if the entity type can share its ClrType with other entities; otherwise false. + new bool IsSharedType { get; set; } + /// /// Gets the LINQ query used as the default source for queries of this type. /// diff --git a/src/EFCore/Metadata/IMutableModel.cs b/src/EFCore/Metadata/IMutableModel.cs index bbd8783d884..17b14415a2b 100644 --- a/src/EFCore/Metadata/IMutableModel.cs +++ b/src/EFCore/Metadata/IMutableModel.cs @@ -64,6 +64,16 @@ public interface IMutableModel : IModel, IMutableAnnotatable [NotNull] string definingNavigationName, [NotNull] IMutableEntityType definingEntityType); + /// + /// Adds an entity type to the model which has a CLR-type which can be shared with other entity types. + /// + /// The name of the entity to be added. + /// The CLR class that is used to represent instances of this entity type. + /// The new entity type. + IMutableEntityType AddSharedTypeEntityType( + [NotNull] string name, + [NotNull] Type type); + /// /// Gets the entity with the given name. Returns null if no entity type with the given name is found /// or the entity type has a defining navigation. diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index 3591a638f5a..37dd4c56e2f 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -120,6 +120,20 @@ public EntityType([NotNull] Type clrType, [NotNull] Model model, ConfigurationSo DefiningEntityType = definingEntityType; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public EntityType([NotNull] string name, + [NotNull] Type clrType, + [NotNull] Model model, + ConfigurationSource configurationSource) + : base(name, clrType, model, configurationSource) + { + _properties = new SortedDictionary(new PropertyComparer(this)); + Builder = new InternalEntityTypeBuilder(this, model.Builder); + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. @@ -174,6 +188,12 @@ public virtual LambdaExpression QueryFilter /// public virtual bool IsQueryType { get; set; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual bool IsSharedType { get; set; } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs index 702b59c80e9..c813acef0e8 100644 --- a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs +++ b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs @@ -33,7 +33,7 @@ public static class EntityTypeExtensions [DebuggerStepThrough] public static string ShortName([NotNull] this IEntityType type) { - if (type.ClrType != null) + if (!type.IsSharedType && type.ClrType != null) { return type.ClrType.ShortDisplayName(); } diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 278de5a08e6..2102e4e16c0 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -154,6 +154,27 @@ public virtual IEnumerable GetEntityTypes() return AddEntityType(queryType); } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual EntityType AddSharedTypeEntityType( + [NotNull] string name, + [NotNull] Type type, + // ReSharper disable once MethodOverloadWithOptionalParameter + ConfigurationSource configurationSource = ConfigurationSource.Explicit) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(type, nameof(type)); + + var entityType = new EntityType(name, type, this, configurationSource) + { + IsSharedType = true + }; + + return AddEntityType(entityType); + } + private EntityType AddEntityType(EntityType entityType) { var entityTypeName = entityType.Name; @@ -187,6 +208,20 @@ private EntityType AddEntityType(EntityType entityType) var added = entityTypesWithSameType.Add(entityType); Debug.Assert(added); } + else if (entityType.IsSharedType) + { + if (_entityTypesWithDefiningNavigation.ContainsKey(entityTypeName)) + { + throw new InvalidOperationException(CoreStrings.ClashingWeakEntityType(entityType.DisplayName())); + } + + if (_entityTypes.ContainsKey(entityTypeName)) + { + throw new InvalidOperationException(CoreStrings.ClashingSharedTypeEntityType(entityType.DisplayName())); + } + + _entityTypes.Add(entityTypeName, entityType); + } else { if (_entityTypesWithDefiningNavigation.ContainsKey(entityTypeName)) @@ -663,6 +698,7 @@ protected override Annotation OnAnnotationSet(string name, Annotation annotation IMutableEntityType IMutableModel.AddEntityType(string name) => AddEntityType(name); IMutableEntityType IMutableModel.AddEntityType(Type type) => AddEntityType(type); IMutableEntityType IMutableModel.AddQueryType(Type type) => AddQueryType(type); + IMutableEntityType IMutableModel.AddSharedTypeEntityType(string name, Type type) => AddSharedTypeEntityType(name, type); IMutableEntityType IMutableModel.RemoveEntityType(string name) => RemoveEntityType(name); IEntityType IModel.FindEntityType(string name, string definingNavigationName, IEntityType definingEntityType) diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index 894a0bec607..d35070320ba 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -62,9 +62,19 @@ private static PropertyAccessors CreateGeneric(IPropertyBase property Expression.Property(entryParameter, "Entity"), entityClrType); - currentValueExpression = Expression.MakeMemberAccess( - convertedExpression, - propertyBase.GetMemberInfo(forConstruction: false, forSet: false)); + if (propertyBase.IsIndexedProperty) + { + currentValueExpression = Expression.MakeIndex( + convertedExpression, + propertyBase.PropertyInfo, + new [] { Expression.Constant(propertyBase.Name) }); + } + else + { + currentValueExpression = Expression.MakeMemberAccess( + convertedExpression, + propertyBase.GetMemberInfo(forConstruction: false, forSet: false)); + } if (currentValueExpression.Type != typeof(TProperty)) { diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index a8fe7a79681..77305b182df 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -50,6 +50,22 @@ protected TypeBase([NotNull] Type clrType, [NotNull] Model model, ConfigurationS ClrType = clrType; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected TypeBase([NotNull] string name, [NotNull] Type clrType, + [NotNull] Model model, ConfigurationSource configurationSource) + : this(model, configurationSource) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(clrType, nameof(clrType)); + Check.NotNull(model, nameof(model)); + + Name = name; + ClrType = clrType; + } + private TypeBase([NotNull] Model model, ConfigurationSource configurationSource) { Model = model; diff --git a/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs b/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs index 1fe72b0d2b2..5b5fe1e706e 100644 --- a/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs +++ b/src/EFCore/Metadata/Internal/TypeBaseExtensions.cs @@ -48,11 +48,11 @@ public static bool IsAbstract([NotNull] this ITypeBase type) public static PropertyInfo EFIndexerProperty([NotNull] this ITypeBase type) { var runtimeProperties = type is TypeBase typeBase - ? typeBase.GetRuntimeProperties().Values // better perf if we've already computed them once + ? typeBase.GetRuntimeProperties()?.Values // better perf if we've already computed them once : type.ClrType.GetRuntimeProperties(); // find the indexer with single argument of type string which returns an object - return runtimeProperties.FirstOrDefault(p => p.IsEFIndexerProperty()); + return runtimeProperties?.FirstOrDefault(p => p.IsEFIndexerProperty()); } } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 68eed6f4b98..df0cd38cd83 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2828,6 +2828,22 @@ public static string ConflictingForeignKeyAttributes([CanBeNull] object property GetString("ConflictingForeignKeyAttributes", nameof(propertyList), nameof(entityType)), propertyList, entityType); + /// + /// The shared-type entity type '{entityType}' cannot be added to the model because an entity type with the same name already exists. + /// + public static string ClashingSharedTypeEntityType([CanBeNull] object entityType) + => string.Format( + GetString("ClashingSharedTypeEntityType", nameof(entityType)), + entityType); + + /// + /// Cannot create a shared-type DbSet with name '{entityTypeName}' because no entity type with that name has been included in the model for this context. Please define a shared-type entity type by calling 'modelBuilder.Model.AddSharedTypeEntityType()' in `OnModelCreating()`. + /// + public static string InvalidSharedTypeSet([CanBeNull] object entityTypeName) + => string.Format( + GetString("InvalidSharedTypeSet", nameof(entityTypeName)), + entityTypeName); + 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 9737cbc3503..82c64c08288 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1138,4 +1138,10 @@ There are multiple ForeignKeyAttributes which are pointing to same set of properties - '{propertyList}' on entity type '{entityType}'. + + The shared-type entity type '{entityType}' cannot be added to the model because an entity type with the same name already exists. + + + Cannot create a shared-type DbSet with name '{entityTypeName}' because no entity type with that name has been included in the model for this context. Please define a shared-type entity type by calling 'modelBuilder.Model.AddSharedTypeEntityType()' in `OnModelCreating()`. + \ No newline at end of file diff --git a/src/EFCore/breakingchanges.netcore.json b/src/EFCore/breakingchanges.netcore.json index 36ce4493a0b..c5cd0f1e3d4 100644 --- a/src/EFCore/breakingchanges.netcore.json +++ b/src/EFCore/breakingchanges.netcore.json @@ -16,5 +16,25 @@ "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IPropertyBase : Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable", "MemberId": "System.Boolean get_IsIndexedProperty()", "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IEntityType : Microsoft.EntityFrameworkCore.Metadata.ITypeBase", + "MemberId": "System.Boolean get_IsSharedType()", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType : Microsoft.EntityFrameworkCore.Metadata.IEntityType, Microsoft.EntityFrameworkCore.Metadata.IMutableTypeBase", + "MemberId": "System.Boolean get_IsSharedType()", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType : Microsoft.EntityFrameworkCore.Metadata.IEntityType, Microsoft.EntityFrameworkCore.Metadata.IMutableTypeBase", + "MemberId": "System.Void set_IsSharedType(System.Boolean value)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableModel : Microsoft.EntityFrameworkCore.Metadata.IModel, Microsoft.EntityFrameworkCore.Metadata.IMutableAnnotatable", + "MemberId": "Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType AddSharedTypeEntityType(System.String name, System.Type type)", + "Kind": "Addition" } ] diff --git a/test/EFCore.Tests/ChangeTracking/Internal/SelfDescribingIndexPropertyEntityFinderTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/SelfDescribingIndexPropertyEntityFinderTest.cs new file mode 100644 index 00000000000..c294907fbb3 --- /dev/null +++ b/test/EFCore.Tests/ChangeTracking/Internal/SelfDescribingIndexPropertyEntityFinderTest.cs @@ -0,0 +1,103 @@ +// 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.Collections.Generic; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal +{ + public class SelfDescribingIndexPropertyEntityFinderTest + { + [Fact] + public void Can_find_EntityType_using_default_EntityTypeNamePropertyName() + { + var model = new Model(); + var entityType = model.AddSharedTypeEntityType("TestDictionaryEntityType", typeof(Dictionary)); + var idProperty = entityType.AddIndexedProperty("Id", typeof(int)); + idProperty.IsNullable = false; + var propA = entityType.AddIndexedProperty("PropA", typeof(string)); + + var instance = new Dictionary() + { + { "__EntityTypeName__", "TestDictionaryEntityType" }, + { "Id", 0 }, + { "PropA", "PropAValue"}, + }; + + var finder = new SelfDescribingIndexPropertyEntityFinder(model); + + Assert.Equal(entityType, finder.FindSharedTypeEntityType(instance)); + } + + [Fact] + public void Can_find_EntityType_using_user_defined_EntityTypeNamePropertyName() + { + var model = new Model(); + var entityType = model.AddSharedTypeEntityType("TestDictionaryEntityType", typeof(Dictionary)); + var idProperty = entityType.AddIndexedProperty("Id", typeof(int)); + idProperty.IsNullable = false; + var propA = entityType.AddIndexedProperty("PropA", typeof(string)); + + var instance = new Dictionary() + { + { "SelfDescribingProperty", "TestDictionaryEntityType" }, + { "Id", 0 }, + { "PropA", "PropAValue"}, + }; + + var finder = new SelfDescribingIndexPropertyEntityFinder(model); + finder.EntityTypeNamePropertyName = "SelfDescribingProperty"; + + Assert.Equal(entityType, finder.FindSharedTypeEntityType(instance)); + } + + [Fact] + public void Return_null_if_no_matching_EntityType_found() + { + var model = new Model(); + var entityType = model.AddSharedTypeEntityType("DifferentlyNamedEntityType", typeof(Dictionary)); + var idProperty = entityType.AddIndexedProperty("Id", typeof(int)); + idProperty.IsNullable = false; + var propA = entityType.AddIndexedProperty("PropA", typeof(string)); + + var instance = new Dictionary() + { + { "__EntityTypeName__", "TestDictionaryEntityType" }, + { "Id", 0 }, + { "PropA", "PropAValue"}, + }; + + var finder = new SelfDescribingIndexPropertyEntityFinder(model); + + Assert.Null(finder.FindSharedTypeEntityType(instance)); + } + [Fact] + public void Return_null_if_find_non_shared_type_EntityType_with_same_name() + { + var model = new Model(); + var entityType = model.AddEntityType(typeof(NonSharedTypeEntity)); + var idProperty = entityType.AddProperty("Id", typeof(int)); + idProperty.IsNullable = false; + var propA = entityType.AddProperty("PropA", typeof(string)); + + var instance = new Dictionary() + { + { "__EntityTypeName__", entityType.Name }, + { "Id", 0 }, + { "PropA", "PropAValue"}, + }; + + var finder = new SelfDescribingIndexPropertyEntityFinder(model); + + Assert.Null(finder.FindSharedTypeEntityType(instance)); + } + + private class NonSharedTypeEntity + { + public int Id { get; set; } + public string PropA { get; set; } + } + } +} diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 8d336055e15..fdfd83258b6 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -862,7 +862,7 @@ public async Task It_throws_object_disposed_exception() await Assert.ThrowsAsync(() => context.FindAsync(typeof(Random), 77)); var methodCount = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Count(); - var expectedMethodCount = 41; + var expectedMethodCount = 42; Assert.True( methodCount == expectedMethodCount, userMessage: $"Expected {expectedMethodCount} methods on DbContext but found {methodCount}. " + diff --git a/test/EFCore.Tests/DbSetSourceTest.cs b/test/EFCore.Tests/DbSetSourceTest.cs index 9ed5034ee70..38ce21af622 100644 --- a/test/EFCore.Tests/DbSetSourceTest.cs +++ b/test/EFCore.Tests/DbSetSourceTest.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 Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -22,6 +23,19 @@ public void Can_create_new_generic_DbSet() Assert.IsType>(set); } + [Fact] + public void Can_create_new_generic_SharedTypeDbSet() + { + var context = InMemoryTestHelpers.Instance.CreateContext(); + + var factorySource = new DbSetSource(); + + var set = factorySource.CreateSharedTypeSet(context, "SharedTypeEntityType", typeof(Dictionary)); + + Assert.IsType>>(set); + } + + [Fact] public void Always_creates_a_new_DbSet_instance() { @@ -31,5 +45,17 @@ public void Always_creates_a_new_DbSet_instance() Assert.NotSame(factorySource.Create(context, typeof(Random)), factorySource.Create(context, typeof(Random))); } + + [Fact] + public void Always_creates_a_new_SharedTypeDbSet_instance() + { + var context = InMemoryTestHelpers.Instance.CreateContext(); + + var factorySource = new DbSetSource(); + + Assert.NotSame( + factorySource.CreateSharedTypeSet(context, "SharedTypeEntityType", typeof(Dictionary)), + factorySource.CreateSharedTypeSet(context, "SharedTypeEntityType", typeof(Dictionary))); + } } } diff --git a/test/EFCore.Tests/DbSetTest.cs b/test/EFCore.Tests/DbSetTest.cs index 7c800ab6c09..8c11a33cc41 100644 --- a/test/EFCore.Tests/DbSetTest.cs +++ b/test/EFCore.Tests/DbSetTest.cs @@ -114,6 +114,15 @@ public void Direct_use_of_Set_throws_if_context_disposed() Assert.Throws(() => context.Set()); } + [Fact] + public void Direct_use_of_SharedTypeSet_throws_if_context_disposed() + { + var context = new EarlyLearningCenter(); + context.Dispose(); + + Assert.Throws(() => context.SharedTypeSet>("SharedTypeEntityTypeName")); + } + [Fact] public void Direct_use_of_Query_throws_if_context_disposed() { diff --git a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs index f5251865cc4..738ac5d8602 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs @@ -102,6 +102,7 @@ private class FakeEntityType : IEntityType public LambdaExpression QueryFilter { get; } public LambdaExpression DefiningQuery { get; } public bool IsQueryType { get; } + public bool IsSharedType { get; } public IKey FindPrimaryKey() => throw new NotImplementedException(); public IKey FindKey(IReadOnlyList properties) => throw new NotImplementedException(); public IEnumerable GetKeys() => throw new NotImplementedException(); diff --git a/test/EFCore.Tests/Metadata/Internal/PropertyAccessorsFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/PropertyAccessorsFactoryTest.cs new file mode 100644 index 00000000000..b172242febb --- /dev/null +++ b/test/EFCore.Tests/Metadata/Internal/PropertyAccessorsFactoryTest.cs @@ -0,0 +1,88 @@ +// 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; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + public class PropertyAccessorsFactoryTest + { + [Fact] + public void Can_use_PropertyAccessorsFactory_on_indexed_property() + { + var model = new Model(); + var entityType = model.AddEntityType(typeof(IndexedClass)); + var id = entityType.AddProperty("Id", typeof(int)); + var propertyA = entityType.AddIndexedProperty("PropertyA", typeof(string)); + + 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", ((Func)propertyAccessors.ValueBufferGetter)(valueBuffer)); + } + + [Fact] + public void Can_use_PropertyAccessorsFactory_on_non_indexed_property() + { + var model = new Model(); + var entityType = model.AddEntityType(typeof(NonIndexedClass)); + var id = entityType.AddProperty("Id", typeof(int)); + var propA = entityType.AddProperty("PropA", typeof(string)); + + var contextServices = InMemoryTestHelpers.Instance.CreateContextServices(model); + var stateManager = contextServices.GetRequiredService(); + var factory = contextServices.GetRequiredService(); + + var entity = new NonIndexedClass(); + var entry = factory.Create(stateManager, entityType, entity); + + var propertyAccessors = new PropertyAccessorsFactory().Create(propA); + 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", ((Func)propertyAccessors.ValueBufferGetter)(valueBuffer)); + } + + private class IndexedClass + { + private Dictionary _internalValues = new Dictionary() + { + { "PropertyA", "ValueA" } + }; + + internal int Id { get; set; } + + public object this[string name] + { + get => _internalValues[name]; + } + } + + private class NonIndexedClass + { + internal int Id { get; set; } + public string PropA { get; set; } = "ValueA"; + } + } +}