diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index 95da9d7f6f8..59d906936d6 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -304,6 +304,11 @@ private IEntityFinder Finder(Type type) throw new InvalidOperationException(CoreStrings.InvalidSetTypeWeak(type.ShortDisplayName())); } + if (Model.IsShared(type)) + { + throw new InvalidOperationException(CoreStrings.InvalidSetSharedType(type.ShortDisplayName())); + } + throw new InvalidOperationException(CoreStrings.InvalidSetType(type.ShortDisplayName())); } diff --git a/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs b/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs index 3684ac12a6a..4784e0dcf2e 100644 --- a/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs @@ -116,7 +116,7 @@ public static IServiceCollection AddDbContext)null - : (p, b) => optionsAction.Invoke(b), contextLifetime, optionsLifetime); + : (p, b) => optionsAction(b), contextLifetime, optionsLifetime); /// /// @@ -672,7 +672,7 @@ public static IServiceCollection AddDbContextFactory( serviceCollection, optionsAction == null ? (Action)null - : (p, b) => optionsAction.Invoke(b), + : (p, b) => optionsAction(b), lifetime); /// diff --git a/src/EFCore/Extensions/ModelExtensions.cs b/src/EFCore/Extensions/ModelExtensions.cs index 27f39d3d9f6..63b454aba55 100644 --- a/src/EFCore/Extensions/ModelExtensions.cs +++ b/src/EFCore/Extensions/ModelExtensions.cs @@ -22,7 +22,8 @@ public static class ModelExtensions { /// /// Gets the entity that maps the given entity class. Returns if no entity type with - /// the given CLR type is found or the entity type has a defining navigation. + /// the given CLR type is found or the given CLR type is being used by shared type entity type + /// or the entity type has a defining navigation. /// /// The model to find the entity type in. /// The type to find the corresponding entity type for. @@ -34,6 +35,7 @@ public static IEntityType FindEntityType([NotNull] this IModel model, [NotNull] /// /// Gets the entity that maps the given entity class, where the class may be a proxy derived from the /// actual entity type. Returns if no entity type with the given CLR type is found + /// or the given CLR type is being used by shared type entity type /// or the entity type has a defining navigation. /// /// The model to find the entity type in. @@ -119,6 +121,16 @@ public static bool HasEntityTypeWithDefiningNavigation([NotNull] this IModel mod => Check.NotNull(model, nameof(model)).AsModel() .HasEntityTypeWithDefiningNavigation(Check.NotNull(name, nameof(name))); + /// + /// Gets whether the CLR type is used by shared type entities in the model. + /// + /// The model to find the entity type in. + /// The CLR type. + /// Whether the CLR type is used by shared type entities in the model. + [DebuggerStepThrough] + public static bool IsShared([NotNull] this IModel model, [NotNull] Type clrType) + => Check.NotNull(model, nameof(model)).AsModel().IsShared(Check.NotNull(clrType, nameof(clrType))); + /// /// Gets the default change tracking strategy being used for entities in the model. This strategy indicates how the /// context detects changes to properties for an instance of an entity type. diff --git a/src/EFCore/Extensions/MutableModelExtensions.cs b/src/EFCore/Extensions/MutableModelExtensions.cs index 9fd779ab82d..2f77272b42b 100644 --- a/src/EFCore/Extensions/MutableModelExtensions.cs +++ b/src/EFCore/Extensions/MutableModelExtensions.cs @@ -18,7 +18,9 @@ namespace Microsoft.EntityFrameworkCore public static class MutableModelExtensions { /// - /// Gets the entity that maps the given entity class. Returns if no entity type with the given name is found. + /// Gets the entity that maps the given entity class. Returns if no entity type with + /// the given CLR type is found or the given CLR type is being used by shared type entity type + /// or the entity type has a defining navigation. /// /// The model to find the entity type in. /// The type to find the corresponding entity type for. diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index bd469c1d482..aed28fa485a 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -207,6 +207,7 @@ var isTargetWeakOrOwned if (targetType?.IsValidEntityType() == true && (isTargetWeakOrOwned + || conventionModel.IsShared(targetType) || conventionModel.FindEntityType(targetType) != null || targetType.GetRuntimeProperties().Any(p => p.IsCandidateProperty()))) { diff --git a/src/EFCore/Internal/EntityFinder.cs b/src/EFCore/Internal/EntityFinder.cs index d81e4d2c277..d1c757a8f08 100644 --- a/src/EFCore/Internal/EntityFinder.cs +++ b/src/EFCore/Internal/EntityFinder.cs @@ -29,7 +29,7 @@ public class EntityFinder : IEntityFinder private readonly IStateManager _stateManager; private readonly IDbSetSource _setSource; private readonly IDbSetCache _setCache; - private readonly IModel _model; + private readonly IEntityType _entityType; private readonly IQueryable _queryRoot; /// @@ -47,7 +47,7 @@ public EntityFinder( _stateManager = stateManager; _setSource = setSource; _setCache = setCache; - _model = entityType.Model; + _entityType = entityType; _queryRoot = (IQueryable)BuildQueryRoot(entityType); } @@ -274,7 +274,7 @@ private static IReadOnlyList GetLoadProperties(INavigation navigation private TEntity FindTracked(object[] keyValues, out IReadOnlyList keyProperties) { - var key = _model.FindEntityType(typeof(TEntity)).FindPrimaryKey(); + var key = _entityType.FindPrimaryKey(); keyProperties = key.Properties; if (keyProperties.Count != keyValues.Length) diff --git a/src/EFCore/Internal/InternalDbSet.cs b/src/EFCore/Internal/InternalDbSet.cs index a8047c70350..aad2138367e 100644 --- a/src/EFCore/Internal/InternalDbSet.cs +++ b/src/EFCore/Internal/InternalDbSet.cs @@ -74,6 +74,11 @@ private IEntityType EntityType throw new InvalidOperationException(CoreStrings.InvalidSetTypeWeak(typeof(TEntity).ShortDisplayName())); } + if (_context.Model.IsShared(typeof(TEntity))) + { + throw new InvalidOperationException(CoreStrings.InvalidSetSharedType(typeof(TEntity).ShortDisplayName())); + } + throw new InvalidOperationException(CoreStrings.InvalidSetType(typeof(TEntity).ShortDisplayName())); } diff --git a/src/EFCore/Metadata/Conventions/BackingFieldConvention.cs b/src/EFCore/Metadata/Conventions/BackingFieldConvention.cs index 24d3a3df633..31ffe0372f0 100644 --- a/src/EFCore/Metadata/Conventions/BackingFieldConvention.cs +++ b/src/EFCore/Metadata/Conventions/BackingFieldConvention.cs @@ -87,10 +87,12 @@ private FieldInfo GetFieldToSet(IConventionPropertyBase propertyBase) return null; } - var type = propertyBase.DeclaringType.ClrType; + var entityType = (IConventionEntityType)propertyBase.DeclaringType; + var type = entityType.ClrType; + var baseTypes = entityType.GetAllBaseTypes().ToArray(); while (type != null) { - var fieldInfo = TryMatchFieldName(propertyBase, type); + var fieldInfo = TryMatchFieldName(propertyBase, entityType, type); if (fieldInfo != null && (propertyBase.PropertyInfo != null || propertyBase.Name == fieldInfo.GetSimpleMemberName())) { @@ -98,18 +100,18 @@ private FieldInfo GetFieldToSet(IConventionPropertyBase propertyBase) } type = type.BaseType; + entityType = baseTypes.FirstOrDefault(et => et.ClrType == type); + } return null; } - private static FieldInfo TryMatchFieldName(IConventionPropertyBase propertyBase, Type entityClrType) + private static FieldInfo TryMatchFieldName(IConventionPropertyBase propertyBase, IConventionEntityType entityType, Type entityClrType) { - var model = propertyBase.DeclaringType.Model; var propertyName = propertyBase.Name; IReadOnlyDictionary fields; - var entityType = model.FindEntityType(entityClrType); if (entityType == null) { var newFields = new Dictionary(StringComparer.Ordinal); diff --git a/src/EFCore/Metadata/Conventions/BaseTypeDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/BaseTypeDiscoveryConvention.cs index bf8d5b5502b..cf9fa1d13f9 100644 --- a/src/EFCore/Metadata/Conventions/BaseTypeDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/BaseTypeDiscoveryConvention.cs @@ -34,6 +34,7 @@ public virtual void ProcessEntityTypeAdded( var entityType = entityTypeBuilder.Metadata; var clrType = entityType.ClrType; if (clrType == null + || entityType.HasSharedClrType || entityType.HasDefiningNavigation() || entityType.FindDeclaredOwnership() != null || entityType.Model.FindIsOwnedConfigurationSource(clrType) != null) diff --git a/src/EFCore/Metadata/Conventions/DerivedTypeDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/DerivedTypeDiscoveryConvention.cs index c1ef72e9474..4418cc82cc7 100644 --- a/src/EFCore/Metadata/Conventions/DerivedTypeDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/DerivedTypeDiscoveryConvention.cs @@ -35,7 +35,10 @@ public virtual void ProcessEntityTypeAdded( var entityType = entityTypeBuilder.Metadata; var clrType = entityType.ClrType; if (clrType == null - || entityType.HasDefiningNavigation()) + || entityType.HasSharedClrType + || entityType.HasDefiningNavigation() + || entityType.FindDeclaredOwnership() != null + || entityType.Model.FindIsOwnedConfigurationSource(clrType) != null) { return; } diff --git a/src/EFCore/Metadata/IConventionModel.cs b/src/EFCore/Metadata/IConventionModel.cs index 7c7e66c82d2..faec2a3f689 100644 --- a/src/EFCore/Metadata/IConventionModel.cs +++ b/src/EFCore/Metadata/IConventionModel.cs @@ -50,7 +50,13 @@ public interface IConventionModel : IModel, IConventionAnnotatable IConventionEntityType AddEntityType([NotNull] Type clrType, bool fromDataAnnotation = false); /// - /// Adds an entity type to the model. + /// + /// Adds a shared type entity type to the model. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// /// /// The name of the entity to be added. /// The CLR class that is used to represent instances of the entity type. @@ -88,6 +94,7 @@ IConventionEntityType AddEntityType( /// /// Gets the entity with the given name. Returns if no entity type with the given name is found + /// or the given CLR type is being used by shared type entity type /// or the entity type has a defining navigation. /// /// The name of the entity type to find. @@ -136,10 +143,10 @@ IConventionEntityType FindEntityType( string RemoveIgnored([NotNull] string typeName); /// - /// Gets whether the CLR type is used by shared entities in the model. + /// Gets whether the CLR type is used by shared type entities in the model. /// /// The CLR type. - /// Whether the CLR type is used by shared entities in the model. + /// Whether the CLR type is used by shared type entities in the model. bool IsShared([NotNull] Type clrType); /// diff --git a/src/EFCore/Metadata/IModel.cs b/src/EFCore/Metadata/IModel.cs index 92b5e6c702c..44b97d36711 100644 --- a/src/EFCore/Metadata/IModel.cs +++ b/src/EFCore/Metadata/IModel.cs @@ -32,6 +32,7 @@ public interface IModel : IAnnotatable /// /// Gets the entity type with the given name. Returns null if no entity type with the given name is found + /// or the given CLR type is being used by shared type entity type /// or the entity type has a defining navigation. /// /// The name of the entity type to find. diff --git a/src/EFCore/Metadata/IMutableModel.cs b/src/EFCore/Metadata/IMutableModel.cs index 8c5a7672d44..5311a26d2e1 100644 --- a/src/EFCore/Metadata/IMutableModel.cs +++ b/src/EFCore/Metadata/IMutableModel.cs @@ -42,7 +42,13 @@ public interface IMutableModel : IModel, IMutableAnnotatable IMutableEntityType AddEntityType([NotNull] Type clrType); /// - /// Adds an entity type to the model. + /// + /// Adds a shared type entity type to the model. + /// + /// + /// Shared type entity type is an entity type which can share CLR type with other types in the model but has + /// a unique name and always identified by the name. + /// /// /// The name of the entity to be added. /// The CLR class that is used to represent instances of the entity type. @@ -75,6 +81,7 @@ IMutableEntityType AddEntityType( /// /// Gets the entity with the given name. Returns if no entity type with the given name is found + /// or the given CLR type is being used by shared type entity type /// or the entity type has a defining navigation. /// /// The name of the entity type to find. diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 961796ed6af..922e797674d 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -230,6 +230,11 @@ private EntityType AddEntityType(EntityType entityType) if (entityType.HasSharedClrType) { + if (_entityTypes.Any(et => !et.Value.HasSharedClrType && et.Value.ClrType == entityType.ClrType)) + { + throw new InvalidOperationException(CoreStrings.ClashingNonSharedType(entityType.DisplayName())); + } + _sharedEntityClrTypes.Add(entityType.ClrType); } else if (_sharedEntityClrTypes.Contains(entityType.ClrType)) @@ -249,15 +254,7 @@ private EntityType AddEntityType(EntityType entityType) /// 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 EntityType FindEntityType([NotNull] Type type) - { - if (_sharedEntityClrTypes.Contains(type)) - { - throw new InvalidOperationException(CoreStrings.CannotFindEntityWithClrTypeWhenShared(type.DisplayName())); - } - - return FindEntityType(GetDisplayName(type)); - } + public virtual EntityType FindEntityType([NotNull] Type type) => FindEntityType(GetDisplayName(type)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -714,6 +711,15 @@ private string AddIgnored( _ignoredTypeNames[name] = configurationSource; + if (type == null) + { + // This is to populate Type for convention when removing shared type entity type + type = _entityTypes.TryGetValue(name, out var existingEntityType) + && existingEntityType.HasSharedClrType + ? existingEntityType.ClrType + : null; + } + return ConventionDispatcher.OnEntityTypeIgnored(Builder, name, type); } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index d7116a813c3..827ed1689b6 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1885,7 +1885,7 @@ public static string OwnedDerivedType([CanBeNull] object entityType) entityType); /// - /// Cannot create a DbSet for '{typeName}' because it is mapped to multiple entity types and should they should be accessed through the defining entities. + /// Cannot create a DbSet for '{typeName}' because it is mapped to multiple entity types and should be accessed through the defining entities. /// public static string InvalidSetTypeWeak([CanBeNull] object typeName) => string.Format( @@ -2382,14 +2382,6 @@ public static string ClashingSharedType([CanBeNull] object entityType) GetString("ClashingSharedType", nameof(entityType)), entityType); - /// - /// Cannot find entity type with type '{clrType}' since model contains shared entity type(s) with same type. - /// - public static string CannotFindEntityWithClrTypeWhenShared([CanBeNull] object clrType) - => string.Format( - GetString("CannotFindEntityWithClrTypeWhenShared", nameof(clrType)), - clrType); - /// /// The skip navigation '{skipNavigation}' cannot be removed because it is set as the inverse of the skip navigation '{inverseSkipNavigation}' on '{referencingEntityType}'. All referencing skip navigations must be removed before this skip navigation can be removed. /// @@ -2716,6 +2708,30 @@ public static string PrincipalKeylessType([CanBeNull] object entityType, [CanBeN GetString("PrincipalKeylessType", nameof(entityType), nameof(firstNavigationSpecification), nameof(secondNavigationSpecification)), entityType, firstNavigationSpecification, secondNavigationSpecification); + /// + /// The shared type entity type '{entityType}' cannot be added to the model because a shared entity type with the same name but different clr type already exists. + /// + public static string ClashingMismatchedSharedType([CanBeNull] object entityType) + => string.Format( + GetString("ClashingMismatchedSharedType", nameof(entityType)), + entityType); + + /// + /// The shared type entity type '{entityType}' cannot be added to the model because a non shared entity type with the same clr type already exists. + /// + public static string ClashingNonSharedType([CanBeNull] object entityType) + => string.Format( + GetString("ClashingNonSharedType", nameof(entityType)), + entityType); + + /// + /// Cannot create a DbSet for '{typeName}' because it is configured as an shared type entity type and should be accessed through entity type name based Set method. + /// + public static string InvalidSetSharedType([CanBeNull] object typeName) + => string.Format( + GetString("InvalidSetSharedType", nameof(typeName)), + typeName); + /// /// Cannot use UsingEntity() passing type '{clrType}' because the model contains shared entity type(s) with same type. Use a type which uniquely defines an entity type. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index ff2259dfd4f..f4ce86df6a5 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1066,7 +1066,7 @@ The owned entity type '{entityType}' cannot have a base type. - Cannot create a DbSet for '{typeName}' because it is mapped to multiple entity types and should they should be accessed through the defining entities. + Cannot create a DbSet for '{typeName}' because it is mapped to multiple entity types and should be accessed through the defining entities. The navigation '{targetEntityType}.{inverseNavigation}' cannot be used as the inverse of '{weakEntityType}.{navigation}' because it's not the defining navigation '{definingNavigation}' @@ -1286,9 +1286,6 @@ The entity type '{entityType}' cannot be added to the model because a shared entity type with the same clr type already exists. - - Cannot find entity type with type '{clrType}' since model contains shared entity type(s) with same type. - The skip navigation '{skipNavigation}' cannot be removed because it is set as the inverse of the skip navigation '{inverseSkipNavigation}' on '{referencingEntityType}'. All referencing skip navigations must be removed before this skip navigation can be removed. @@ -1432,6 +1429,15 @@ The entity type '{entityType}' cannot be on the principal end of the relationship between '{firstNavigationSpecification}' and '{secondNavigationSpecification}'. The principal entity type must have a key. + + The shared type entity type '{entityType}' cannot be added to the model because a shared entity type with the same name but different clr type already exists. + + + The shared type entity type '{entityType}' cannot be added to the model because a non shared entity type with the same clr type already exists. + + + Cannot create a DbSet for '{typeName}' because it is configured as an shared type entity type and should be accessed through entity type name based Set method. + Cannot use UsingEntity() passing type '{clrType}' because the model contains shared entity type(s) with same type. Use a type which uniquely defines an entity type. diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index a716b7567e5..14ab2385596 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -172,7 +172,7 @@ protected override Expression VisitExtension(Expression extensionExpression) navigationExpansionExpression = CreateNavigationExpansionExpression(queryRootExpression, entityType); } - return ApplyQueryFilter(navigationExpansionExpression); + return ApplyQueryFilter(entityType, navigationExpansionExpression); case NavigationExpansionExpression _: case OwnedNavigationReference _: @@ -1353,12 +1353,11 @@ static LambdaExpression ChangeReturnType(LambdaExpression lambdaExpression, Type } } - private Expression ApplyQueryFilter(NavigationExpansionExpression navigationExpansionExpression) + private Expression ApplyQueryFilter(IEntityType entityType, NavigationExpansionExpression navigationExpansionExpression) { if (!_queryCompilationContext.IgnoreQueryFilters) { var sequenceType = navigationExpansionExpression.Type.GetSequenceType(); - var entityType = _queryCompilationContext.Model.FindEntityType(sequenceType); var rootEntityType = entityType.GetRootType(); var queryFilter = rootEntityType.GetQueryFilter(); if (queryFilter != null) diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 5ef70fc0f71..c9097dc1fab 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -113,6 +113,22 @@ public void Set_throws_for_weak_types() Assert.Equal(CoreStrings.InvalidSetTypeWeak(nameof(User)), ex.Message); } + [ConditionalFact] + public void Set_throws_for_shared_types() + { + var model = new Model(new ConventionSet()); + var question = model.AddEntityType("SharedQuestion", typeof(Question), ConfigurationSource.Explicit); + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .UseInternalServiceProvider(InMemoryTestHelpers.Instance.CreateServiceProvider()) + .UseModel(model.FinalizeModel()); + using var context = new DbContext(optionsBuilder.Options); + var ex = Assert.Throws(() => context.Set().Local); + Assert.Equal(CoreStrings.InvalidSetSharedType(typeof(Question).ShortDisplayName()), ex.Message); + } + [ConditionalFact] public void SaveChanges_calls_DetectChanges() { diff --git a/test/EFCore.Tests/Metadata/Internal/ModelTest.cs b/test/EFCore.Tests/Metadata/Internal/ModelTest.cs index a171a9aed29..18c72e387e8 100644 --- a/test/EFCore.Tests/Metadata/Internal/ModelTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ModelTest.cs @@ -123,10 +123,7 @@ public void Can_add_and_remove_shared_entity() Assert.NotNull(((EntityType)entityType).Builder); Assert.Same(entityType, model.FindEntityType(entityTypeName)); - Assert.Equal( - CoreStrings.CannotFindEntityWithClrTypeWhenShared(typeof(Customer).DisplayName()), - Assert.Throws( - () => model.FindEntityType(typeof(Customer))).Message); + Assert.Null(model.FindEntityType(typeof(Customer))); Assert.Equal(new[] { entityType }, model.GetEntityTypes().ToArray());