diff --git a/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs b/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs index d7f4451e502..9279af77a42 100644 --- a/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs @@ -13,12 +13,12 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions { /// /// A convention that ensures that the check constraints on the derived types are compatible with - /// the check constraints on the base type. + /// the check constraints on the base type. And also ensures that the declaring type is current. /// /// /// See Model building conventions for more information. /// - public class CheckConstraintConvention : IEntityTypeBaseTypeChangedConvention + public class CheckConstraintConvention : IEntityTypeBaseTypeChangedConvention, IEntityTypeAddedConvention { /// /// Creates a new instance of . @@ -43,6 +43,50 @@ public CheckConstraintConvention( /// protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + /// + /// Called after an entity type is added to the model. + /// + /// The builder for the entity type. + /// Additional information associated with convention execution. + public virtual void ProcessEntityTypeAdded(IConventionEntityTypeBuilder entityTypeBuilder, IConventionContext context) + { + var entityType = entityTypeBuilder.Metadata; + if (!entityType.HasSharedClrType) + { + return; + } + + List? constraintsToReattach = null; + foreach (var checkConstraint in entityType.GetCheckConstraints()) + { + if (checkConstraint.EntityType == entityType) + { + continue; + } + + if (constraintsToReattach == null) + { + constraintsToReattach = new(); + } + + constraintsToReattach.Add(checkConstraint); + } + + if (constraintsToReattach == null) + { + return; + } + + foreach (var checkConstraint in constraintsToReattach) + { + var removedCheckConstraint = entityType.RemoveCheckConstraint(checkConstraint.ModelName); + if (removedCheckConstraint != null) + { + CheckConstraint.Attach(entityType, removedCheckConstraint); + } + } + } + /// /// Called after the base type of an entity type changes. /// diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 59764963ed2..7e2a71208e5 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -67,19 +67,23 @@ public override ConventionSet CreateConventionSet() conventionSet.PropertyAddedConventions.Add(relationalColumnAttributeConvention); conventionSet.PropertyAddedConventions.Add(relationalCommentAttributeConvention); + var checkConstraintConvention = new CheckConstraintConvention(Dependencies, RelationalDependencies); var tableNameFromDbSetConvention = new TableNameFromDbSetConvention(Dependencies, RelationalDependencies); conventionSet.EntityTypeAddedConventions.Add(new RelationalTableAttributeConvention(Dependencies, RelationalDependencies)); conventionSet.EntityTypeAddedConventions.Add( new RelationalTableCommentAttributeConvention(Dependencies, RelationalDependencies)); conventionSet.EntityTypeAddedConventions.Add(tableNameFromDbSetConvention); + conventionSet.EntityTypeAddedConventions.Add(checkConstraintConvention); ValueGenerationConvention valueGenerationConvention = new RelationalValueGenerationConvention(Dependencies, RelationalDependencies); ReplaceConvention(conventionSet.EntityTypeBaseTypeChangedConventions, valueGenerationConvention); + conventionSet.EntityTypeBaseTypeChangedConventions.Add(tableNameFromDbSetConvention); + conventionSet.EntityTypeBaseTypeChangedConventions.Add(checkConstraintConvention); + ReplaceConvention(conventionSet.ForeignKeyPropertiesChangedConventions, valueGenerationConvention); + ReplaceConvention(conventionSet.ForeignKeyOwnershipChangedConventions, valueGenerationConvention); - conventionSet.EntityTypeBaseTypeChangedConventions.Add(tableNameFromDbSetConvention); - conventionSet.EntityTypeBaseTypeChangedConventions.Add(new CheckConstraintConvention(Dependencies, RelationalDependencies)); conventionSet.EntityTypeAnnotationChangedConventions.Add((RelationalValueGenerationConvention)valueGenerationConvention); diff --git a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs index 765d6606b71..8b35e3c63bb 100644 --- a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs @@ -166,6 +166,23 @@ public static IEnumerable GetCheckConstraints(IReadOnl return null; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static void Attach(IConventionEntityType entityType, IConventionCheckConstraint detachedCheckConstraint) + { + var newCheckConstraint = new CheckConstraint( + (IMutableEntityType)entityType, + detachedCheckConstraint.ModelName, + detachedCheckConstraint.Sql, + detachedCheckConstraint.GetConfigurationSource()); + + Attach(detachedCheckConstraint, newCheckConstraint); + } + /// /// 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 @@ -180,6 +197,8 @@ public static void Attach(IConventionCheckConstraint detachedCheckConstraint, IC ((InternalCheckConstraintBuilder)existingCheckConstraint.Builder).HasName( detachedCheckConstraint.Name, nameConfigurationSource.Value); } + + ((InternalCheckConstraintBuilder)existingCheckConstraint.Builder).MergeAnnotationsFrom((CheckConstraint)detachedCheckConstraint); } /// diff --git a/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs index 292af98d429..9bb12353ea7 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs @@ -16,8 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public class InternalCheckConstraintBuilder : - AnnotatableBuilder, + AnnotatableBuilder, IConventionCheckConstraintBuilder { /// diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs index 947dd045a2a..2d7812e620d 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs @@ -813,37 +813,62 @@ public override void Can_configure_owned_type() { var modelBuilder = CreateModelBuilder(); - var ownedBuilder = modelBuilder.Entity().OwnsOne(c => c.Details) - .ToTable("CustomerDetails") + modelBuilder.Ignore(); + modelBuilder.Ignore(); + + var ownedBuilder = modelBuilder.Entity().OwnsOne(c => c.Details) + .ToTable("OtherCustomerDetails") .HasCheckConstraint("CK_CustomerDetails_T", "AlternateKey <> 0", c => c.HasName("CK_Guid")); ownedBuilder.Property(d => d.CustomerId); ownedBuilder.HasIndex(d => d.CustomerId); - ownedBuilder.WithOwner(d => d.Customer) + ownedBuilder.WithOwner(d => (OtherCustomer)d.Customer) .HasPrincipalKey(c => c.AlternateKey); + modelBuilder.Entity().OwnsOne(c => c.Details, b => + { + b.ToTable("SpecialCustomerDetails"); + b.HasCheckConstraint("CK_CustomerDetails_T", "AlternateKey <> 0", c => c.HasName("CK_Guid")); + b.Property(d => d.CustomerId); + b.HasIndex(d => d.CustomerId); + b.WithOwner(d => (SpecialCustomer)d.Customer) + .HasPrincipalKey(c => c.AlternateKey); + }); + var model = modelBuilder.FinalizeModel(); - var owner = model.FindEntityType(typeof(Customer)); - Assert.Equal(typeof(Customer).FullName, owner.Name); - var ownership = owner.FindNavigation(nameof(Customer.Details)).ForeignKey; - Assert.True(ownership.IsOwnership); - Assert.Equal(nameof(Customer.Details), ownership.PrincipalToDependent.Name); - Assert.Equal("CustomerAlternateKey", ownership.Properties.Single().Name); - Assert.Equal(nameof(Customer.AlternateKey), ownership.PrincipalKey.Properties.Single().Name); - var owned = ownership.DeclaringEntityType; - Assert.Same(ownedBuilder.OwnedEntityType, owned); - Assert.Equal("CustomerDetails", owned.GetTableName()); - var checkConstraint = owned.GetCheckConstraints().Single(); - Assert.Equal("CK_CustomerDetails_T", checkConstraint.ModelName); - Assert.Equal("AlternateKey <> 0", checkConstraint.Sql); - Assert.Equal("CK_Guid", checkConstraint.Name); - Assert.Single(owned.GetForeignKeys()); - Assert.Equal(nameof(CustomerDetails.CustomerId), owned.GetIndexes().Single().Properties.Single().Name); - Assert.Equal( - new[] { "CustomerAlternateKey", nameof(CustomerDetails.CustomerId), nameof(CustomerDetails.Id) }, - owned.GetProperties().Select(p => p.Name)); - Assert.NotNull(model.FindEntityType(typeof(CustomerDetails))); - Assert.Equal(1, model.GetEntityTypes().Count(e => e.ClrType == typeof(CustomerDetails))); + var owner1 = model.FindEntityType(typeof(OtherCustomer)); + Assert.Equal(typeof(OtherCustomer).FullName, owner1.Name); + AssertOwnership(owner1); + + var owner2 = model.FindEntityType(typeof(SpecialCustomer)); + Assert.Equal(typeof(SpecialCustomer).FullName, owner2.Name); + AssertOwnership(owner2); + + Assert.Null(model.FindEntityType(typeof(CustomerDetails))); + Assert.Equal(2, model.GetEntityTypes().Count(e => e.ClrType == typeof(CustomerDetails))); + + static void AssertOwnership(IEntityType owner) + { + var ownership1 = owner.FindNavigation(nameof(Customer.Details)).ForeignKey; + Assert.True(ownership1.IsOwnership); + Assert.Equal(nameof(Customer.Details), ownership1.PrincipalToDependent.Name); + Assert.Equal("CustomerAlternateKey", ownership1.Properties.Single().Name); + Assert.Equal(nameof(Customer.AlternateKey), ownership1.PrincipalKey.Properties.Single().Name); + var owned = ownership1.DeclaringEntityType; + Assert.Equal(owner.ShortName() + "Details", owned.GetTableName()); + var checkConstraint = owned.GetCheckConstraints().Single(); + Assert.Same(owned, checkConstraint.EntityType); + Assert.Equal("CK_CustomerDetails_T", checkConstraint.ModelName); + Assert.Equal("AlternateKey <> 0", checkConstraint.Sql); + Assert.Equal("CK_Guid", checkConstraint.Name); + Assert.Single(owned.GetForeignKeys()); + var index = owned.GetIndexes().Single(); + Assert.Same(owned, index.DeclaringEntityType); + Assert.Equal(nameof(CustomerDetails.CustomerId), index.Properties.Single().Name); + Assert.Equal( + new[] { "CustomerAlternateKey", nameof(CustomerDetails.CustomerId), nameof(CustomerDetails.Id) }, + owned.GetProperties().Select(p => p.Name)); + } } [ConditionalFact]