From 584855adb1f5a46eb26f19298c8d9a3193b74713 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Tue, 7 Jan 2020 15:49:52 -0800 Subject: [PATCH] Metadata: Add metadata support for shared type entity type Part of #9914 --- src/EFCore/Extensions/EntityTypeExtensions.cs | 4 +- src/EFCore/Metadata/IConventionModel.cs | 9 +++ src/EFCore/Metadata/IMutableModel.cs | 8 +++ src/EFCore/Metadata/ITypeBase.cs | 5 ++ src/EFCore/Metadata/Internal/EntityType.cs | 18 ++++++ src/EFCore/Metadata/Internal/Model.cs | 59 ++++++++++++++++++- src/EFCore/Metadata/Internal/TypeBase.cs | 28 +++++++++ src/EFCore/Properties/CoreStrings.Designer.cs | 16 +++++ src/EFCore/Properties/CoreStrings.resx | 6 ++ .../Metadata/Internal/EntityTypeTest.cs | 5 ++ .../Metadata/Internal/ModelTest.cs | 33 ++++++++++- 11 files changed, 186 insertions(+), 5 deletions(-) diff --git a/src/EFCore/Extensions/EntityTypeExtensions.cs b/src/EFCore/Extensions/EntityTypeExtensions.cs index 4fbadcd219c..aa6087ac2eb 100644 --- a/src/EFCore/Extensions/EntityTypeExtensions.cs +++ b/src/EFCore/Extensions/EntityTypeExtensions.cs @@ -292,7 +292,7 @@ public static IEnumerable GetDeclaredIndexes([NotNull] this IEntityType => entityType.AsEntityType().GetDeclaredIndexes(); private static string DisplayNameDefault(this ITypeBase type) - => type.ClrType != null + => type.ClrType != null && !type.IsSharedType ? type.ClrType.ShortDisplayName() : type.Name; @@ -345,7 +345,7 @@ public static string DisplayName([NotNull] this ITypeBase type) [DebuggerStepThrough] public static string ShortName([NotNull] this ITypeBase type) { - if (type.ClrType != null) + if (type.ClrType != null && !type.IsSharedType) { return type.ClrType.ShortDisplayName(); } diff --git a/src/EFCore/Metadata/IConventionModel.cs b/src/EFCore/Metadata/IConventionModel.cs index 558118313da..1b45ca9c0d0 100644 --- a/src/EFCore/Metadata/IConventionModel.cs +++ b/src/EFCore/Metadata/IConventionModel.cs @@ -49,6 +49,15 @@ public interface IConventionModel : IModel, IConventionAnnotatable /// The new entity type. IConventionEntityType AddEntityType([NotNull] Type clrType, bool fromDataAnnotation = false); + /// + /// Adds an entity type to the model. + /// + /// The name of the entity to be added. + /// The CLR class that is used to represent instances of the entity type. + /// Indicates whether the configuration was specified using a data annotation. + /// The new entity type. + IConventionEntityType AddEntityType([NotNull] string name, [NotNull] Type clrType, bool fromDataAnnotation = false); + /// /// Adds an entity type with a defining navigation to the model. /// diff --git a/src/EFCore/Metadata/IMutableModel.cs b/src/EFCore/Metadata/IMutableModel.cs index 272b0c9cf4a..642cf142ba8 100644 --- a/src/EFCore/Metadata/IMutableModel.cs +++ b/src/EFCore/Metadata/IMutableModel.cs @@ -41,6 +41,14 @@ public interface IMutableModel : IModel, IMutableAnnotatable /// The new entity type. IMutableEntityType AddEntityType([NotNull] Type clrType); + /// + /// Adds an entity type to the model. + /// + /// The name of the entity to be added. + /// The CLR class that is used to represent instances of the entity type. + /// The new entity type. + IMutableEntityType AddEntityType([NotNull] string name, [NotNull] Type clrType); + /// /// Adds an entity type with a defining navigation to the model. /// diff --git a/src/EFCore/Metadata/ITypeBase.cs b/src/EFCore/Metadata/ITypeBase.cs index 352e6c65b7a..48deb75bb53 100644 --- a/src/EFCore/Metadata/ITypeBase.cs +++ b/src/EFCore/Metadata/ITypeBase.cs @@ -32,5 +32,10 @@ public interface ITypeBase : IAnnotatable /// /// Type ClrType { get; } + + /// + /// Gets whether this entity type can share its ClrType with other entities. + /// + bool IsSharedType { get; } } } diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index e8b0392baa7..fdc4df8297f 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -98,6 +98,24 @@ public EntityType([NotNull] Type clrType, [NotNull] Model model, ConfigurationSo Builder = new InternalEntityTypeBuilder(this, model.Builder); } + /// + /// 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 EntityType([NotNull] string name, [NotNull] Type clrType, [NotNull] Model model, ConfigurationSource configurationSource) + : base(name, clrType, model, configurationSource) + { + if (!clrType.IsValidEntityType()) + { + throw new ArgumentException(CoreStrings.InvalidEntityType(clrType)); + } + + _properties = new SortedDictionary(new PropertyNameComparer(this)); + Builder = new InternalEntityTypeBuilder(this, model.Builder); + } + /// /// 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/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 551b81528dc..690babbd114 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -52,6 +52,8 @@ private readonly SortedDictionary> _entityTypesWit private readonly Dictionary _ignoredTypeNames = new Dictionary(StringComparer.Ordinal); + private readonly HashSet _sharedEntityClrTypes = new HashSet(); + /// /// 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 @@ -146,6 +148,25 @@ public virtual EntityType AddEntityType( return AddEntityType(entityType); } + /// + /// 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 EntityType AddEntityType( + [NotNull] string name, + [NotNull] Type type, + ConfigurationSource configurationSource) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(type, nameof(type)); + + var entityType = new EntityType(name, type, this, configurationSource); + + return AddEntityType(entityType); + } + private EntityType AddEntityType(EntityType entityType) { var entityTypeName = entityType.Name; @@ -193,6 +214,15 @@ private EntityType AddEntityType(EntityType entityType) throw new InvalidOperationException(CoreStrings.DuplicateEntityType(entityType.DisplayName())); } + if (entityType.IsSharedType) + { + _sharedEntityClrTypes.Add(entityType.ClrType); + } + else if (_sharedEntityClrTypes.Contains(entityType.ClrType)) + { + throw new InvalidOperationException(CoreStrings.ClashingSharedType(entityType.DisplayName())); + } + _entityTypes.Add(entityTypeName, entityType); } @@ -206,7 +236,14 @@ private EntityType AddEntityType(EntityType entityType) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual EntityType FindEntityType([NotNull] Type type) - => FindEntityType(GetDisplayName(type)); + { + if (_sharedEntityClrTypes.Contains(type)) + { + throw new InvalidOperationException(CoreStrings.CannotFindEntityWithClrTypeWhenShared(type.DisplayName())); + } + + return FindEntityType(GetDisplayName(type)); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -905,6 +942,14 @@ public virtual DebugView DebugView /// IMutableEntityType IMutableModel.AddEntityType(Type type) => AddEntityType(type, ConfigurationSource.Explicit); + /// + /// 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. + /// + IMutableEntityType IMutableModel.AddEntityType(string name, Type type) => AddEntityType(name, type, ConfigurationSource.Explicit); + /// /// 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 @@ -981,7 +1026,8 @@ void IMutableModel.AddIgnored(string name) /// IConventionModelBuilder IConventionModel.Builder { - [DebuggerStepThrough] get => Builder; + [DebuggerStepThrough] + get => Builder; } /// @@ -1020,6 +1066,15 @@ IConventionEntityType IConventionModel.AddEntityType(string name, bool fromDataA IConventionEntityType IConventionModel.AddEntityType(Type clrType, bool fromDataAnnotation) => AddEntityType(clrType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// + /// 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. + /// + IConventionEntityType IConventionModel.AddEntityType(string name, Type clrType, bool fromDataAnnotation) + => AddEntityType(name, clrType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + /// /// 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/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index 17c4484d8cd..29b20b00bc6 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -44,6 +44,7 @@ protected TypeBase([NotNull] string name, [NotNull] Model model, ConfigurationSo Check.NotNull(model, nameof(model)); Name = name; + IsSharedType = false; } /// @@ -59,6 +60,25 @@ protected TypeBase([NotNull] Type clrType, [NotNull] Model model, ConfigurationS Name = model.GetDisplayName(clrType); ClrType = clrType; + IsSharedType = false; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + 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; + IsSharedType = true; } private TypeBase([NotNull] Model model, ConfigurationSource configurationSource) @@ -91,6 +111,14 @@ private TypeBase([NotNull] Model model, ConfigurationSource configurationSource) /// public virtual string Name { [DebuggerStepThrough] get; } + /// + /// 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 bool IsSharedType { [DebuggerStepThrough] get; } + /// /// 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/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index c961338d586..daed773a0a1 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2348,6 +2348,22 @@ public static string BackingFieldOnIndexer([CanBeNull] object field, [CanBeNull] GetString("BackingFieldOnIndexer", nameof(field), nameof(entityType), nameof(property)), field, entityType, property); + /// + /// The entity type '{entityType}' cannot be added to the model because a shared entity type with the same clr type already exists. + /// + public static string ClashingSharedType([CanBeNull] object entityType) + => string.Format( + 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); + 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 9cb7a9e7390..051d9f66479 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1260,4 +1260,10 @@ Cannot set backing field '{field}' for the indexer property '{entityType}.{property}'. Indexer properties are not allowed to use a backing field. + + 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. + \ No newline at end of file diff --git a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs index 81c3758dc44..335731d47d2 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs @@ -64,6 +64,7 @@ private class FakeEntityType : IEntityType public IEnumerable GetAnnotations() => throw new NotImplementedException(); public IModel Model { get; } public string Name { get; } + public bool IsSharedType { get; } public Type ClrType { get; } public IEntityType BaseType { get; } public string DefiningNavigationName { get; } @@ -102,6 +103,10 @@ public void Display_name_is_entity_type_name_when_no_CLR_type() "Everything.Is+Awesome>", CreateModel().AddEntityType("Everything.Is+Awesome>").DisplayName()); + [ConditionalFact] + public void Display_name_is_entity_type_name_when_shared_entity_type() + => Assert.Equal("PostTag", CreateModel().AddEntityType("PostTag", typeof(Dictionary)).DisplayName()); + [ConditionalFact] public void Name_is_prettified_CLR_full_name() { diff --git a/test/EFCore.Tests/Metadata/Internal/ModelTest.cs b/test/EFCore.Tests/Metadata/Internal/ModelTest.cs index 3b48999ab4a..404dc9b70db 100644 --- a/test/EFCore.Tests/Metadata/Internal/ModelTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ModelTest.cs @@ -106,10 +106,41 @@ public void Can_add_and_remove_entity_by_name() Assert.Null(((EntityType)entityType).Builder); } + [ConditionalFact] + public void Can_add_and_remove_shared_entity() + { + var model = CreateModel(); + var entityTypeName = "SharedCustomer1"; + Assert.Null(model.FindEntityType(typeof(Customer))); + Assert.Null(model.FindEntityType(entityTypeName)); + + var entityType = model.AddEntityType(entityTypeName, typeof(Customer)); + + Assert.Equal(typeof(Customer), entityType.ClrType); + Assert.Equal(entityTypeName, entityType.Name); + Assert.NotNull(model.FindEntityType(entityTypeName)); + Assert.Same(model, entityType.Model); + 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.Equal(new[] { entityType }, model.GetEntityTypes().ToArray()); + + Assert.Same(entityType, model.RemoveEntityType(entityType.Name)); + + Assert.Null(model.RemoveEntityType(entityType.Name)); + Assert.Null(model.FindEntityType(entityTypeName)); + Assert.Null(((EntityType)entityType).Builder); + } + [ConditionalFact] public void Can_add_weak_entity_types() { - IMutableModel model = CreateModel(); + var model = CreateModel(); var customerType = model.AddEntityType(typeof(Customer)); var idProperty = customerType.AddProperty(Customer.IdProperty); var customerKey = customerType.AddKey(idProperty);