diff --git a/src/EFCore/Extensions/MutableModelExtensions.cs b/src/EFCore/Extensions/MutableModelExtensions.cs index 2f77272b42b..e9b85b64073 100644 --- a/src/EFCore/Extensions/MutableModelExtensions.cs +++ b/src/EFCore/Extensions/MutableModelExtensions.cs @@ -243,6 +243,16 @@ public static string RemoveOwned([NotNull] this IMutableModel model, [NotNull] T => Check.NotNull((Model)model, nameof(model)).RemoveOwned( Check.NotNull(clrType, nameof(clrType))); + /// + /// Marks the given entity type as shared, indicating that when discovered matching entity types + /// should be configured as shared type entity type. + /// + /// The model to add the owned type to. + /// The type of the entity type that should be owned. + public static void AddShared([NotNull] this IMutableModel model, [NotNull] Type clrType) + => Check.NotNull((Model)model, nameof(model)).AddShared( + Check.NotNull(clrType, nameof(clrType)), ConfigurationSource.Explicit); + /// /// Forces post-processing on the model such that it is ready for use by the runtime. This post /// processing happens automatically when using ; this method allows it to be run diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs index 7ccb3d02088..8e3ac1a2cff 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs @@ -376,7 +376,7 @@ public virtual EntityTypeBuilder OwnsOne( Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); + buildAction(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); return this; } @@ -412,7 +412,7 @@ public virtual EntityTypeBuilder OwnsOne( Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); + buildAction(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); return this; } @@ -523,7 +523,7 @@ public virtual EntityTypeBuilder OwnsMany( Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName)); + buildAction(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName)); return this; } @@ -559,7 +559,7 @@ public virtual EntityTypeBuilder OwnsMany( Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); + buildAction(OwnsManyBuilder(new TypeIdentity(ownedType, (Model)Metadata.Model), navigationName)); return this; } diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs index d5a00ae7610..e3339372c96 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs @@ -346,8 +346,7 @@ public virtual OwnedNavigationBuilder OwnsOne OwnsOne( [NotNull] Expression> navigationExpression) where TRelatedEntity : class - => OwnsOneBuilder( - new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); + => OwnsOneBuilder(new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); /// /// @@ -380,7 +379,7 @@ public virtual EntityTypeBuilder OwnsOne( Check.NotNull(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsOneBuilder(new MemberIdentity(navigationName))); + buildAction(OwnsOneBuilder(new MemberIdentity(navigationName))); return this; } @@ -416,7 +415,7 @@ public virtual EntityTypeBuilder OwnsOne( Check.NotNull(navigationExpression, nameof(navigationExpression)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); + buildAction(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); return this; } @@ -426,9 +425,8 @@ private OwnedNavigationBuilder OwnsOneBuilder OwnsMany OwnsMany( [NotNull] Expression>> navigationExpression) where TRelatedEntity : class - => OwnsManyBuilder( - new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); + => OwnsManyBuilder(new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); /// /// @@ -523,10 +520,10 @@ public virtual EntityTypeBuilder OwnsMany( [NotNull] Action> buildAction) where TRelatedEntity : class { - Check.NotNull(navigationName, nameof(navigationName)); + Check.NotEmpty(navigationName, nameof(navigationName)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsManyBuilder(new MemberIdentity(navigationName))); + buildAction(OwnsManyBuilder(new MemberIdentity(navigationName))); return this; } @@ -562,7 +559,7 @@ public virtual EntityTypeBuilder OwnsMany( Check.NotNull(navigationExpression, nameof(navigationExpression)); Check.NotNull(buildAction, nameof(buildAction)); - buildAction.Invoke(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); + buildAction(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); return this; } @@ -572,9 +569,8 @@ private OwnedNavigationBuilder OwnsManyBuilder IConventionEntityTypeBuilder Entity([NotNull] string name, bool? shouldBeOwned = false, bool fromDataAnnotation = false); + /// + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// 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 type to be configured. + /// The type of the entity type to be configured. + /// + /// if the entity type should be owned, + /// if the entity type should not be owned + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// An object that can be used to configure the entity type if the entity type was added or already part of the model, + /// otherwise. + /// + IConventionEntityTypeBuilder SharedEntity([NotNull] string name, [NotNull] Type type, bool? shouldBeOwned = false, bool fromDataAnnotation = false); + /// /// Returns an object that can be used to configure a given entity type in the model. /// If an entity type with the provided type is not already part of the model, diff --git a/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs new file mode 100644 index 00000000000..7674d56ac2a --- /dev/null +++ b/src/EFCore/Metadata/Builders/IConventionSharedEntityTypeBuilder.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// This interface is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + public interface IConventionSharedEntityTypeBuilder + { + } +} diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs index b35f18ae957..d56be9f980a 100644 --- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs @@ -397,7 +397,7 @@ public virtual OwnedNavigationBuilder OwnsOne( using (DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); + buildAction(OwnsOneBuilder(new TypeIdentity(ownedTypeName), navigationName)); return this; } } @@ -437,7 +437,7 @@ public virtual OwnedNavigationBuilder OwnsOne( using (DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - buildAction.Invoke(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)OwnedEntityType.Model), navigationName)); + buildAction(OwnsOneBuilder(new TypeIdentity(ownedType, (Model)OwnedEntityType.Model), navigationName)); return this; } } @@ -551,7 +551,7 @@ public virtual OwnedNavigationBuilder OwnsMany( using (DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName)); + buildAction(OwnsManyBuilder(new TypeIdentity(ownedTypeName), navigationName)); return this; } } @@ -590,7 +590,7 @@ public virtual OwnedNavigationBuilder OwnsMany( using (DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - buildAction.Invoke(OwnsManyBuilder(new TypeIdentity(ownedType, DependentEntityType.Model), navigationName)); + buildAction(OwnsManyBuilder(new TypeIdentity(ownedType, DependentEntityType.Model), navigationName)); return this; } } diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs index 33b4177954a..22d4f130972 100644 --- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs @@ -284,8 +284,7 @@ public virtual OwnedNavigationBuilder Own [NotNull] Expression> navigationExpression) where TNewDependentEntity : class => OwnsOneBuilder( - new MemberIdentity( - Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); + new MemberIdentity(Check.NotNull(navigationExpression, nameof(navigationExpression)).GetMemberAccess())); /// /// @@ -319,7 +318,7 @@ public virtual OwnedNavigationBuilder OwnsOne(new MemberIdentity(navigationName))); + buildAction(OwnsOneBuilder(new MemberIdentity(navigationName))); return this; } @@ -356,7 +355,7 @@ public virtual OwnedNavigationBuilder OwnsOne(new MemberIdentity(navigationExpression.GetMemberAccess()))); + buildAction(OwnsOneBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); return this; } @@ -367,10 +366,8 @@ private OwnedNavigationBuilder OwnsOneBui InternalForeignKeyBuilder relationship; using (var batch = DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - relationship = navigation.MemberInfo == null - ? DependentEntityType.Builder.HasOwnership(typeof(TNewDependentEntity), navigation.Name, ConfigurationSource.Explicit) - : DependentEntityType.Builder.HasOwnership( - typeof(TNewDependentEntity), (PropertyInfo)navigation.MemberInfo, ConfigurationSource.Explicit); + relationship = DependentEntityType.Builder.HasOwnership(typeof(TNewDependentEntity), navigation, ConfigurationSource.Explicit); + relationship.IsUnique(true, ConfigurationSource.Explicit); relationship = (InternalForeignKeyBuilder)batch.Run(relationship.Metadata).Builder; } @@ -470,7 +467,7 @@ public virtual OwnedNavigationBuilder OwnsMany(new MemberIdentity(navigationName))); + buildAction(OwnsManyBuilder(new MemberIdentity(navigationName))); return this; } } @@ -509,7 +506,7 @@ public virtual OwnedNavigationBuilder OwnsMany(new MemberIdentity(navigationExpression.GetMemberAccess()))); + buildAction(OwnsManyBuilder(new MemberIdentity(navigationExpression.GetMemberAccess()))); return this; } } @@ -520,10 +517,8 @@ private OwnedNavigationBuilder OwnsManyBuil InternalForeignKeyBuilder relationship; using (var batch = DependentEntityType.Model.ConventionDispatcher.DelayConventions()) { - relationship = navigation.MemberInfo == null - ? DependentEntityType.Builder.HasOwnership(typeof(TNewRelatedEntity), navigation.Name, ConfigurationSource.Explicit) - : DependentEntityType.Builder.HasOwnership( - typeof(TNewRelatedEntity), (PropertyInfo)navigation.MemberInfo, ConfigurationSource.Explicit); + relationship = DependentEntityType.Builder.HasOwnership(typeof(TNewRelatedEntity), navigation, ConfigurationSource.Explicit); + relationship.IsUnique(false, ConfigurationSource.Explicit); relationship = (InternalForeignKeyBuilder)batch.Run(relationship.Metadata).Builder; } diff --git a/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs new file mode 100644 index 00000000000..13fe5a83bbf --- /dev/null +++ b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder.cs @@ -0,0 +1,54 @@ +// 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.ComponentModel; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + public class SharedEntityTypeBuilder + { + /// + /// 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. + /// + [EntityFrameworkInternal] + public SharedEntityTypeBuilder() + { + } + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string ToString() => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + #endregion + } +} diff --git a/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs new file mode 100644 index 00000000000..8abb3b920a5 --- /dev/null +++ b/src/EFCore/Metadata/Builders/SharedEntityTypeBuilder`.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + /// The entity type being configured. + // ReSharper disable once UnusedTypeParameter + public class SharedEntityTypeBuilder : SharedEntityTypeBuilder + { + /// + /// 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. + /// + [EntityFrameworkInternal] + public SharedEntityTypeBuilder() + { + } + } +} diff --git a/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs b/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs index 02026872d57..6e766f8dd67 100644 --- a/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs +++ b/src/EFCore/Metadata/Conventions/ManyToManyAssociationEntityTypeConvention.cs @@ -106,12 +106,9 @@ private void CreateAssociationEntityType( inverseEntityType.ShortName()), otherIdentifiers, int.MaxValue); - //TODO #9914 - when the shared-type entity type version of model.Entity() is available call that instead - var associationEntityTypeBuilder = - model.AddEntityType( - associationEntityTypeName, - Model.DefaultPropertyBagType, - ConfigurationSource.Convention).Builder; + + var associationEntityTypeBuilder = model.Builder.SharedEntity( + associationEntityTypeName, Model.DefaultPropertyBagType, ConfigurationSource.Convention); // Create left and right foreign keys from the outer entity types to // the association entity type and configure the skip navigations. diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index 1882acc1b65..b3b3eb7493c 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -2952,8 +2952,7 @@ public virtual InternalForeignKeyBuilder HasOwnership( [NotNull] string navigationName, ConfigurationSource configurationSource) => HasOwnership( - new TypeIdentity(targetEntityTypeName), MemberIdentity.Create(navigationName), - inverse: null, configurationSource); + new TypeIdentity(targetEntityTypeName), MemberIdentity.Create(navigationName), inverse: null, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2966,8 +2965,7 @@ public virtual InternalForeignKeyBuilder HasOwnership( [NotNull] string navigationName, ConfigurationSource configurationSource) => HasOwnership( - new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationName), - inverse: null, configurationSource); + new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationName), inverse: null, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2980,8 +2978,20 @@ public virtual InternalForeignKeyBuilder HasOwnership( [NotNull] MemberInfo navigationMember, ConfigurationSource configurationSource) => HasOwnership( - new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationMember), - inverse: null, configurationSource); + new TypeIdentity(targetEntityType, Metadata.Model), MemberIdentity.Create(navigationMember), inverse: null, configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalForeignKeyBuilder HasOwnership( + [NotNull] Type targetEntityType, + MemberIdentity navigation, + ConfigurationSource configurationSource) + => HasOwnership( + new TypeIdentity(targetEntityType, Metadata.Model), navigation, inverse: null, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3033,6 +3043,7 @@ private InternalForeignKeyBuilder HasOwnership( if (existingNavigation.TargetEntityType.Name == targetEntityType.Name) { var existingOwnedEntityType = existingNavigation.ForeignKey.DeclaringEntityType; + // Upgrade configurationSource for existing entity type if (existingOwnedEntityType.HasDefiningNavigation()) { if (targetEntityType.Type != null) @@ -4244,7 +4255,8 @@ private bool CanAddDiscriminatorProperty( /// IConventionEntityType IConventionEntityTypeBuilder.Metadata { - [DebuggerStepThrough] get => Metadata; + [DebuggerStepThrough] + get => Metadata; } /// diff --git a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs index 95fbf275a8c..31b670443b8 100644 --- a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs @@ -48,7 +48,17 @@ public InternalModelBuilder([NotNull] Model metadata) /// public virtual InternalEntityTypeBuilder Entity( [NotNull] string name, ConfigurationSource configurationSource, bool? shouldBeOwned = false) - => Entity(new TypeIdentity(name), configurationSource, shouldBeOwned); + => Entity(new TypeIdentity(name), null, configurationSource, shouldBeOwned); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalEntityTypeBuilder SharedEntity( + [NotNull] string name, [NotNull] Type type, ConfigurationSource configurationSource, bool? shouldBeOwned = false) + => Entity(new TypeIdentity(name), Check.NotNull(type, nameof(type)), configurationSource, shouldBeOwned); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -58,10 +68,10 @@ public virtual InternalEntityTypeBuilder Entity( /// public virtual InternalEntityTypeBuilder Entity( [NotNull] Type type, ConfigurationSource configurationSource, bool? shouldBeOwned = false) - => Entity(new TypeIdentity(type, Metadata), configurationSource, shouldBeOwned); + => Entity(new TypeIdentity(type, Metadata), null, configurationSource, shouldBeOwned); private InternalEntityTypeBuilder Entity( - in TypeIdentity type, ConfigurationSource configurationSource, bool? shouldBeOwned) + in TypeIdentity type, Type sharedTypeClrType, ConfigurationSource configurationSource, bool? shouldBeOwned) { if (IsIgnored(type, configurationSource)) { @@ -69,14 +79,35 @@ private InternalEntityTypeBuilder Entity( } var clrType = type.Type; - var entityType = clrType == null - ? Metadata.FindEntityType(type.Name) - : Metadata.FindEntityType(clrType); + EntityType entityType; + if (clrType != null) + { + if (Metadata.IsShared(clrType)) + { + return configurationSource == ConfigurationSource.Explicit + ? throw new InvalidOperationException(CoreStrings.ClashingSharedType(clrType.DisplayName())) + : (InternalEntityTypeBuilder)null; + } + + entityType = Metadata.FindEntityType(clrType); + } + else + { + if (sharedTypeClrType != null && Metadata.FindEntityType(Metadata.GetDisplayName(sharedTypeClrType)) != null) + { + return configurationSource == ConfigurationSource.Explicit + ? throw new InvalidOperationException(CoreStrings.ClashingNonSharedType(type.Name)) + : (InternalEntityTypeBuilder)null; + } + + entityType = Metadata.FindEntityType(type.Name); + } if (shouldBeOwned == false - && (ShouldBeOwnedType(type) - || entityType != null && entityType.IsOwned())) + && (ShouldBeOwnedType(type) // Marked in model as owned + || entityType != null && entityType.IsOwned())) // Created using Owns* API { + // We always throw as configuring a type as owned is always comes from user (through Explicit/DataAnnotation) throw new InvalidOperationException( CoreStrings.ClashingOwnedEntityType( clrType == null ? type.Name : clrType.ShortDisplayName())); @@ -106,14 +137,24 @@ private InternalEntityTypeBuilder Entity( if (entityType != null) { + if (sharedTypeClrType != null) + { + if (entityType.ClrType != sharedTypeClrType) + { + throw new InvalidOperationException(CoreStrings.ClashingMismatchedSharedType(type.Name)); + } + } + entityType.UpdateConfigurationSource(configurationSource); return entityType.Builder; } Metadata.RemoveIgnored(type.Name); - entityType = clrType == null - ? Metadata.AddEntityType(type.Name, configurationSource) - : Metadata.AddEntityType(clrType, configurationSource); + entityType = clrType != null + ? Metadata.AddEntityType(clrType, configurationSource) + : sharedTypeClrType != null + ? Metadata.AddEntityType(type.Name, sharedTypeClrType, configurationSource) + : Metadata.AddEntityType(type.Name, configurationSource); return entityType?.Builder; } @@ -307,6 +348,33 @@ public virtual IConventionOwnedEntityTypeBuilder Owned( private bool ShouldBeOwnedType(in TypeIdentity type) => type.Type != null && Metadata.IsOwned(type.Type); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionSharedEntityTypeBuilder SharedEntity( + [NotNull] Type type, ConfigurationSource configurationSource) + { + if (IsIgnored(type, configurationSource)) + { + return null; + } + + Metadata.RemoveIgnored(type); + + foreach (var entityType in Metadata.GetEntityTypes() + .Where(et => !et.HasSharedClrType && et.ClrType == type && configurationSource.Overrides(et.GetConfigurationSource()))) + { + Metadata.RemoveEntityType(entityType); + } + + Metadata.AddShared(type); + + return new InternalSharedEntityTypeBuilder(); + } + /// /// 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 @@ -560,7 +628,8 @@ public virtual bool CanSetPropertyAccessMode( /// IConventionModel IConventionModelBuilder.Metadata { - [DebuggerStepThrough] get => Metadata; + [DebuggerStepThrough] + get => Metadata; } /// @@ -573,6 +642,16 @@ IConventionModel IConventionModelBuilder.Metadata IConventionEntityTypeBuilder IConventionModelBuilder.Entity(string name, bool? shouldBeOwned, bool fromDataAnnotation) => Entity(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, shouldBeOwned); + /// + /// 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. + /// + [DebuggerStepThrough] + IConventionEntityTypeBuilder IConventionModelBuilder.SharedEntity(string name, Type type, bool? shouldBeOwned, bool fromDataAnnotation) + => SharedEntity(name, type, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention, shouldBeOwned); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs new file mode 100644 index 00000000000..849619e4f46 --- /dev/null +++ b/src/EFCore/Metadata/Internal/InternalSharedEntityTypeBuilder.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// 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 class InternalSharedEntityTypeBuilder : IConventionSharedEntityTypeBuilder + { + } +} diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 922e797674d..94f2c17aabb 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -58,7 +58,7 @@ private readonly SortedDictionary> _entityTypesWit private readonly Dictionary _ignoredTypeNames = new Dictionary(StringComparer.Ordinal); - private readonly HashSet _sharedEntityClrTypes = new HashSet(); + private readonly Dictionary _sharedEntityClrTypes = new Dictionary(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -235,9 +235,16 @@ private EntityType AddEntityType(EntityType entityType) throw new InvalidOperationException(CoreStrings.ClashingNonSharedType(entityType.DisplayName())); } - _sharedEntityClrTypes.Add(entityType.ClrType); + if (_sharedEntityClrTypes.TryGetValue(entityType.ClrType, out var existingConfigurationSource)) + { + _sharedEntityClrTypes[entityType.ClrType] = entityType.GetConfigurationSource().Max(existingConfigurationSource); + } + else + { + _sharedEntityClrTypes.Add(entityType.ClrType, entityType.GetConfigurationSource()); + } } - else if (_sharedEntityClrTypes.Contains(entityType.ClrType)) + else if (_sharedEntityClrTypes.ContainsKey(entityType.ClrType)) { throw new InvalidOperationException(CoreStrings.ClashingSharedType(entityType.DisplayName())); } @@ -772,7 +779,7 @@ public virtual bool IsIgnored([NotNull] Type type) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool IsShared([NotNull] Type type) - => _sharedEntityClrTypes.Contains(type); + => _sharedEntityClrTypes.ContainsKey(type); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -877,6 +884,29 @@ public virtual string RemoveOwned([NotNull] Type clrType) return ownedTypes.Remove(name) ? name : 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 virtual void AddShared([NotNull] Type clrType, ConfigurationSource configurationSource) + { + if (_entityTypes.Any(et => !et.Value.HasSharedClrType && et.Value.ClrType == clrType)) + { + throw new InvalidOperationException(CoreStrings.CannotMarkShared(clrType.ShortDisplayName())); + } + + if (_sharedEntityClrTypes.TryGetValue(clrType, out var existingConfigurationSource)) + { + _sharedEntityClrTypes[clrType] = configurationSource.Max(existingConfigurationSource); + } + else + { + _sharedEntityClrTypes.Add(clrType, configurationSource); + } + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/ModelBuilder.cs b/src/EFCore/ModelBuilder.cs index 111e7e0be7a..4725a3c4246 100644 --- a/src/EFCore/ModelBuilder.cs +++ b/src/EFCore/ModelBuilder.cs @@ -115,6 +115,30 @@ public virtual EntityTypeBuilder Entity() where TEntity : class => new EntityTypeBuilder(Builder.Entity(typeof(TEntity), ConfigurationSource.Explicit).Metadata); + /// + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// 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 CLR type of the entity type to be configured. + /// The name of the entity type to be configured. + /// An object that can be used to configure the entity type. + public virtual EntityTypeBuilder SharedEntity([NotNull] string name) + where TEntity : class + { + Check.NotEmpty(name, nameof(name)); + + return new EntityTypeBuilder(Builder.SharedEntity(name, typeof(TEntity), ConfigurationSource.Explicit).Metadata); + } + /// /// Returns an object that can be used to configure a given entity type in the model. /// If the entity type is not already part of the model, it will be added to the model. @@ -142,6 +166,30 @@ public virtual EntityTypeBuilder Entity([NotNull] string name) return new EntityTypeBuilder(Builder.Entity(name, ConfigurationSource.Explicit).Metadata); } + /// + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// 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 type to be configured. + /// The CLR type of the entity type to be configured. + /// An object that can be used to configure the entity type. + public virtual EntityTypeBuilder SharedEntity([NotNull] string name, [NotNull] Type clrType) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(clrType, nameof(clrType)); + + return new EntityTypeBuilder(Builder.SharedEntity(name, clrType, ConfigurationSource.Explicit).Metadata); + } + /// /// /// Performs configuration of a given entity type in the model. If the entity type is not already part @@ -168,6 +216,41 @@ public virtual ModelBuilder Entity([NotNull] Action + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// 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. + /// + /// + /// This overload allows configuration of the entity type to be done inline in the method call rather + /// than being chained after a call to . This allows additional + /// configuration at the model level to be chained after configuration for the entity type. + /// + /// + /// The CLR type of the entity type to be configured. + /// The name of the entity type to be configured. + /// An action that performs configuration of the entity type. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelBuilder SharedEntity([NotNull] string name, [NotNull] Action> buildAction) + where TEntity : class + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(buildAction, nameof(buildAction)); + + buildAction(SharedEntity(name)); + + return this; + } + /// /// /// Performs configuration of a given entity type in the model. If the entity type is not already part @@ -221,11 +304,46 @@ public virtual ModelBuilder Entity([NotNull] string name, [NotNull] Action + /// + /// Returns an object that can be used to configure a given shared type entity type in the model. + /// + /// + /// If an entity type with the provided name is not already part of the model, a new entity type with provided CLR + /// type will be added to the model as shared type entity type. + /// + /// + /// 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. + /// + /// + /// This overload allows configuration of the entity type to be done in line in the method call rather + /// than being chained after a call to . This allows additional + /// configuration at the model level to be chained after configuration for the entity type. + /// + /// + /// The name of the entity type to be configured. + /// The CLR type of the entity type to be configured. + /// An action that performs configuration of the entity type. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelBuilder SharedEntity([NotNull] string name, [NotNull] Type clrType, [NotNull] Action buildAction) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(clrType, nameof(clrType)); + Check.NotNull(buildAction, nameof(buildAction)); + + buildAction(SharedEntity(name, clrType)); + + return this; + } + /// /// Excludes the given entity type from the model. This method is typically used to remove types from /// the model that were added by convention. /// - /// The entity type to be removed from the model. + /// The entity type to be removed from the model. /// /// The same instance so that additional configuration calls can be chained. /// @@ -234,7 +352,7 @@ public virtual ModelBuilder Ignore() => Ignore(typeof(TEntity)); /// - /// Excludes the given entity type from the model. This method is typically used to remove types from + /// Excludes an entity type with given CLR type from the model. This method is typically used to remove types from /// the model that were added by convention. /// /// The entity type to be removed from the model. @@ -250,6 +368,23 @@ public virtual ModelBuilder Ignore([NotNull] Type type) return this; } + /// + /// Excludes an entity type with the given name from the model. This method is typically used to remove types from + /// the model that were added by convention. + /// + /// The name of the entity type to be removed from the model. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelBuilder Ignore([NotNull] string name) + { + Check.NotEmpty(name, nameof(name)); + + Builder.Ignore(name, ConfigurationSource.Explicit); + + return this; + } + /// /// Applies configuration that is defined in an instance. /// @@ -342,6 +477,43 @@ public virtual OwnedEntityTypeBuilder Owned([NotNull] Type type) return new OwnedEntityTypeBuilder(); } + /// + /// + /// Marks an entity type as shared type. All references to this type will be configured as separate entity types. + /// + /// + /// 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 entity type to be configured. + public virtual SharedEntityTypeBuilder SharedEntity() + where T : class + { + Builder.SharedEntity(typeof(T), ConfigurationSource.Explicit); + + return new SharedEntityTypeBuilder(); + } + + /// + /// + /// Marks an entity type as shared type. All references to this type will be configured as separate entity types. + /// + /// + /// 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 entity type to be configured. + public virtual SharedEntityTypeBuilder SharedEntity([NotNull] Type type) + { + Check.NotNull(type, nameof(type)); + + Builder.SharedEntity(type, ConfigurationSource.Explicit); + + return new SharedEntityTypeBuilder(); + } + /// /// Configures the default to be used for this model. /// This strategy indicates how the context detects changes to properties for an instance of an entity type. diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 827ed1689b6..8bd026b8767 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2733,12 +2733,12 @@ public static string InvalidSetSharedType([CanBeNull] object 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. + /// Type '{type}' cannot be marked as shared type since entity type with same CLR type exists in the model. /// - public static string DoNotUseUsingEntityOnSharedClrType([CanBeNull] object clrType) + public static string CannotMarkShared([CanBeNull] object type) => string.Format( - GetString("DoNotUseUsingEntityOnSharedClrType", nameof(clrType)), - clrType); + GetString("CannotMarkShared", nameof(type)), + type); private static string GetString(string name, params string[] formatterNames) { diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index f4ce86df6a5..f1eaf7f6971 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -1438,7 +1438,7 @@ 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. + + Type '{type}' cannot be marked as shared type since entity type with same CLR type exists in the model. \ No newline at end of file diff --git a/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs index 1faa3027bdd..eb444c1c398 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalModelBuilderTest.cs @@ -477,6 +477,34 @@ public void Cannot_remove_manually_created_association_entity_type(bool removeSk Assert.Same(manyToManyJoin.Metadata, leftSkipNav.AssociationEntityType); } + [ConditionalFact] + public void Can_add_shared_type() + { + var model = new Model(); + var modelBuilder = CreateModelBuilder(model); + + var entityBuilder = modelBuilder.Entity(typeof(Customer), ConfigurationSource.Explicit); + var sharedTypeName = "SpecialDetails"; + + Assert.NotNull(modelBuilder.SharedEntity(sharedTypeName, typeof(Details), ConfigurationSource.Convention)); + + Assert.True(model.FindEntityType(sharedTypeName).HasSharedClrType); + + Assert.Equal( + CoreStrings.ClashingMismatchedSharedType("SpecialDetails"), + Assert.Throws(() => modelBuilder.SharedEntity(sharedTypeName, typeof(Product), ConfigurationSource.DataAnnotation)).Message); + + Assert.NotNull(modelBuilder.Entity(typeof(Product), ConfigurationSource.DataAnnotation)); + + Assert.Null(modelBuilder.SharedEntity(typeof(Product).DisplayName(), typeof(Product), ConfigurationSource.DataAnnotation)); + + Assert.NotNull(modelBuilder.Entity(typeof(Product), ConfigurationSource.Explicit)); + + Assert.Equal( + CoreStrings.ClashingNonSharedType(typeof(Product).DisplayName()), + Assert.Throws(() => modelBuilder.SharedEntity(typeof(Product).DisplayName(), typeof(Product), ConfigurationSource.Explicit)).Message); + } + private static void Cleanup(InternalModelBuilder modelBuilder) { new ModelCleanupConvention(CreateDependencies()) diff --git a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs index ad4c4b67d1b..788c2d2f72c 100644 --- a/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ManyToManyTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; @@ -356,6 +357,71 @@ public virtual void Navigation_properties_can_set_access_mode_using_navigation_n Assert.Equal(PropertyAccessMode.Field, principal.FindSkipNavigation("Dependents").GetPropertyAccessMode()); Assert.Equal(PropertyAccessMode.Property, dependent.FindSkipNavigation("ManyToManyPrincipals").GetPropertyAccessMode()); } + + [ConditionalFact] + public virtual void Can_use_shared_Type_as_join_entity() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity>( + "Shared1", + e => e.HasOne().WithMany(), + e => e.HasOne().WithMany()); + + modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity>( + "Shared2", + e => e.HasOne().WithMany(), + e => e.HasOne().WithMany(), + e => e.IndexerProperty("Payload")); + + var shared1 = modelBuilder.Model.FindEntityType("Shared1"); + Assert.NotNull(shared1); + Assert.Equal(2, shared1.GetForeignKeys().Count()); + Assert.True(shared1.HasSharedClrType); + Assert.Equal(typeof(Dictionary), shared1.ClrType); + + var shared2 = modelBuilder.Model.FindEntityType("Shared2"); + Assert.NotNull(shared2); + Assert.Equal(2, shared2.GetForeignKeys().Count()); + Assert.True(shared2.HasSharedClrType); + Assert.Equal(typeof(Dictionary), shared2.ClrType); + Assert.NotNull(shared2.FindProperty("Payload")); + + Assert.Equal( + CoreStrings.ClashingSharedType(typeof(Dictionary).DisplayName()), + Assert.Throws(() => modelBuilder.Entity>()).Message); + } + + [ConditionalFact] + public virtual void Cannot_add_shared_type_when_non_shared_exists() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity(); + + Assert.Equal( + CoreStrings.ClashingNonSharedType("Shared"), + Assert.Throws( + () => modelBuilder.Entity() + .HasMany(e => e.Dependents) + .WithMany(e => e.ManyToManyPrincipals) + .UsingEntity( + "Shared", + r => r.HasOne().WithMany(), + l => l.HasOne().WithMany())).Message); + } + + } } } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs index a26a57a3d29..2c06d56ae6c 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipStringTest.cs @@ -48,6 +48,9 @@ public GenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericStringTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name)); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -56,6 +59,15 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericStringTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs index d3b08524a5b..7748e546236 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericRelationshipTypeTest.cs @@ -29,6 +29,9 @@ public GenericTypeTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericTypeTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericTypeTestEntityTypeBuilder(ModelBuilder.SharedEntity(name)); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -37,6 +40,15 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericTypeTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index 67be90429d6..fc3e31d4343 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -127,6 +127,9 @@ public GenericTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new GenericTestEntityTypeBuilder(ModelBuilder.Entity()); + public override TestEntityTypeBuilder SharedEntity(string name) + => new GenericTestEntityTypeBuilder(ModelBuilder.SharedEntity(name )); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -135,6 +138,15 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + entityTypeBuilder => + buildAction(new GenericTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new GenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned()); @@ -652,7 +664,35 @@ public override TestEntityTypeBuilder UsingEntity ((GenericTestReferenceCollectionBuilder)configureLeft( new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, + l => ((GenericTestReferenceCollectionBuilder)configureRight( + new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((GenericTestReferenceCollectionBuilder)configureLeft( + new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + + public override TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class + => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + l => ((GenericTestReferenceCollectionBuilder)configureRight( + new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((GenericTestReferenceCollectionBuilder)configureLeft( + new GenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder, + e => configureAssociation(new GenericTestEntityTypeBuilder(e)))); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, @@ -660,6 +700,7 @@ public override TestEntityTypeBuilder UsingEntity> configureAssociation) where TAssociationEntity : class => new GenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, l => ((GenericTestReferenceCollectionBuilder)configureRight( new GenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, r => ((GenericTestReferenceCollectionBuilder)configureLeft( diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs index 4c491d10c51..d861fb921bd 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericStringTest.cs @@ -81,6 +81,9 @@ public NonGenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -89,6 +92,15 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), + e => buildAction(new NonGenericStringTestEntityTypeBuilder(e))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index c01283d2765..a1fb696af1b 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -75,6 +75,9 @@ public NonGenericTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -83,6 +86,15 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), entityTypeBuilder => + buildAction(new NonGenericTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); @@ -646,7 +658,37 @@ public override TestEntityTypeBuilder UsingEntity ((NonGenericTestReferenceCollectionBuilder)configureLeft( new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, + typeof(TAssociationEntity), + l => ((NonGenericTestReferenceCollectionBuilder)configureRight( + new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((NonGenericTestReferenceCollectionBuilder)configureLeft( + new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder)); + + public override TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class + => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + typeof(TAssociationEntity), + l => ((NonGenericTestReferenceCollectionBuilder)configureRight( + new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, + r => ((NonGenericTestReferenceCollectionBuilder)configureLeft( + new NonGenericTestEntityTypeBuilder(r))).ReferenceCollectionBuilder, + e => configureAssociation(new NonGenericTestEntityTypeBuilder(e)))); + public override TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, @@ -654,6 +696,7 @@ public override TestEntityTypeBuilder UsingEntity> configureAssociation) where TAssociationEntity : class => new NonGenericTestEntityTypeBuilder(CollectionCollectionBuilder.UsingEntity( + joinEntityName, typeof(TAssociationEntity), l => ((NonGenericTestReferenceCollectionBuilder)configureRight( new NonGenericTestEntityTypeBuilder(l))).ReferenceCollectionBuilder, diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs index bba78ade724..fff8f1e42b1 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericUnqualifiedStringTest.cs @@ -30,6 +30,9 @@ public NonGenericStringTestModelBuilder(TestHelpers testHelpers) public override TestEntityTypeBuilder Entity() => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.Entity(typeof(TEntity))); + public override TestEntityTypeBuilder SharedEntity(string name) + => new NonGenericStringTestEntityTypeBuilder(ModelBuilder.SharedEntity(name, typeof(TEntity))); + public override TestModelBuilder Entity(Action> buildAction) { ModelBuilder.Entity( @@ -38,6 +41,15 @@ public override TestModelBuilder Entity(Action(string name, Action> buildAction) + { + ModelBuilder.SharedEntity( + name, + typeof(TEntity), entityTypeBuilder => + buildAction(new NonGenericStringTestEntityTypeBuilder(entityTypeBuilder))); + return this; + } + public override TestOwnedEntityTypeBuilder Owned() => new NonGenericTestOwnedEntityTypeBuilder(ModelBuilder.Owned(typeof(TEntity))); diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 6784adb0b5b..e40dc847419 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -150,12 +150,18 @@ public TestModelBuilder HasAnnotation(string annotation, object value) public abstract TestEntityTypeBuilder Entity() where TEntity : class; + public abstract TestEntityTypeBuilder SharedEntity(string name) + where TEntity : class; + public abstract TestOwnedEntityTypeBuilder Owned() where TEntity : class; public abstract TestModelBuilder Entity(Action> buildAction) where TEntity : class; + public abstract TestModelBuilder SharedEntity(string name, Action> buildAction) + where TEntity : class; + public abstract TestModelBuilder Ignore() where TEntity : class; @@ -446,7 +452,24 @@ public abstract TestEntityTypeBuilder UsingEntity> configureLeft) where TAssociationEntity : class; + public abstract TestEntityTypeBuilder UsingEntity( + string joinEntityName, + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft) + where TAssociationEntity : class; + + public abstract TestEntityTypeBuilder UsingEntity( + Func, + TestReferenceCollectionBuilder> configureRight, + Func, + TestReferenceCollectionBuilder> configureLeft, + Action> configureAssociation) + where TAssociationEntity : class; + public abstract TestEntityTypeBuilder UsingEntity( + string joinEntityName, Func, TestReferenceCollectionBuilder> configureRight, Func, diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index d80328bb7d8..707ffbe6316 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -1540,6 +1540,44 @@ public virtual void Can_add_seed_data_anonymous_objects_indexed_property_diction Assert.Equal(2, data["Required"]); Assert.False(data.ContainsKey("Optional")); } + + [ConditionalFact] + public virtual void Can_add_shared_type_entity_type() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.SharedEntity>("Shared1"); + + modelBuilder.SharedEntity>("Shared2", b => b.IndexerProperty("Id")); + + var model = modelBuilder.Model; + Assert.Equal(2, model.GetEntityTypes().Count()); + + var shared1 = modelBuilder.Model.FindEntityType("Shared1"); + Assert.NotNull(shared1); + Assert.True(shared1.HasSharedClrType); + Assert.Null(shared1.FindProperty("Id")); + + var shared2 = modelBuilder.Model.FindEntityType("Shared2"); + Assert.NotNull(shared2); + Assert.True(shared2.HasSharedClrType); + Assert.NotNull(shared2.FindProperty("Id")); + + Assert.Equal( + CoreStrings.ClashingSharedType(typeof(Dictionary).DisplayName()), + Assert.Throws(() => modelBuilder.Entity>()).Message); + } + + [ConditionalFact] + public virtual void Cannot_add_shared_type_when_non_shared_exists() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity(); + + Assert.Equal( + CoreStrings.ClashingNonSharedType("Shared1"), + Assert.Throws(() => modelBuilder.SharedEntity("Shared1")).Message); + } } } } diff --git a/test/EFCore.Tests/ModelBuilding/TestModel.cs b/test/EFCore.Tests/ModelBuilding/TestModel.cs index a30f95f0d6d..ec74f9c8180 100644 --- a/test/EFCore.Tests/ModelBuilding/TestModel.cs +++ b/test/EFCore.Tests/ModelBuilding/TestModel.cs @@ -144,7 +144,6 @@ protected class Order : INotifyPropertyChanged public OrderCombination OrderCombination { get; set; } public OrderDetails Details { get; set; } public ICollection Products { get; set; } - public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { @@ -930,7 +929,7 @@ private class OwnedOneToManyNavDependent public OneToManyNavPrincipalOwner OneToManyOwner { get; set; } } - public class OwnerOfOwnees + protected class OwnerOfOwnees { public string Id { get; private set; } @@ -938,22 +937,22 @@ public class OwnerOfOwnees public Ownee1 Ownee1 { get; private set; } } - public class Ownee1 + protected class Ownee1 { public Ownee3 NewOwnee3 { get; private set; } } - public class Ownee2 + protected class Ownee2 { public Ownee3 Ownee3 { get; private set; } } - public class Ownee3 + protected class Ownee3 { public string Name { get; private set; } } - public class OneToManyPrincipalWithField + protected class OneToManyPrincipalWithField { public int Id; public Guid AlternateKey; @@ -962,7 +961,7 @@ public class OneToManyPrincipalWithField public IEnumerable Dependents; } - public class OneToOnePrincipalWithField + protected class OneToOnePrincipalWithField { public int Id; public string Name; @@ -970,7 +969,7 @@ public class OneToOnePrincipalWithField public DependentWithField Dependent; } - public class ManyToManyPrincipalWithField + protected class ManyToManyPrincipalWithField { public int Id; public string Name; @@ -987,7 +986,7 @@ protected class ManyToManyJoinWithFields public DependentWithField DependentWithField { get; set; } } - public class DependentWithField + protected class DependentWithField { public int DependentWithFieldId; @@ -999,7 +998,7 @@ public class DependentWithField public List ManyToManyPrincipals { get; set; } } - public class OneToManyOwnerWithField + protected class OneToManyOwnerWithField { public int Id; public Guid AlternateKey; @@ -1008,7 +1007,7 @@ public class OneToManyOwnerWithField public List OwnedDependents { get; set; } } - public class OneToManyOwnedWithField + protected class OneToManyOwnedWithField { public string FirstName; public string LastName; @@ -1017,7 +1016,7 @@ public class OneToManyOwnedWithField public OneToManyOwnerWithField OneToManyOwner { get; set; } } - public class OneToOneOwnerWithField + protected class OneToOneOwnerWithField { public int Id; public Guid AlternateKey; @@ -1026,7 +1025,7 @@ public class OneToOneOwnerWithField public OneToOneOwnedWithField OwnedDependent { get; set; } } - public class OneToOneOwnedWithField + protected class OneToOneOwnedWithField { public string FirstName; public string LastName; @@ -1036,7 +1035,7 @@ public class OneToOneOwnedWithField } - public class ImplicitManyToManyA + protected class ImplicitManyToManyA { public int Id { get; set; } public string Name { get; set; } @@ -1045,12 +1044,55 @@ public class ImplicitManyToManyA } - public class ImplicitManyToManyB + protected class ImplicitManyToManyB { public int Id { get; set; } public string Name { get; set; } public List As { get; set; } } + + protected class SharedHolderAlpha + { + public int Id { get; set; } + [NotMapped] + public SharedTypeEntityType SharedReference { get; set; } + [NotMapped] + public List SharedCollection { get; set; } + } + + protected class SharedHolderBeta + { + public int Id { get; set; } + [NotMapped] + public SharedTypeEntityType SharedReference { get; set; } + [NotMapped] + public List SharedCollection { get; set; } + } + + protected class SharedTypeEntityType + { + [NotMapped] + public int Random { get; set; } + [NotMapped] + public SharedNestedOwnedEntityType NestedReference { get; set; } + [NotMapped] + public List NestedCollection { get; set; } + [NotMapped] + public NestedReference ReferenceNavigation { get; set; } + } + + protected class SharedNestedOwnedEntityType + { + [NotMapped] + public int NestedRandom { get; set; } + } + + protected class NestedReference + { + public int Id { get; set; } + [NotMapped] + public string Value { get; set; } + } } }