Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions src/EFCore/EFCore.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -4487,6 +4487,12 @@
{
"Member": "static string OwnerlessOwnedType(object? ownedType);"
},
{
"Member": "static string OwnershipNotCascadeDelete(object? principalEntityType, object? dependentEntityType, object? deleteBehavior, object? cascadeBehavior);"
},
{
"Member": "static string OwnershipNotRequired(object? principalEntityType, object? dependentEntityType);"
},
{
"Member": "static string OwnershipToDependent(object? navigation, object? principalEntityType, object? dependentEntityType);"
},
Expand Down
18 changes: 18 additions & 0 deletions src/EFCore/Infrastructure/ModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,24 @@ protected virtual void ValidateOwnership(
throw new InvalidOperationException(CoreStrings.OwnedDerivedType(entityType.DisplayName()));
}

if (ownership.DeleteBehavior != DeleteBehavior.Cascade)
{
throw new InvalidOperationException(
Comment thread
AndriySvyryd marked this conversation as resolved.
Comment thread
AndriySvyryd marked this conversation as resolved.
CoreStrings.OwnershipNotCascadeDelete(
ownership.PrincipalEntityType.DisplayName(),
ownership.DeclaringEntityType.DisplayName(),
ownership.DeleteBehavior,
DeleteBehavior.Cascade));
}

if (!ownership.IsRequired)
{
throw new InvalidOperationException(
CoreStrings.OwnershipNotRequired(
ownership.PrincipalEntityType.DisplayName(),
ownership.DeclaringEntityType.DisplayName()));
}

foreach (var referencingFk in entityType.GetReferencingForeignKeys().Where(fk => !fk.IsOwnership
&& (fk.PrincipalEntityType != fk.DeclaringEntityType
|| !fk.Properties.SequenceEqual(entityType.FindPrimaryKey()!.Properties))
Expand Down
16 changes: 16 additions & 0 deletions src/EFCore/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/EFCore/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,12 @@
<data name="OwnedEntitiesCannotBeTrackedWithoutTheirOwner" xml:space="preserve">
<value>A tracking query is attempting to project an owned entity without a corresponding owner in its result, but owned entities cannot be tracked without their owner. Either include the owner entity in the result or make the query non-tracking using 'AsNoTracking'.</value>
</data>
<data name="OwnershipNotCascadeDelete" xml:space="preserve">
<value>The ownership relationship from '{principalEntityType}' to '{dependentEntityType}' is configured with '{deleteBehavior}' delete behavior. Ownership relationships must use '{cascadeBehavior}' delete behavior. Either remove the explicit delete behavior configuration or don't configure this relationship as an ownership.</value>
</data>
<data name="OwnershipNotRequired" xml:space="preserve">
<value>The ownership relationship from '{principalEntityType}' to '{dependentEntityType}' is configured as optional. Ownership relationships must be required. Either remove the optional configuration or don't configure this relationship as an ownership.</value>
</data>
<data name="OwnerlessOwnedType" xml:space="preserve">
<value>The entity type '{ownedType}' has been marked as owned and must be referenced from another entity type via a navigation. Add a navigation to an entity type that points at '{ownedType}' or don't configure it as owned.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,25 +369,11 @@ public virtual async Task Delete_principal_with_shadow_key_owned_collection_thro
context.Attach(owner);
context.Remove(owner);

if (Fixture.ForceClientNoAction)
{
if (async)
{
await Assert.ThrowsAsync<DbUpdateException>(async () => await context.SaveChangesAsync());
}
else
{
Assert.Throws<DbUpdateException>(() => context.SaveChanges());
}
}
else
{
Assert.Equal(
CoreStrings.UnknownShadowKeyValue("Owner.OwnedCollection#Owned", "Id"),
(async
? await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.SaveChangesAsync())
: Assert.Throws<InvalidOperationException>(() => context.SaveChanges())).Message);
}
Assert.Equal(
CoreStrings.UnknownShadowKeyValue("Owner.OwnedCollection#Owned", "Id"),
(async
? await Assert.ThrowsAsync<InvalidOperationException>(async () => await context.SaveChangesAsync())
: Assert.Throws<InvalidOperationException>(() => context.SaveChanges())).Message);
});

[Theory, InlineData(false, false, false), InlineData(false, false, true), InlineData(false, true, false),
Expand Down Expand Up @@ -453,30 +439,17 @@ public virtual async Task Saving_unknown_key_value_marks_it_as_unmodified(bool a
owner.Owned.Remove(owner.Owned.Single());
owner.Owned.Add(new NonCompositeOwnedCollection { Foo = "Rome" });

if (Fixture.ForceClientNoAction)
{
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
_ = async
? await context.SaveChangesAsync()
: context.SaveChanges());
}
else
{
_ = async
? await context.SaveChangesAsync()
: context.SaveChanges();
}
_ = async
? await context.SaveChangesAsync()
: context.SaveChanges();
},
async context =>
{
if (!Fixture.ForceClientNoAction)
{
var owner = async
? await context.Set<OwnerWithNonCompositeOwnedCollection>().SingleAsync()
: context.Set<OwnerWithNonCompositeOwnedCollection>().Single();
var owner = async
? await context.Set<OwnerWithNonCompositeOwnedCollection>().SingleAsync()
: context.Set<OwnerWithNonCompositeOwnedCollection>().Single();

Assert.Equal("Rome", owner.Owned.Single().Foo);
}
Assert.Equal("Rome", owner.Owned.Single().Foo);
});

[Theory, InlineData(false), InlineData(true)] // Issue #19856
Expand Down Expand Up @@ -563,39 +536,19 @@ public virtual async Task Delete_principal_with_CLR_key_owned_collection(bool as
context.Attach(owner);
context.Remove(owner);

if (Fixture.ForceClientNoAction)
if (async)
{
if (async)
{
await Assert.ThrowsAsync<DbUpdateException>(async () => await context.SaveChangesAsync());
}
else
{
Assert.Throws<DbUpdateException>(() => context.SaveChanges());
}
await context.SaveChangesAsync();
}
else
{
if (async)
{
await context.SaveChangesAsync();
}
else
{
context.SaveChanges();
}
context.SaveChanges();
}
},
async context =>
{
if (!Fixture.ForceClientNoAction)
{
Assert.False(
async
? await context.Set<OwnerWithKeyedCollection>().AnyAsync()
: context.Set<OwnerWithKeyedCollection>().Any());
}
});
async context => Assert.False(
async
? await context.Set<OwnerWithKeyedCollection>().AnyAsync()
: context.Set<OwnerWithKeyedCollection>().Any()));

[Theory, InlineData(false, false, false), InlineData(false, false, true), InlineData(false, true, false),
InlineData(false, true, true), InlineData(true, false, false), InlineData(true, false, true), InlineData(true, true, false),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
foreach (var foreignKey in modelBuilder.Model
.GetEntityTypes()
.SelectMany(e => e.GetDeclaredForeignKeys())
.Where(e => e.DeleteBehavior == DeleteBehavior.Cascade))
.Where(e => e is { IsOwnership: false, DeleteBehavior: DeleteBehavior.Cascade }))
{
foreignKey.DeleteBehavior = DeleteBehavior.ClientCascade;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con

foreach (var foreignKey in modelBuilder.Model
.GetEntityTypes()
.SelectMany(e => e.GetDeclaredForeignKeys()))
.SelectMany(e => e.GetDeclaredForeignKeys())
.Where(e => !e.IsOwnership))
Comment thread
AndriySvyryd marked this conversation as resolved.
{
foreignKey.DeleteBehavior = DeleteBehavior.ClientNoAction;
}
Expand Down
55 changes: 55 additions & 0 deletions test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,61 @@ public virtual void Detects_entity_type_with_multiple_ownerships()
builder);
}

[Fact]
public virtual void Detects_ownership_with_non_cascade_delete_behavior()
{
var builder = CreateConventionlessModelBuilder();
var modelBuilder = (InternalModelBuilder)builder.GetInfrastructure();
var entityTypeBuilder = modelBuilder.Entity(typeof(SampleEntity), ConfigurationSource.Convention);
entityTypeBuilder.PrimaryKey([nameof(SampleEntity.Id)], ConfigurationSource.Convention);
entityTypeBuilder.Ignore(nameof(SampleEntity.Name), ConfigurationSource.Explicit);
entityTypeBuilder.Ignore(nameof(SampleEntity.Number), ConfigurationSource.Explicit);
entityTypeBuilder.Ignore(nameof(SampleEntity.OtherSamples), ConfigurationSource.Explicit);
entityTypeBuilder.Ignore(nameof(SampleEntity.AnotherReferencedEntity), ConfigurationSource.Explicit);

var ownershipBuilder = entityTypeBuilder.HasOwnership(
typeof(ReferencedEntity), nameof(SampleEntity.ReferencedEntity), ConfigurationSource.Convention);

var ownedTypeBuilder = ownershipBuilder.Metadata.DeclaringEntityType.Builder;
ownedTypeBuilder.PrimaryKey(ownershipBuilder.Metadata.Properties.Select(p => p.Name).ToList(), ConfigurationSource.Convention);
ownedTypeBuilder.Ignore(nameof(ReferencedEntity.Id), ConfigurationSource.Explicit);
ownedTypeBuilder.Ignore(nameof(ReferencedEntity.SampleEntityId), ConfigurationSource.Explicit);

ownershipBuilder.Metadata.DeleteBehavior = DeleteBehavior.Restrict;

VerifyError(
CoreStrings.OwnershipNotCascadeDelete(
nameof(SampleEntity), nameof(ReferencedEntity), DeleteBehavior.Restrict, DeleteBehavior.Cascade),
builder);
}

[Fact]
public virtual void Detects_optional_ownership()
{
var builder = CreateConventionlessModelBuilder();
var modelBuilder = (InternalModelBuilder)builder.GetInfrastructure();
var entityTypeBuilder = modelBuilder.Entity(typeof(SampleEntity), ConfigurationSource.Convention);
entityTypeBuilder.PrimaryKey([nameof(SampleEntity.Id)], ConfigurationSource.Convention);
entityTypeBuilder.Ignore(nameof(SampleEntity.Name), ConfigurationSource.Explicit);
entityTypeBuilder.Ignore(nameof(SampleEntity.Number), ConfigurationSource.Explicit);
entityTypeBuilder.Ignore(nameof(SampleEntity.OtherSamples), ConfigurationSource.Explicit);
entityTypeBuilder.Ignore(nameof(SampleEntity.AnotherReferencedEntity), ConfigurationSource.Explicit);

var ownershipBuilder = entityTypeBuilder.HasOwnership(
typeof(ReferencedEntity), nameof(SampleEntity.ReferencedEntity), ConfigurationSource.Convention);

var ownedTypeBuilder = ownershipBuilder.Metadata.DeclaringEntityType.Builder;
ownedTypeBuilder.PrimaryKey(ownershipBuilder.Metadata.Properties.Select(p => p.Name).ToList(), ConfigurationSource.Convention);
ownedTypeBuilder.Ignore(nameof(ReferencedEntity.Id), ConfigurationSource.Explicit);
ownedTypeBuilder.Ignore(nameof(ReferencedEntity.SampleEntityId), ConfigurationSource.Explicit);

ownershipBuilder.Metadata.IsRequired = false;

VerifyError(
CoreStrings.OwnershipNotRequired(nameof(SampleEntity), nameof(ReferencedEntity)),
builder);
}

[Fact]
public virtual void Detects_principal_owned_entity_type()
{
Expand Down
Loading