Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the entity type on check constraints when it's converted to a STET #26112

Merged
merged 2 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,50 @@ 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;
}

if (constraintsToReattach == null)
{
constraintsToReattach = new();
}
AndriySvyryd marked this conversation as resolved.
Show resolved Hide resolved

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