Skip to content

Commit

Permalink
Update the entity type on check constraints when it's converted to a …
Browse files Browse the repository at this point in the history
…STET

Fixes #23399
  • Loading branch information
AndriySvyryd authored Sep 21, 2021
1 parent 7ffacdb commit 3697418
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-conventions">Model building conventions</see> for more information.
/// </remarks>
public class CheckConstraintConvention : IEntityTypeBaseTypeChangedConvention
public class CheckConstraintConvention : IEntityTypeBaseTypeChangedConvention, IEntityTypeAddedConvention
{
/// <summary>
/// Creates a new instance of <see cref="CheckConstraintConvention" />.
Expand All @@ -43,6 +43,47 @@ public CheckConstraintConvention(
/// </summary>
protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; }

/// <summary>
/// Called after an entity type is added to the model.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type. </param>
/// <param name="context"> Additional information associated with convention execution. </param>
public virtual void ProcessEntityTypeAdded(IConventionEntityTypeBuilder entityTypeBuilder, IConventionContext<IConventionEntityTypeBuilder> context)
{
var entityType = entityTypeBuilder.Metadata;
if (!entityType.HasSharedClrType)
{
return;
}

List<IConventionCheckConstraint>? constraintsToReattach = null;
foreach (var checkConstraint in entityType.GetCheckConstraints())
{
if (checkConstraint.EntityType == entityType)
{
continue;
}

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);
}
}
}

/// <summary>
/// Called after the base type of an entity type changes.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
19 changes: 19 additions & 0 deletions src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,23 @@ public static IEnumerable<IReadOnlyCheckConstraint> GetCheckConstraints(IReadOnl
return null;
}

/// <summary>
/// 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.
/// </summary>
public static void Attach(IConventionEntityType entityType, IConventionCheckConstraint detachedCheckConstraint)
{
var newCheckConstraint = new CheckConstraint(
(IMutableEntityType)entityType,
detachedCheckConstraint.ModelName,
detachedCheckConstraint.Sql,
detachedCheckConstraint.GetConfigurationSource());

Attach(detachedCheckConstraint, newCheckConstraint);
}

/// <summary>
/// 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
Expand All @@ -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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public class InternalCheckConstraintBuilder :
AnnotatableBuilder<CheckConstraint,
IConventionModelBuilder>,
AnnotatableBuilder<CheckConstraint, IConventionModelBuilder>,
IConventionCheckConstraintBuilder
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -813,37 +813,62 @@ public override void Can_configure_owned_type()
{
var modelBuilder = CreateModelBuilder();

var ownedBuilder = modelBuilder.Entity<Customer>().OwnsOne(c => c.Details)
.ToTable("CustomerDetails")
modelBuilder.Ignore<Customer>();
modelBuilder.Ignore<Product>();

var ownedBuilder = modelBuilder.Entity<OtherCustomer>().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<SpecialCustomer>().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]
Expand Down

0 comments on commit 3697418

Please sign in to comment.