From 2929859d36cba859e6f3ab5591d802a08abb97e4 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Thu, 10 Mar 2022 23:57:20 +0100 Subject: [PATCH 01/33] Add DeleteBehaviorAttribute --- .../DeleteBehaviorAttribute.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/EFCore.Abstractions/DeleteBehaviorAttribute.cs diff --git a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs new file mode 100644 index 00000000000..9fcc1bcafe0 --- /dev/null +++ b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Configures the class to indicate how a delete operation is applied to dependent entities +/// in a relationship when it is deleted or the relationship is severed. +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class DeleteBehaviorAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The DeleteBehavior value of entity + public DeleteBehaviorAttribute(int behavior) + { + if ( behavior < 0 || behavior > 6) // Valid values for DeleteBehavior enum + { + throw new ArgumentException("This behavior is not defined in DeleteBehavior Enum."); + } + + Behavior = behavior; + } + + /// + /// The DeleteBehavior value + /// + public int Behavior { get; } +} From 25bf1d8500d8197db571d2a0c625cbad2fb11437 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Thu, 10 Mar 2022 23:58:01 +0100 Subject: [PATCH 02/33] Add DeleteBehaviorAttribute Convention that sets delete behavior on foreign key --- .../DeleteBehaviorAttributeConvention.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs new file mode 100644 index 00000000000..efdc768209e --- /dev/null +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that configures the DeleteBehavior based on the applied on the property. +/// +/// +/// See Model building conventions for more information and examples. +/// +public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencies dependencies) + : base(dependencies) { } + + /// + /// Called after a property is added to the entity type with an attribute on the associated CLR property or field. + /// + /// The builder for the property. + /// The attribute. + /// The member that has the attribute. + /// Additional information associated with convention execution. + protected override void ProcessPropertyAdded( + IConventionPropertyBuilder propertyBuilder, + DeleteBehaviorAttribute attribute, + MemberInfo clrMember, + IConventionContext context) + { + if (Enum.IsDefined(typeof(DeleteBehavior), attribute.Behavior)) + { + throw new InvalidEnumArgumentException("This behavior is not defined in DeleteBehavior Enum."); + } + + var deleteBehavior = (DeleteBehavior)attribute.Behavior; + + var propertyForeignKeys = propertyBuilder.Metadata.GetContainingForeignKeys(); + foreach (var foreignKey in propertyForeignKeys) + { + foreignKey.SetDeleteBehavior(deleteBehavior, fromDataAnnotation: true); + } + + } +} From a9034619d63239497a724869cf40c6110e7d0366 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 00:28:22 +0100 Subject: [PATCH 03/33] Register DeleteBehaviorAttributeConvention in ConventionSet provider --- .../Conventions/Infrastructure/ProviderConventionSetBuilder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 83b6dbfb060..8f8c97cca42 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -114,6 +114,7 @@ public virtual ConventionSet CreateConventionSet() var backingFieldAttributeConvention = new BackingFieldAttributeConvention(Dependencies); var unicodeAttributeConvention = new UnicodeAttributeConvention(Dependencies); var precisionAttributeConvention = new PrecisionAttributeConvention(Dependencies); + var deleteBehaviorAttributeConvention = new DeleteBehaviorAttributeConvention(Dependencies); conventionSet.PropertyAddedConventions.Add(backingFieldAttributeConvention); conventionSet.PropertyAddedConventions.Add(backingFieldConvention); @@ -129,6 +130,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.PropertyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.PropertyAddedConventions.Add(unicodeAttributeConvention); conventionSet.PropertyAddedConventions.Add(precisionAttributeConvention); + conventionSet.PropertyAddedConventions.Add(deleteBehaviorAttributeConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(valueGeneratorConvention); From 88b23766baab2d49bca215ad759d3cc0a8dacb13 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 02:33:19 +0100 Subject: [PATCH 04/33] Add tests for DeleteBehaviorAttribute --- .../DeleteBehaviorAttributeConventionTest.cs | 325 ++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs new file mode 100644 index 00000000000..206a28b5cf6 --- /dev/null +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -0,0 +1,325 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + + +// ReSharper disable UnusedAutoPropertyAccessor.Local +// ReSharper disable UnusedMember.Local +// ReSharper disable ClassNeverInstantiated.Local +// ReSharper disable CollectionNeverUpdated.Local +// ReSharper disable InconsistentNaming + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +public class DeleteBehaviorAttributeConventionTest +{ + [ConditionalFact] + public void Without_attribute_preserve_default_behavior() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog).Metadata; + + Assert.Equal(DeleteBehavior.ClientSetNull, fk.DeleteBehavior); + } + + [ConditionalFact] + public void Correctly_set_cascade_delete_behavior_on_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_Cascade).Metadata; + + Assert.Equal(DeleteBehavior.Cascade, fk.DeleteBehavior); + } + + [ConditionalFact] + public void Correctly_set_restrict_delete_behavior_on_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_Restrict).Metadata; + + Assert.Equal(DeleteBehavior.Restrict, fk.DeleteBehavior); + } + + [ConditionalFact] + public void Correctly_set_clientCascade_delete_behavior_on_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_ClientCascade).Metadata; + + Assert.Equal(DeleteBehavior.ClientCascade, fk.DeleteBehavior); + } + + [ConditionalFact] + public void Correctly_set_noAction_delete_behavior_on_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_NoAction).Metadata; + + Assert.Equal(DeleteBehavior.NoAction, fk.DeleteBehavior); + } + + [ConditionalFact] + public void Correctly_set_setNull_delete_behavior_on_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_SetNull).Metadata; + + Assert.Equal(DeleteBehavior.SetNull, fk.DeleteBehavior); + } + + [ConditionalFact] + public void Correctly_set_clientNoAction_delete_behavior_on_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_ClientNoAction).Metadata; + + Assert.Equal(DeleteBehavior.ClientNoAction, fk.DeleteBehavior); + } + + [ConditionalFact] + public void Correctly_set_clientSetNull_delete_behavior_on_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_ClientSetNull).Metadata; + + Assert.Equal(DeleteBehavior.ClientSetNull, fk.DeleteBehavior); + } + + [ConditionalFact] + public void Correctly_set_delete_behavior_on_compound_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .HasKey(e => new { e.Id, e.Id2 }); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_Compound) + .HasForeignKey(e => new {e.BlogId, e.BlogId2}).Metadata; + + Assert.Equal(DeleteBehavior.Cascade, fk.DeleteBehavior); + } + + #region DeleteBehaviorAttribute not set + private class Blog + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post + { + public int Id { get; set; } + + public Blog Blog { get; set; } + + public int? BlogId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set to Cascade + private class Blog_Cascade + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_Cascade + { + public int Id { get; set; } + + public Blog_Cascade Blog_Cascade { get; set; } + + [DeleteBehavior((int)DeleteBehavior.Cascade)] + public int? BlogId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set to Restrict + private class Blog_Restrict + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_Restrict + { + public int Id { get; set; } + + public Blog_Restrict Blog_Restrict { get; set; } + + [DeleteBehavior((int)DeleteBehavior.Restrict)] + public int? BlogId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set to ClientCascade + private class Blog_ClientCascade + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_ClientCascade + { + public int Id { get; set; } + + public Blog_ClientCascade Blog_ClientCascade { get; set; } + + [DeleteBehavior((int)DeleteBehavior.ClientCascade)] + public int? BlogId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set to NoAction + private class Blog_NoAction + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_NoAction + { + public int Id { get; set; } + + public Blog_NoAction Blog_NoAction { get; set; } + + [DeleteBehavior((int)DeleteBehavior.NoAction)] + public int? BlogId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set to SetNull + private class Blog_SetNull + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_SetNull + { + public int Id { get; set; } + + public Blog_SetNull Blog_SetNull { get; set; } + + [DeleteBehavior((int)DeleteBehavior.SetNull)] + public int? BlogId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set to ClientNoAction + private class Blog_ClientNoAction + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_ClientNoAction + { + public int Id { get; set; } + + public Blog_ClientNoAction Blog_ClientNoAction { get; set; } + + [DeleteBehavior((int)DeleteBehavior.ClientNoAction)] + public int? BlogId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set to ClientSetNull + private class Blog_ClientSetNull + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_ClientSetNull + { + public int Id { get; set; } + + public Blog_ClientSetNull Blog_ClientSetNull { get; set; } + + [DeleteBehavior((int)DeleteBehavior.ClientSetNull)] + public int? BlogId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set on compound key + private class Blog_Compound + { + public int Id { get; set; } + public int Id2 { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_Compound + { + public int Id { get; set; } + + public Blog_Compound Blog_Compound { get; set; } + + [DeleteBehavior((int)DeleteBehavior.Cascade)] + public int? BlogId { get; set; } + [DeleteBehavior((int)DeleteBehavior.Cascade)] + public int? BlogId2 { get; set; } + } + #endregion + + private static ModelBuilder CreateModelBuilder() + => InMemoryTestHelpers.Instance.CreateConventionBuilder(); +} From 9ac476a7bf35b34345d8e31474dffa4fe8940392 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 02:33:58 +0100 Subject: [PATCH 05/33] Make DeleteBehaviorAttribute a property or field attribute --- src/EFCore.Abstractions/DeleteBehaviorAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs index 9fcc1bcafe0..2a820b93cbd 100644 --- a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs +++ b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs @@ -4,13 +4,13 @@ namespace Microsoft.EntityFrameworkCore; /// -/// Configures the class to indicate how a delete operation is applied to dependent entities +/// Configures the Property or Field to indicate how a delete operation is applied to dependent entities /// in a relationship when it is deleted or the relationship is severed. /// /// /// See Modeling entity types and relationships for more information and examples. /// -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class DeleteBehaviorAttribute : Attribute { /// From c4103041874e6a6daae2ddcc1b449b6f0ea29fad Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 02:34:23 +0100 Subject: [PATCH 06/33] Make changes to detect added foreign key --- .../DeleteBehaviorAttributeConvention.cs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index efdc768209e..a93f1d43cd3 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; @@ -11,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// /// See Model building conventions for more information and examples. /// -public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase +public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase, IForeignKeyAddedConvention { /// /// Creates a new instance of . @@ -33,18 +34,24 @@ protected override void ProcessPropertyAdded( MemberInfo clrMember, IConventionContext context) { - if (Enum.IsDefined(typeof(DeleteBehavior), attribute.Behavior)) + if (!Enum.IsDefined(typeof(DeleteBehavior), attribute.Behavior)) { throw new InvalidEnumArgumentException("This behavior is not defined in DeleteBehavior Enum."); } - var deleteBehavior = (DeleteBehavior)attribute.Behavior; - - var propertyForeignKeys = propertyBuilder.Metadata.GetContainingForeignKeys(); - foreach (var foreignKey in propertyForeignKeys) - { - foreignKey.SetDeleteBehavior(deleteBehavior, fromDataAnnotation: true); - } + _deleteBehavior = (DeleteBehavior)attribute.Behavior; + } + /// + /// Called after a foreign key is added to the entity type. + /// + /// The builder for the foreign key. + /// Additional information associated with convention execution. + public void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext context) + { + // TODO: Add check does this foreign key contains DeleteBehavior attribute and only then set it + foreignKeyBuilder.Metadata.SetDeleteBehavior(_deleteBehavior); } + + private DeleteBehavior _deleteBehavior; } From 3a1693d43445d56cfe328f6f589caff1107c207d Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 02:34:52 +0100 Subject: [PATCH 07/33] Add new registration in ConventionSet provider --- .../Conventions/Infrastructure/ProviderConventionSetBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 8f8c97cca42..87ef6ad7694 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -150,6 +150,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ForeignKeyAddedConventions.Add(valueGeneratorConvention); conventionSet.ForeignKeyAddedConventions.Add(cascadeDeleteConvention); conventionSet.ForeignKeyAddedConventions.Add(foreignKeyIndexConvention); + conventionSet.ForeignKeyAddedConventions.Add(deleteBehaviorAttributeConvention); conventionSet.ForeignKeyRemovedConventions.Add(baseTypeDiscoveryConvention); conventionSet.ForeignKeyRemovedConventions.Add(relationshipDiscoveryConvention); From 6a59b7ade67515a148123ec4197609305bb70ab5 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 13:35:58 +0100 Subject: [PATCH 08/33] Add more test cases for different edge cases --- .../DeleteBehaviorAttributeConventionTest.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index 206a28b5cf6..bbb372a93e0 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -146,6 +146,8 @@ public void Correctly_set_delete_behavior_on_compound_foreign_key() modelBuilder.Entity() .Property(e => e.BlogId); + modelBuilder.Entity() + .Property(e => e.BlogId2); var fk = modelBuilder.Entity() .HasMany(e => e.Posts) @@ -155,6 +157,30 @@ public void Correctly_set_delete_behavior_on_compound_foreign_key() Assert.Equal(DeleteBehavior.Cascade, fk.DeleteBehavior); } + [ConditionalFact] + public void Correctly_set_delete_behavior_on_two_different_foreign_keys() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.Blog_OneId); + modelBuilder.Entity() + .Property(e => e.Blog_TwoId); + + var fk_One = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_One) + .HasForeignKey(e => e.Blog_OneId).Metadata; + + var fk_Two = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_Two) + .HasForeignKey(e => e.Blog_TwoId).Metadata; + + Assert.Equal(DeleteBehavior.Restrict, fk_One.DeleteBehavior); + Assert.Equal(DeleteBehavior.Cascade, fk_Two.DeleteBehavior); + } + #region DeleteBehaviorAttribute not set private class Blog { @@ -311,14 +337,45 @@ private class Post_Compound { public int Id { get; set; } + public Blog_Compound Blog_Compound { get; set; } [DeleteBehavior((int)DeleteBehavior.Cascade)] public int? BlogId { get; set; } + [DeleteBehavior((int)DeleteBehavior.Cascade)] public int? BlogId2 { get; set; } } #endregion + #region DeleteBehaviourAttribute set on two different foreign keys + private class Blog_One + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + private class Blog_Two + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_Both + { + public int Id { get; set; } + + + public Blog_One Blog_One { get; set; } + public Blog_Two Blog_Two { get; set; } + + [DeleteBehavior((int)DeleteBehavior.Restrict)] + public int? Blog_OneId { get; set; } + + [DeleteBehavior((int)DeleteBehavior.Cascade)] + public int? Blog_TwoId { get; set; } + } + #endregion private static ModelBuilder CreateModelBuilder() => InMemoryTestHelpers.Instance.CreateConventionBuilder(); From 7dc5a6f02337bf90513eb592061923e5ef53d087 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 14:39:54 +0100 Subject: [PATCH 09/33] Add more edge cases to tests --- .../DeleteBehaviorAttributeConventionTest.cs | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index bbb372a93e0..41a6c3569a6 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -141,9 +141,6 @@ public void Correctly_set_delete_behavior_on_compound_foreign_key() { var modelBuilder = CreateModelBuilder(); - modelBuilder.Entity() - .HasKey(e => new { e.Id, e.Id2 }); - modelBuilder.Entity() .Property(e => e.BlogId); modelBuilder.Entity() @@ -151,8 +148,7 @@ public void Correctly_set_delete_behavior_on_compound_foreign_key() var fk = modelBuilder.Entity() .HasMany(e => e.Posts) - .WithOne(e => e.Blog_Compound) - .HasForeignKey(e => new {e.BlogId, e.BlogId2}).Metadata; + .WithOne(e => e.Blog_Compound).Metadata; Assert.Equal(DeleteBehavior.Cascade, fk.DeleteBehavior); } @@ -169,18 +165,32 @@ public void Correctly_set_delete_behavior_on_two_different_foreign_keys() var fk_One = modelBuilder.Entity() .HasMany(e => e.Posts) - .WithOne(e => e.Blog_One) - .HasForeignKey(e => e.Blog_OneId).Metadata; + .WithOne(e => e.Blog_One).Metadata; var fk_Two = modelBuilder.Entity() .HasMany(e => e.Posts) - .WithOne(e => e.Blog_Two) - .HasForeignKey(e => e.Blog_TwoId).Metadata; + .WithOne(e => e.Blog_Two).Metadata; Assert.Equal(DeleteBehavior.Restrict, fk_One.DeleteBehavior); Assert.Equal(DeleteBehavior.Cascade, fk_Two.DeleteBehavior); } + [ConditionalFact] + public void Correctly_set_restrict_delete_behavior_on_foreign_key_declared_by_FluentAPI() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_Restrict) + .HasForeignKey(e => e.BlogId).Metadata; + + Assert.Equal(DeleteBehavior.Restrict, fk.DeleteBehavior); + } + #region DeleteBehaviorAttribute not set private class Blog { @@ -212,6 +222,7 @@ private class Post_Cascade public Blog_Cascade Blog_Cascade { get; set; } + [ForeignKey("Blog_Cascade")] [DeleteBehavior((int)DeleteBehavior.Cascade)] public int? BlogId { get; set; } } @@ -230,6 +241,8 @@ private class Post_Restrict public Blog_Restrict Blog_Restrict { get; set; } + + [ForeignKey("Blog_Restrict")] [DeleteBehavior((int)DeleteBehavior.Restrict)] public int? BlogId { get; set; } } @@ -248,6 +261,7 @@ private class Post_ClientCascade public Blog_ClientCascade Blog_ClientCascade { get; set; } + [ForeignKey("Blog_ClientCascade")] [DeleteBehavior((int)DeleteBehavior.ClientCascade)] public int? BlogId { get; set; } } @@ -266,6 +280,8 @@ private class Post_NoAction public Blog_NoAction Blog_NoAction { get; set; } + + [ForeignKey("Blog_NoAction")] [DeleteBehavior((int)DeleteBehavior.NoAction)] public int? BlogId { get; set; } } @@ -284,6 +300,7 @@ private class Post_SetNull public Blog_SetNull Blog_SetNull { get; set; } + [ForeignKey("Blog_SetNull")] [DeleteBehavior((int)DeleteBehavior.SetNull)] public int? BlogId { get; set; } } @@ -302,6 +319,7 @@ private class Post_ClientNoAction public Blog_ClientNoAction Blog_ClientNoAction { get; set; } + [ForeignKey("Blog_ClientNoAction")] [DeleteBehavior((int)DeleteBehavior.ClientNoAction)] public int? BlogId { get; set; } } @@ -320,6 +338,7 @@ private class Post_ClientSetNull public Blog_ClientSetNull Blog_ClientSetNull { get; set; } + [ForeignKey("Blog_ClientSetNull")] [DeleteBehavior((int)DeleteBehavior.ClientSetNull)] public int? BlogId { get; set; } } @@ -327,7 +346,11 @@ private class Post_ClientSetNull #region DeleteBehaviourAttribute set on compound key private class Blog_Compound { + [Key] + [Column(Order=0)] public int Id { get; set; } + [Key] + [Column(Order=1)] public int Id2 { get; set; } public ICollection Posts { get; set; } @@ -337,12 +360,14 @@ private class Post_Compound { public int Id { get; set; } - + [ForeignKey("BlogId, BlogId2")] public Blog_Compound Blog_Compound { get; set; } + [Column(Order = 0)] [DeleteBehavior((int)DeleteBehavior.Cascade)] public int? BlogId { get; set; } + [Column(Order = 1)] [DeleteBehavior((int)DeleteBehavior.Cascade)] public int? BlogId2 { get; set; } } @@ -369,13 +394,33 @@ private class Post_Both public Blog_One Blog_One { get; set; } public Blog_Two Blog_Two { get; set; } + [ForeignKey("Blog_One")] [DeleteBehavior((int)DeleteBehavior.Restrict)] public int? Blog_OneId { get; set; } + [ForeignKey("Blog_Two")] [DeleteBehavior((int)DeleteBehavior.Cascade)] public int? Blog_TwoId { get; set; } } #endregion + #region DeleteBehaviourAttribute set to Restrict and foreign key defined by FluentApi + private class Blog_Restrict_Fluent + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_Restrict_Fluent + { + public int Id { get; set; } + + public Blog_Restrict_Fluent Blog_Restrict_Fluent { get; set; } + + [DeleteBehavior((int)DeleteBehavior.Restrict)] + public int? BlogId { get; set; } + } + #endregion private static ModelBuilder CreateModelBuilder() => InMemoryTestHelpers.Instance.CreateConventionBuilder(); From 46d054bc370a5aa7f273e575440e2137f43739de Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 14:45:53 +0100 Subject: [PATCH 10/33] Add test case for implicit foreign key --- .../DeleteBehaviorAttributeConventionTest.cs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index 41a6c3569a6..3ef878da13e 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -176,7 +176,7 @@ public void Correctly_set_delete_behavior_on_two_different_foreign_keys() } [ConditionalFact] - public void Correctly_set_restrict_delete_behavior_on_foreign_key_declared_by_FluentAPI() + public void Correctly_set_delete_behavior_on_foreign_key_declared_by_FluentAPI() { var modelBuilder = CreateModelBuilder(); @@ -191,6 +191,21 @@ public void Correctly_set_restrict_delete_behavior_on_foreign_key_declared_by_Fl Assert.Equal(DeleteBehavior.Restrict, fk.DeleteBehavior); } + [ConditionalFact] + public void Correctly_set_delete_behavior_on_implicit_foreign_key() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.BlogId); + + var fk = modelBuilder.Entity() + .HasMany(e => e.Posts) + .WithOne(e => e.Blog_Restrict).Metadata; + + Assert.Equal(DeleteBehavior.Restrict, fk.DeleteBehavior); + } + #region DeleteBehaviorAttribute not set private class Blog { @@ -421,6 +436,24 @@ private class Post_Restrict_Fluent public int? BlogId { get; set; } } #endregion + #region DeleteBehaviourAttribute set to Restrict and implicit foreign key + private class Blog_Restrict_Implicit + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_Restrict_Implicit + { + public int Id { get; set; } + + public Blog_Restrict_Implicit Blog_Restrict_Implicit { get; set; } + + [DeleteBehavior((int)DeleteBehavior.Restrict)] + public int? BlogId { get; set; } + } + #endregion private static ModelBuilder CreateModelBuilder() => InMemoryTestHelpers.Instance.CreateConventionBuilder(); From c454830f6e6476d615eb28ef72d009dfbe0e0cc6 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 14:46:59 +0100 Subject: [PATCH 11/33] Cleanup convention and registration in provider --- .../DeleteBehaviorAttributeConvention.cs | 46 ++++++++----------- .../ProviderConventionSetBuilder.cs | 3 +- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index a93f1d43cd3..601434d1fed 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; -using Microsoft.EntityFrameworkCore.Metadata.Internal; - namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// @@ -12,35 +9,21 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// /// See Model building conventions for more information and examples. /// -public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase, IForeignKeyAddedConvention +public class DeleteBehaviorAttributeConvention : IForeignKeyAddedConvention { /// /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencies dependencies) - : base(dependencies) { } - - /// - /// Called after a property is added to the entity type with an attribute on the associated CLR property or field. - /// - /// The builder for the property. - /// The attribute. - /// The member that has the attribute. - /// Additional information associated with convention execution. - protected override void ProcessPropertyAdded( - IConventionPropertyBuilder propertyBuilder, - DeleteBehaviorAttribute attribute, - MemberInfo clrMember, - IConventionContext context) { - if (!Enum.IsDefined(typeof(DeleteBehavior), attribute.Behavior)) - { - throw new InvalidEnumArgumentException("This behavior is not defined in DeleteBehavior Enum."); - } - - _deleteBehavior = (DeleteBehavior)attribute.Behavior; + Dependencies = dependencies; } + + /// + /// Dependencies for this service. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } /// /// Called after a foreign key is added to the entity type. @@ -49,9 +32,16 @@ protected override void ProcessPropertyAdded( /// Additional information associated with convention execution. public void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext context) { - // TODO: Add check does this foreign key contains DeleteBehavior attribute and only then set it - foreignKeyBuilder.Metadata.SetDeleteBehavior(_deleteBehavior); + var foreignKey = foreignKeyBuilder.Metadata; + var properties = foreignKey.Properties; + foreach (var property in properties) + { + var attribute = property?.PropertyInfo?.GetCustomAttribute(); + if (attribute != null) + { + var deleteBehavior = (DeleteBehavior)attribute.Behavior; + foreignKey.SetDeleteBehavior(deleteBehavior); + } + } } - - private DeleteBehavior _deleteBehavior; } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 87ef6ad7694..13e4b30feb0 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -114,7 +114,6 @@ public virtual ConventionSet CreateConventionSet() var backingFieldAttributeConvention = new BackingFieldAttributeConvention(Dependencies); var unicodeAttributeConvention = new UnicodeAttributeConvention(Dependencies); var precisionAttributeConvention = new PrecisionAttributeConvention(Dependencies); - var deleteBehaviorAttributeConvention = new DeleteBehaviorAttributeConvention(Dependencies); conventionSet.PropertyAddedConventions.Add(backingFieldAttributeConvention); conventionSet.PropertyAddedConventions.Add(backingFieldConvention); @@ -130,7 +129,6 @@ public virtual ConventionSet CreateConventionSet() conventionSet.PropertyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.PropertyAddedConventions.Add(unicodeAttributeConvention); conventionSet.PropertyAddedConventions.Add(precisionAttributeConvention); - conventionSet.PropertyAddedConventions.Add(deleteBehaviorAttributeConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(valueGeneratorConvention); @@ -143,6 +141,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.KeyRemovedConventions.Add(keyDiscoveryConvention); var cascadeDeleteConvention = new CascadeDeleteConvention(Dependencies); + var deleteBehaviorAttributeConvention = new DeleteBehaviorAttributeConvention(Dependencies); conventionSet.ForeignKeyAddedConventions.Add(foreignKeyAttributeConvention); conventionSet.ForeignKeyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); From ac255370e81fc68c3cc4c0766bb81b7f1cda63f6 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 23:21:13 +0100 Subject: [PATCH 12/33] Move DeleteBehavior enum and add TypeForward --- .../DeleteBehavior.cs | 72 ++++++++++++------- src/EFCore/Properties/TypeForwards.cs | 1 + 2 files changed, 46 insertions(+), 27 deletions(-) rename src/{EFCore => EFCore.Abstractions}/DeleteBehavior.cs (53%) diff --git a/src/EFCore/DeleteBehavior.cs b/src/EFCore.Abstractions/DeleteBehavior.cs similarity index 53% rename from src/EFCore/DeleteBehavior.cs rename to src/EFCore.Abstractions/DeleteBehavior.cs index 52b7e1eb54c..6ad8603d33d 100644 --- a/src/EFCore/DeleteBehavior.cs +++ b/src/EFCore.Abstractions/DeleteBehavior.cs @@ -10,12 +10,15 @@ namespace Microsoft.EntityFrameworkCore; /// /// /// Behaviors in the database are dependent on the database schema being created -/// appropriately. Using Entity Framework Migrations or +/// appropriately. Using Entity Framework Migrations or +/// +/// EnsureCreated() /// will create the appropriate schema. /// /// /// Note that the in-memory behavior for entities that are currently tracked by -/// the can be different from the behavior that happens in the database. +/// the DbContext +/// can be different from the behavior that happens in the database. /// /// /// See Cascade delete and deleting orphans in EF Core for more information and @@ -25,16 +28,20 @@ namespace Microsoft.EntityFrameworkCore; public enum DeleteBehavior { /// - /// For entities being tracked by the , the values of foreign key properties in + /// For entities being tracked by the + /// DbContext, the values of foreign key properties in /// dependent entities are set to null when the related principal is deleted. /// This helps keep the graph of entities in a consistent state while they are being tracked, such that a /// fully consistent graph can then be written to the database. If a property cannot be set to null because - /// it is not a nullable type, then an exception will be thrown when is called. + /// it is not a nullable type, then an exception will be thrown when + /// + /// SaveChanges() is called. /// /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// method, then the behavior in the database + /// + /// EnsureCreated() method, then the behavior in the database /// is to generate an error if a foreign key constraint is violated. /// /// @@ -45,29 +52,35 @@ public enum DeleteBehavior ClientSetNull, /// - /// For entities being tracked by the , the values of foreign key properties in - /// dependent entities are set to null when the related principal is deleted. + /// For entities being tracked by the + /// DbContext, the values of foreign key properties in dependent entities are set to null when the related principal is deleted. /// This helps keep the graph of entities in a consistent state while they are being tracked, such that a /// fully consistent graph can then be written to the database. If a property cannot be set to null because - /// it is not a nullable type, then an exception will be thrown when is called. + /// it is not a nullable type, then an exception will be thrown when + /// + /// SaveChanges() is called. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// method, then the behavior in the database - /// is to generate an error if a foreign key constraint is violated. + /// + /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. /// Restrict, /// - /// For entities being tracked by the , the values of foreign key properties in + /// For entities being tracked by the + /// DbContext, the values of foreign key properties in /// dependent entities are set to null when the related principal is deleted. /// This helps keep the graph of entities in a consistent state while they are being tracked, such that a /// fully consistent graph can then be written to the database. If a property cannot be set to null because - /// it is not a nullable type, then an exception will be thrown when is called. + /// it is not a nullable type, then an exception will be thrown when + /// + /// SaveChanges() is called. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// method, then the behavior in the database is + /// + /// EnsureCreated() method, then the behavior in the database is /// the same as is described above for tracked entities. Keep in mind that some databases cannot easily /// support this behavior, especially if there are cycles in relationships, in which case it may /// be better to use which will allow EF to cascade null values @@ -76,13 +89,15 @@ public enum DeleteBehavior SetNull, /// - /// For entities being tracked by the , dependent entities + /// For entities being tracked by the + /// DbContext, dependent entities /// will be deleted when the related principal is deleted. /// /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// method, then the behavior in the database is + /// + /// EnsureCreated() method, then the behavior in the database is /// the same as is described above for tracked entities. Keep in mind that some databases cannot easily /// support this behavior, especially if there are cycles in relationships, in which case it may /// be better to use which will allow EF to perform cascade deletes @@ -96,27 +111,30 @@ public enum DeleteBehavior Cascade, /// - /// For entities being tracked by the , dependent entities + /// For entities being tracked by the + /// DbContext, dependent entities /// will be deleted when the related principal is deleted. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// method, then the behavior in the database - /// is to generate an error if a foreign key constraint is violated. + /// + /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. /// ClientCascade, /// - /// For entities being tracked by the , the values of foreign key properties in - /// dependent entities are set to null when the related principal is deleted. + /// For entities being tracked by the + /// DbContext, the values of foreign key properties in dependent entities are set to null when the related principal is deleted. /// This helps keep the graph of entities in a consistent state while they are being tracked, such that a /// fully consistent graph can then be written to the database. If a property cannot be set to null because - /// it is not a nullable type, then an exception will be thrown when is called. + /// it is not a nullable type, then an exception will be thrown when + /// + /// SaveChanges() is called. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// method, then the behavior in the database - /// is to generate an error if a foreign key constraint is violated. + /// + /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. /// NoAction, @@ -126,15 +144,15 @@ public enum DeleteBehavior /// /// /// - /// For entities being tracked by the , the values of foreign key properties in - /// dependent entities are not changed when the related principal entity is deleted. + /// For entities being tracked by the + /// DbContext, the values of foreign key properties in dependent entities are not changed when the related principal entity is deleted. /// This can result in an inconsistent graph of entities where the values of foreign key properties do /// not match the relationships in the graph. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// method, then the behavior in the database - /// is to generate an error if a foreign key constraint is violated. + /// + /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. /// /// ClientNoAction diff --git a/src/EFCore/Properties/TypeForwards.cs b/src/EFCore/Properties/TypeForwards.cs index a199f90d4e2..cb6700c69f9 100644 --- a/src/EFCore/Properties/TypeForwards.cs +++ b/src/EFCore/Properties/TypeForwards.cs @@ -8,3 +8,4 @@ [assembly: TypeForwardedTo(typeof(ObservableCollectionExtensions))] [assembly: TypeForwardedTo(typeof(ObservableCollectionListSource<>))] [assembly: TypeForwardedTo(typeof(SortableBindingList<>))] +[assembly: TypeForwardedTo(typeof(DeleteBehavior))] From e2aed9ab770a71c5531b81e5a62b95a3fa041e8d Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 23:21:55 +0100 Subject: [PATCH 13/33] Make DeleteBehaviorAttribute code and tests use DeleteBehavior enum --- .../DeleteBehaviorAttribute.cs | 9 ++----- .../DeleteBehaviorAttributeConvention.cs | 5 ++-- .../DeleteBehaviorAttributeConventionTest.cs | 26 +++++++++---------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs index 2a820b93cbd..ce55e5e2107 100644 --- a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs +++ b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs @@ -17,18 +17,13 @@ public sealed class DeleteBehaviorAttribute : Attribute /// Initializes a new instance of the class. /// /// The DeleteBehavior value of entity - public DeleteBehaviorAttribute(int behavior) + public DeleteBehaviorAttribute(DeleteBehavior behavior) { - if ( behavior < 0 || behavior > 6) // Valid values for DeleteBehavior enum - { - throw new ArgumentException("This behavior is not defined in DeleteBehavior Enum."); - } - Behavior = behavior; } /// /// The DeleteBehavior value /// - public int Behavior { get; } + public DeleteBehavior Behavior { get; } } diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index 601434d1fed..afb32052790 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -30,7 +30,7 @@ public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencie /// /// The builder for the foreign key. /// Additional information associated with convention execution. - public void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext context) + public virtual void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext context) { var foreignKey = foreignKeyBuilder.Metadata; var properties = foreignKey.Properties; @@ -39,8 +39,7 @@ public void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignKeyBuilde var attribute = property?.PropertyInfo?.GetCustomAttribute(); if (attribute != null) { - var deleteBehavior = (DeleteBehavior)attribute.Behavior; - foreignKey.SetDeleteBehavior(deleteBehavior); + foreignKey.SetDeleteBehavior(attribute.Behavior); } } } diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index 3ef878da13e..61bc72ef090 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -238,7 +238,7 @@ private class Post_Cascade public Blog_Cascade Blog_Cascade { get; set; } [ForeignKey("Blog_Cascade")] - [DeleteBehavior((int)DeleteBehavior.Cascade)] + [DeleteBehavior(DeleteBehavior.Cascade)] public int? BlogId { get; set; } } #endregion @@ -258,7 +258,7 @@ private class Post_Restrict [ForeignKey("Blog_Restrict")] - [DeleteBehavior((int)DeleteBehavior.Restrict)] + [DeleteBehavior(DeleteBehavior.Restrict)] public int? BlogId { get; set; } } #endregion @@ -277,7 +277,7 @@ private class Post_ClientCascade public Blog_ClientCascade Blog_ClientCascade { get; set; } [ForeignKey("Blog_ClientCascade")] - [DeleteBehavior((int)DeleteBehavior.ClientCascade)] + [DeleteBehavior(DeleteBehavior.ClientCascade)] public int? BlogId { get; set; } } #endregion @@ -297,7 +297,7 @@ private class Post_NoAction [ForeignKey("Blog_NoAction")] - [DeleteBehavior((int)DeleteBehavior.NoAction)] + [DeleteBehavior(DeleteBehavior.NoAction)] public int? BlogId { get; set; } } #endregion @@ -316,7 +316,7 @@ private class Post_SetNull public Blog_SetNull Blog_SetNull { get; set; } [ForeignKey("Blog_SetNull")] - [DeleteBehavior((int)DeleteBehavior.SetNull)] + [DeleteBehavior(DeleteBehavior.SetNull)] public int? BlogId { get; set; } } #endregion @@ -335,7 +335,7 @@ private class Post_ClientNoAction public Blog_ClientNoAction Blog_ClientNoAction { get; set; } [ForeignKey("Blog_ClientNoAction")] - [DeleteBehavior((int)DeleteBehavior.ClientNoAction)] + [DeleteBehavior(DeleteBehavior.ClientNoAction)] public int? BlogId { get; set; } } #endregion @@ -354,7 +354,7 @@ private class Post_ClientSetNull public Blog_ClientSetNull Blog_ClientSetNull { get; set; } [ForeignKey("Blog_ClientSetNull")] - [DeleteBehavior((int)DeleteBehavior.ClientSetNull)] + [DeleteBehavior(DeleteBehavior.ClientSetNull)] public int? BlogId { get; set; } } #endregion @@ -379,11 +379,11 @@ private class Post_Compound public Blog_Compound Blog_Compound { get; set; } [Column(Order = 0)] - [DeleteBehavior((int)DeleteBehavior.Cascade)] + [DeleteBehavior(DeleteBehavior.Cascade)] public int? BlogId { get; set; } [Column(Order = 1)] - [DeleteBehavior((int)DeleteBehavior.Cascade)] + [DeleteBehavior(DeleteBehavior.Cascade)] public int? BlogId2 { get; set; } } #endregion @@ -410,11 +410,11 @@ private class Post_Both public Blog_Two Blog_Two { get; set; } [ForeignKey("Blog_One")] - [DeleteBehavior((int)DeleteBehavior.Restrict)] + [DeleteBehavior(DeleteBehavior.Restrict)] public int? Blog_OneId { get; set; } [ForeignKey("Blog_Two")] - [DeleteBehavior((int)DeleteBehavior.Cascade)] + [DeleteBehavior(DeleteBehavior.Cascade)] public int? Blog_TwoId { get; set; } } #endregion @@ -432,7 +432,7 @@ private class Post_Restrict_Fluent public Blog_Restrict_Fluent Blog_Restrict_Fluent { get; set; } - [DeleteBehavior((int)DeleteBehavior.Restrict)] + [DeleteBehavior(DeleteBehavior.Restrict)] public int? BlogId { get; set; } } #endregion @@ -450,7 +450,7 @@ private class Post_Restrict_Implicit public Blog_Restrict_Implicit Blog_Restrict_Implicit { get; set; } - [DeleteBehavior((int)DeleteBehavior.Restrict)] + [DeleteBehavior(DeleteBehavior.Restrict)] public int? BlogId { get; set; } } #endregion From fb3467986353ad9d7ddc08f02fe7b3d28b1bf752 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Fri, 11 Mar 2022 23:22:39 +0100 Subject: [PATCH 14/33] Add EFCore.Abstractions assembly to fix tests after TypeForwarding DeleteBehavior enum --- .../EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs | 1 + .../Migrations/MigrationsTestBase.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 285844ce4c9..8b3b648a8da 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -6083,6 +6083,7 @@ protected virtual ICollection GetReferences() => new List { BuildReference.ByName("Microsoft.EntityFrameworkCore"), + BuildReference.ByName("Microsoft.EntityFrameworkCore.Abstractions"), BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational"), BuildReference.ByName("Microsoft.EntityFrameworkCore.SqlServer"), BuildReference.ByName("NetTopologySuite") diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs index b8ecced9d14..6b1797563f7 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs @@ -2023,6 +2023,7 @@ protected IModel BuildModelFromSnapshotSource(string code) // Add standard EF references, a reference to the provider's assembly, and any extra references added by the provider's test suite build.References.Add(BuildReference.ByName("Microsoft.EntityFrameworkCore")); + build.References.Add(BuildReference.ByName("Microsoft.EntityFrameworkCore.Abstractions")); build.References.Add(BuildReference.ByName("Microsoft.EntityFrameworkCore.Relational")); var databaseProvider = Fixture.TestHelpers.CreateContextServices().GetRequiredService(); From c0e9b70a6720490b5150a1d0b4cf78be7690c6c1 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 23 Mar 2022 22:35:16 +0100 Subject: [PATCH 15/33] Make DeleteBehaviorAttribute descriptions more meaningful --- src/EFCore.Abstractions/DeleteBehaviorAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs index ce55e5e2107..6138dd8a821 100644 --- a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs +++ b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs @@ -16,14 +16,14 @@ public sealed class DeleteBehaviorAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The DeleteBehavior value of entity + /// The to be configured. public DeleteBehaviorAttribute(DeleteBehavior behavior) { Behavior = behavior; } /// - /// The DeleteBehavior value + /// The to be configured. /// public DeleteBehavior Behavior { get; } } From 95cc27e01428087a49e39c9cd8748fde183359ca Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 23 Mar 2022 22:37:29 +0100 Subject: [PATCH 16/33] Change formatting in DeleteBehaviorAttributeConvention and fix incorrect class cref on convention constructor --- .../Metadata/Conventions/DeleteBehaviorAttributeConvention.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index afb32052790..278b144b7a5 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -4,7 +4,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// -/// A convention that configures the DeleteBehavior based on the applied on the property. +/// A convention that configures the delete behavior based on the applied on the property. /// /// /// See Model building conventions for more information and examples. @@ -12,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; public class DeleteBehaviorAttributeConvention : IForeignKeyAddedConvention { /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencies dependencies) From 12d700ecbb26051610f0b0f7ef4a7e2ea26ee127 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 23 Mar 2022 22:58:30 +0100 Subject: [PATCH 17/33] Simplify test suite for DeleteBehaviorAttribute and do some minor formatting fixes --- .../DeleteBehaviorAttribute.cs | 2 +- .../DeleteBehaviorAttributeConvention.cs | 2 +- .../DeleteBehaviorAttributeConventionTest.cs | 274 ------------------ 3 files changed, 2 insertions(+), 276 deletions(-) diff --git a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs index 6138dd8a821..153866e3b88 100644 --- a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs +++ b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs @@ -4,7 +4,7 @@ namespace Microsoft.EntityFrameworkCore; /// -/// Configures the Property or Field to indicate how a delete operation is applied to dependent entities +/// Configures the property or field to indicate how a delete operation is applied to dependent entities /// in a relationship when it is deleted or the relationship is severed. /// /// diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index 278b144b7a5..ea71d493081 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -36,7 +36,7 @@ public virtual void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignK var properties = foreignKey.Properties; foreach (var property in properties) { - var attribute = property?.PropertyInfo?.GetCustomAttribute(); + var attribute = property.PropertyInfo?.GetCustomAttribute(); if (attribute != null) { foreignKey.SetDeleteBehavior(attribute.Behavior); diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index 61bc72ef090..9bafa6c90f6 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -31,21 +31,6 @@ public void Without_attribute_preserve_default_behavior() Assert.Equal(DeleteBehavior.ClientSetNull, fk.DeleteBehavior); } - [ConditionalFact] - public void Correctly_set_cascade_delete_behavior_on_foreign_key() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity() - .Property(e => e.BlogId); - - var fk = modelBuilder.Entity() - .HasMany(e => e.Posts) - .WithOne(e => e.Blog_Cascade).Metadata; - - Assert.Equal(DeleteBehavior.Cascade, fk.DeleteBehavior); - } - [ConditionalFact] public void Correctly_set_restrict_delete_behavior_on_foreign_key() { @@ -61,81 +46,6 @@ public void Correctly_set_restrict_delete_behavior_on_foreign_key() Assert.Equal(DeleteBehavior.Restrict, fk.DeleteBehavior); } - [ConditionalFact] - public void Correctly_set_clientCascade_delete_behavior_on_foreign_key() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity() - .Property(e => e.BlogId); - - var fk = modelBuilder.Entity() - .HasMany(e => e.Posts) - .WithOne(e => e.Blog_ClientCascade).Metadata; - - Assert.Equal(DeleteBehavior.ClientCascade, fk.DeleteBehavior); - } - - [ConditionalFact] - public void Correctly_set_noAction_delete_behavior_on_foreign_key() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity() - .Property(e => e.BlogId); - - var fk = modelBuilder.Entity() - .HasMany(e => e.Posts) - .WithOne(e => e.Blog_NoAction).Metadata; - - Assert.Equal(DeleteBehavior.NoAction, fk.DeleteBehavior); - } - - [ConditionalFact] - public void Correctly_set_setNull_delete_behavior_on_foreign_key() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity() - .Property(e => e.BlogId); - - var fk = modelBuilder.Entity() - .HasMany(e => e.Posts) - .WithOne(e => e.Blog_SetNull).Metadata; - - Assert.Equal(DeleteBehavior.SetNull, fk.DeleteBehavior); - } - - [ConditionalFact] - public void Correctly_set_clientNoAction_delete_behavior_on_foreign_key() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity() - .Property(e => e.BlogId); - - var fk = modelBuilder.Entity() - .HasMany(e => e.Posts) - .WithOne(e => e.Blog_ClientNoAction).Metadata; - - Assert.Equal(DeleteBehavior.ClientNoAction, fk.DeleteBehavior); - } - - [ConditionalFact] - public void Correctly_set_clientSetNull_delete_behavior_on_foreign_key() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity() - .Property(e => e.BlogId); - - var fk = modelBuilder.Entity() - .HasMany(e => e.Posts) - .WithOne(e => e.Blog_ClientSetNull).Metadata; - - Assert.Equal(DeleteBehavior.ClientSetNull, fk.DeleteBehavior); - } - [ConditionalFact] public void Correctly_set_delete_behavior_on_compound_foreign_key() { @@ -175,37 +85,6 @@ public void Correctly_set_delete_behavior_on_two_different_foreign_keys() Assert.Equal(DeleteBehavior.Cascade, fk_Two.DeleteBehavior); } - [ConditionalFact] - public void Correctly_set_delete_behavior_on_foreign_key_declared_by_FluentAPI() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity() - .Property(e => e.BlogId); - - var fk = modelBuilder.Entity() - .HasMany(e => e.Posts) - .WithOne(e => e.Blog_Restrict) - .HasForeignKey(e => e.BlogId).Metadata; - - Assert.Equal(DeleteBehavior.Restrict, fk.DeleteBehavior); - } - - [ConditionalFact] - public void Correctly_set_delete_behavior_on_implicit_foreign_key() - { - var modelBuilder = CreateModelBuilder(); - - modelBuilder.Entity() - .Property(e => e.BlogId); - - var fk = modelBuilder.Entity() - .HasMany(e => e.Posts) - .WithOne(e => e.Blog_Restrict).Metadata; - - Assert.Equal(DeleteBehavior.Restrict, fk.DeleteBehavior); - } - #region DeleteBehaviorAttribute not set private class Blog { @@ -223,25 +102,6 @@ private class Post public int? BlogId { get; set; } } #endregion - #region DeleteBehaviourAttribute set to Cascade - private class Blog_Cascade - { - public int Id { get; set; } - - public ICollection Posts { get; set; } - } - - private class Post_Cascade - { - public int Id { get; set; } - - public Blog_Cascade Blog_Cascade { get; set; } - - [ForeignKey("Blog_Cascade")] - [DeleteBehavior(DeleteBehavior.Cascade)] - public int? BlogId { get; set; } - } - #endregion #region DeleteBehaviourAttribute set to Restrict private class Blog_Restrict { @@ -256,108 +116,10 @@ private class Post_Restrict public Blog_Restrict Blog_Restrict { get; set; } - - [ForeignKey("Blog_Restrict")] [DeleteBehavior(DeleteBehavior.Restrict)] public int? BlogId { get; set; } } #endregion - #region DeleteBehaviourAttribute set to ClientCascade - private class Blog_ClientCascade - { - public int Id { get; set; } - - public ICollection Posts { get; set; } - } - - private class Post_ClientCascade - { - public int Id { get; set; } - - public Blog_ClientCascade Blog_ClientCascade { get; set; } - - [ForeignKey("Blog_ClientCascade")] - [DeleteBehavior(DeleteBehavior.ClientCascade)] - public int? BlogId { get; set; } - } - #endregion - #region DeleteBehaviourAttribute set to NoAction - private class Blog_NoAction - { - public int Id { get; set; } - - public ICollection Posts { get; set; } - } - - private class Post_NoAction - { - public int Id { get; set; } - - public Blog_NoAction Blog_NoAction { get; set; } - - - [ForeignKey("Blog_NoAction")] - [DeleteBehavior(DeleteBehavior.NoAction)] - public int? BlogId { get; set; } - } - #endregion - #region DeleteBehaviourAttribute set to SetNull - private class Blog_SetNull - { - public int Id { get; set; } - - public ICollection Posts { get; set; } - } - - private class Post_SetNull - { - public int Id { get; set; } - - public Blog_SetNull Blog_SetNull { get; set; } - - [ForeignKey("Blog_SetNull")] - [DeleteBehavior(DeleteBehavior.SetNull)] - public int? BlogId { get; set; } - } - #endregion - #region DeleteBehaviourAttribute set to ClientNoAction - private class Blog_ClientNoAction - { - public int Id { get; set; } - - public ICollection Posts { get; set; } - } - - private class Post_ClientNoAction - { - public int Id { get; set; } - - public Blog_ClientNoAction Blog_ClientNoAction { get; set; } - - [ForeignKey("Blog_ClientNoAction")] - [DeleteBehavior(DeleteBehavior.ClientNoAction)] - public int? BlogId { get; set; } - } - #endregion - #region DeleteBehaviourAttribute set to ClientSetNull - private class Blog_ClientSetNull - { - public int Id { get; set; } - - public ICollection Posts { get; set; } - } - - private class Post_ClientSetNull - { - public int Id { get; set; } - - public Blog_ClientSetNull Blog_ClientSetNull { get; set; } - - [ForeignKey("Blog_ClientSetNull")] - [DeleteBehavior(DeleteBehavior.ClientSetNull)] - public int? BlogId { get; set; } - } - #endregion #region DeleteBehaviourAttribute set on compound key private class Blog_Compound { @@ -418,42 +180,6 @@ private class Post_Both public int? Blog_TwoId { get; set; } } #endregion - #region DeleteBehaviourAttribute set to Restrict and foreign key defined by FluentApi - private class Blog_Restrict_Fluent - { - public int Id { get; set; } - - public ICollection Posts { get; set; } - } - - private class Post_Restrict_Fluent - { - public int Id { get; set; } - - public Blog_Restrict_Fluent Blog_Restrict_Fluent { get; set; } - - [DeleteBehavior(DeleteBehavior.Restrict)] - public int? BlogId { get; set; } - } - #endregion - #region DeleteBehaviourAttribute set to Restrict and implicit foreign key - private class Blog_Restrict_Implicit - { - public int Id { get; set; } - - public ICollection Posts { get; set; } - } - - private class Post_Restrict_Implicit - { - public int Id { get; set; } - - public Blog_Restrict_Implicit Blog_Restrict_Implicit { get; set; } - - [DeleteBehavior(DeleteBehavior.Restrict)] - public int? BlogId { get; set; } - } - #endregion private static ModelBuilder CreateModelBuilder() => InMemoryTestHelpers.Instance.CreateConventionBuilder(); From 8168b9f4334fb14f2a0077c2c669e011a22b921e Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 23 Mar 2022 23:36:14 +0100 Subject: [PATCH 18/33] Make tests reflect setting the attribute on navigation property --- .../DeleteBehaviorAttributeConventionTest.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index 9bafa6c90f6..d67dc5bc906 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -114,13 +114,13 @@ private class Post_Restrict { public int Id { get; set; } + [DeleteBehavior(DeleteBehavior.Restrict)] public Blog_Restrict Blog_Restrict { get; set; } - [DeleteBehavior(DeleteBehavior.Restrict)] public int? BlogId { get; set; } } #endregion - #region DeleteBehaviourAttribute set on compound key + #region DeleteBehaviourAttribute set on compound key private class Blog_Compound { [Key] @@ -138,14 +138,13 @@ private class Post_Compound public int Id { get; set; } [ForeignKey("BlogId, BlogId2")] + [DeleteBehavior(DeleteBehavior.Cascade)] public Blog_Compound Blog_Compound { get; set; } [Column(Order = 0)] - [DeleteBehavior(DeleteBehavior.Cascade)] public int? BlogId { get; set; } [Column(Order = 1)] - [DeleteBehavior(DeleteBehavior.Cascade)] public int? BlogId2 { get; set; } } #endregion @@ -167,16 +166,14 @@ private class Post_Both { public int Id { get; set; } - + [DeleteBehavior(DeleteBehavior.Restrict)] public Blog_One Blog_One { get; set; } + + [DeleteBehavior(DeleteBehavior.Cascade)] public Blog_Two Blog_Two { get; set; } - [ForeignKey("Blog_One")] - [DeleteBehavior(DeleteBehavior.Restrict)] public int? Blog_OneId { get; set; } - [ForeignKey("Blog_Two")] - [DeleteBehavior(DeleteBehavior.Cascade)] public int? Blog_TwoId { get; set; } } #endregion From 7e6d76606ee93623fcd44f5c1d1eda5eec58bb53 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 23 Mar 2022 23:37:00 +0100 Subject: [PATCH 19/33] Take attribute value from being set on dependent navigation property instead of backing properties of it --- .../DeleteBehaviorAttributeConvention.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index ea71d493081..2f169341afe 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -33,13 +33,27 @@ public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencie public virtual void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext context) { var foreignKey = foreignKeyBuilder.Metadata; - var properties = foreignKey.Properties; - foreach (var property in properties) + var navigationProperty = foreignKey.GetNavigation(true); + + if (navigationProperty == null) + { + return; + } + + var navigationAttribute = navigationProperty.PropertyInfo?.GetCustomAttribute(); + if (navigationAttribute == null) + { + return; // No DeleteBehaviorAttribute on property - early return + } + foreignKey.SetDeleteBehavior(navigationAttribute.Behavior); + + var backingProperties = foreignKey.Properties; + foreach (var property in backingProperties) { var attribute = property.PropertyInfo?.GetCustomAttribute(); if (attribute != null) { - foreignKey.SetDeleteBehavior(attribute.Behavior); + return; // Possibly throw an exception that attribute should be only configured on navigation property } } } From af2e118ef4be29a32b115834632dd812ac214f11 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 23 Mar 2022 23:37:37 +0100 Subject: [PATCH 20/33] Limit use of the attribute to only properties to accomodate only setting it on the navigation properties --- src/EFCore.Abstractions/DeleteBehaviorAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs index 153866e3b88..8315135ca10 100644 --- a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs +++ b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs @@ -10,7 +10,7 @@ namespace Microsoft.EntityFrameworkCore; /// /// See Modeling entity types and relationships for more information and examples. /// -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +[AttributeUsage(AttributeTargets.Property)] public sealed class DeleteBehaviorAttribute : Attribute { /// From 256cb549a29defcf5d13e8bc2b9f5facaa7b8546 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Thu, 24 Mar 2022 22:13:36 +0100 Subject: [PATCH 21/33] Throw InvalidOperationException when DeleteBehaviorAttribute is set on something different than navigation property --- .../DeleteBehaviorAttributeConvention.cs | 36 +++++++++++++------ .../ProviderConventionSetBuilder.cs | 3 +- src/EFCore/Properties/CoreStrings.Designer.cs | 6 ++++ src/EFCore/Properties/CoreStrings.resx | 3 ++ 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index 2f169341afe..fc82d3beb05 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -9,21 +9,36 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// /// See Model building conventions for more information and examples. /// -public class DeleteBehaviorAttributeConvention : IForeignKeyAddedConvention +public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase, IForeignKeyAddedConvention { /// /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencies dependencies) - { - Dependencies = dependencies; - } - + : base(dependencies) { } + /// - /// Dependencies for this service. + /// Called after a property is added to the entity type with an attribute on the associated CLR property or field. /// - protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + /// The builder for the property. + /// The attribute. + /// The member that has the attribute. + /// Additional information associated with convention execution. + protected override void ProcessPropertyAdded( + IConventionPropertyBuilder propertyBuilder, + DeleteBehaviorAttribute attribute, + MemberInfo clrMember, + IConventionContext context) + { + var isForeignKey = propertyBuilder.Metadata.IsForeignKey(); + if (isForeignKey) + { + return; + } + + throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty); + } /// /// Called after a foreign key is added to the entity type. @@ -41,11 +56,10 @@ public virtual void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignK } var navigationAttribute = navigationProperty.PropertyInfo?.GetCustomAttribute(); - if (navigationAttribute == null) + if (navigationAttribute != null) { - return; // No DeleteBehaviorAttribute on property - early return + foreignKey.SetDeleteBehavior(navigationAttribute.Behavior); } - foreignKey.SetDeleteBehavior(navigationAttribute.Behavior); var backingProperties = foreignKey.Properties; foreach (var property in backingProperties) @@ -53,7 +67,7 @@ public virtual void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignK var attribute = property.PropertyInfo?.GetCustomAttribute(); if (attribute != null) { - return; // Possibly throw an exception that attribute should be only configured on navigation property + throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty); } } } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 13e4b30feb0..87ef6ad7694 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -114,6 +114,7 @@ public virtual ConventionSet CreateConventionSet() var backingFieldAttributeConvention = new BackingFieldAttributeConvention(Dependencies); var unicodeAttributeConvention = new UnicodeAttributeConvention(Dependencies); var precisionAttributeConvention = new PrecisionAttributeConvention(Dependencies); + var deleteBehaviorAttributeConvention = new DeleteBehaviorAttributeConvention(Dependencies); conventionSet.PropertyAddedConventions.Add(backingFieldAttributeConvention); conventionSet.PropertyAddedConventions.Add(backingFieldConvention); @@ -129,6 +130,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.PropertyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.PropertyAddedConventions.Add(unicodeAttributeConvention); conventionSet.PropertyAddedConventions.Add(precisionAttributeConvention); + conventionSet.PropertyAddedConventions.Add(deleteBehaviorAttributeConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.EntityTypePrimaryKeyChangedConventions.Add(valueGeneratorConvention); @@ -141,7 +143,6 @@ public virtual ConventionSet CreateConventionSet() conventionSet.KeyRemovedConventions.Add(keyDiscoveryConvention); var cascadeDeleteConvention = new CascadeDeleteConvention(Dependencies); - var deleteBehaviorAttributeConvention = new DeleteBehaviorAttributeConvention(Dependencies); conventionSet.ForeignKeyAddedConventions.Add(foreignKeyAttributeConvention); conventionSet.ForeignKeyAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 9e1024ec553..195f0e47ef8 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -566,6 +566,12 @@ public static string DbSetIncorrectGenericType(object? entityType, object? entit GetString("DbSetIncorrectGenericType", nameof(entityType), nameof(entityClrType), nameof(genericType)), entityType, entityClrType, genericType); + /// + /// The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. + /// + public static string DeleteBehaviorAttributeNotOnNavigationProperty + => GetString("DeleteBehaviorAttributeNotOnNavigationProperty"); + /// /// You are configuring a relationship between '{dependentEntityType}' and '{principalEntityType}' but have specified a foreign key on '{entityType}'. The foreign key must be defined on a type that is part of the relationship. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 4578370aa47..06f3790bd80 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1475,4 +1475,7 @@ Cannot start tracking the entry for entity type '{entityType}' because it was created by a different StateManager instance. + + The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. + \ No newline at end of file From 599f191603a5ec6cd03107f167c2fe5e6f4c9334 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Thu, 24 Mar 2022 22:14:01 +0100 Subject: [PATCH 22/33] Add tests for when Attribute is set on different property than navigation property --- .../DeleteBehaviorAttributeConventionTest.cs | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index d67dc5bc906..e2b2b3ea7e3 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -85,6 +85,32 @@ public void Correctly_set_delete_behavior_on_two_different_foreign_keys() Assert.Equal(DeleteBehavior.Cascade, fk_Two.DeleteBehavior); } + [ConditionalFact] + public void Throw_InvalidOperationException_if_attribute_was_set_on_one_of_foreign_keys_properties() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty, + Assert.Throws( + () => modelBuilder.Entity() + .Property(e => e.Blog_On_FK_PropertyId)).Message + ); + } + + [ConditionalFact] + public void Throw_InvalidOperationException_if_attribute_was_set_on_random_property() + { + var modelBuilder = CreateModelBuilder(); + + Assert.Equal( + CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty, + Assert.Throws( + () => modelBuilder.Entity() + .Property(e => e.Blog_On_FK_PropertyId)).Message + ); + } + #region DeleteBehaviorAttribute not set private class Blog { @@ -120,7 +146,7 @@ private class Post_Restrict public int? BlogId { get; set; } } #endregion - #region DeleteBehaviourAttribute set on compound key + #region DeleteBehaviourAttribute set on compound key private class Blog_Compound { [Key] @@ -177,6 +203,42 @@ private class Post_Both public int? Blog_TwoId { get; set; } } #endregion + #region DeleteBehaviourAttribute set on one of foreign key's properties + private class Blog_On_FK_Property + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_On_FK_Property + { + public int Id { get; set; } + + public Blog_On_FK_Property Blog_On_FK_Property { get; set; } + + [DeleteBehavior(DeleteBehavior.Restrict)] + public int? Blog_On_FK_PropertyId { get; set; } + } + #endregion + #region DeleteBehaviourAttribute set on random property + private class Blog_On_Property + { + public int Id { get; set; } + + public ICollection Posts { get; set; } + } + + private class Post_On_Property + { + [DeleteBehavior(DeleteBehavior.Restrict)] + public int Id { get; set; } + + public Blog_On_Property Blog_On_Property { get; set; } + + public int? Blog_On_PropertyId { get; set; } + } + #endregion private static ModelBuilder CreateModelBuilder() => InMemoryTestHelpers.Instance.CreateConventionBuilder(); From 4c71154e516be35a63576f2c9582200d415600c4 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 20 Apr 2022 19:07:48 +0200 Subject: [PATCH 23/33] Make sure attribute is set only from dependent side --- .../DeleteBehaviorAttributeConvention.cs | 15 ++++++-- src/EFCore/Properties/CoreStrings.Designer.cs | 6 ++++ src/EFCore/Properties/CoreStrings.resx | 3 ++ .../DeleteBehaviorAttributeConventionTest.cs | 36 +++++++++++++++++-- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index fc82d3beb05..779b4c17795 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Metadata.Internal; + namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// @@ -48,14 +50,21 @@ protected override void ProcessPropertyAdded( public virtual void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext context) { var foreignKey = foreignKeyBuilder.Metadata; - var navigationProperty = foreignKey.GetNavigation(true); + var dependentSideNavigation = foreignKey.GetNavigation(true); + var principalSideNavigation = foreignKey.GetNavigation(false); - if (navigationProperty == null) + if (dependentSideNavigation == null || principalSideNavigation == null) { return; } + + var principalNavAttribute = principalSideNavigation.PropertyInfo?.GetCustomAttribute(); + if (principalNavAttribute != null) + { + throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty); + } - var navigationAttribute = navigationProperty.PropertyInfo?.GetCustomAttribute(); + var navigationAttribute = dependentSideNavigation.PropertyInfo?.GetCustomAttribute(); if (navigationAttribute != null) { foreignKey.SetDeleteBehavior(navigationAttribute.Behavior); diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 195f0e47ef8..667ede0ee72 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -572,6 +572,12 @@ public static string DbSetIncorrectGenericType(object? entityType, object? entit public static string DeleteBehaviorAttributeNotOnNavigationProperty => GetString("DeleteBehaviorAttributeNotOnNavigationProperty"); + /// + /// The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. + /// + public static string DeleteBehaviorAttributeOnPrincipalProperty + => GetString("DeleteBehaviorAttributeOnPrincipalProperty"); + /// /// You are configuring a relationship between '{dependentEntityType}' and '{principalEntityType}' but have specified a foreign key on '{entityType}'. The foreign key must be defined on a type that is part of the relationship. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 06f3790bd80..a81c07fd68d 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1478,4 +1478,7 @@ The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. + + The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. + \ No newline at end of file diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index e2b2b3ea7e3..02c88739741 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -106,8 +106,22 @@ public void Throw_InvalidOperationException_if_attribute_was_set_on_random_prope Assert.Equal( CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty, Assert.Throws( - () => modelBuilder.Entity() - .Property(e => e.Blog_On_FK_PropertyId)).Message + () => modelBuilder.Entity() + .Property(e => e.Blog_On_PropertyId)).Message + ); + } + + [ConditionalFact] + public void Throw_InvalidOperationException_if_attribute_was_set_on_principal_navigation_property() + { + var modelBuilder = CreateModelBuilder(); + + + Assert.Equal( + CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty, + Assert.Throws(() => + modelBuilder.Entity() + .Property(e => e.Blog_On_PrincipalId)).Message ); } @@ -239,6 +253,24 @@ private class Post_On_Property public int? Blog_On_PropertyId { get; set; } } #endregion + #region DeleteBehaviourAttribute set on principal navigation property + private class Blog_On_Principal + { + public int Id { get; set; } + + [DeleteBehavior(DeleteBehavior.Restrict)] + public ICollection Posts { get; set; } + } + + private class Post_On_Principal + { + public int Id { get; set; } + + public Blog_On_Principal Blog_On_Principal { get; set; } + + public int? Blog_On_PrincipalId { get; set; } + } + #endregion private static ModelBuilder CreateModelBuilder() => InMemoryTestHelpers.Instance.CreateConventionBuilder(); From 195673a822f0b748057397ba337282c542dafa1a Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 20 Apr 2022 19:08:06 +0200 Subject: [PATCH 24/33] Make xml comments docs more readable --- src/EFCore.Abstractions/DeleteBehavior.cs | 62 ++++++++--------------- 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/src/EFCore.Abstractions/DeleteBehavior.cs b/src/EFCore.Abstractions/DeleteBehavior.cs index 6ad8603d33d..ecbd5eb6564 100644 --- a/src/EFCore.Abstractions/DeleteBehavior.cs +++ b/src/EFCore.Abstractions/DeleteBehavior.cs @@ -11,14 +11,11 @@ namespace Microsoft.EntityFrameworkCore; /// /// Behaviors in the database are dependent on the database schema being created /// appropriately. Using Entity Framework Migrations or -/// -/// EnsureCreated() -/// will create the appropriate schema. +/// EnsureCreated() will create the appropriate schema. /// /// /// Note that the in-memory behavior for entities that are currently tracked by -/// the DbContext -/// can be different from the behavior that happens in the database. +/// the context can be different from the behavior that happens in the database. /// /// /// See Cascade delete and deleting orphans in EF Core for more information and @@ -28,20 +25,17 @@ namespace Microsoft.EntityFrameworkCore; public enum DeleteBehavior { /// - /// For entities being tracked by the - /// DbContext, the values of foreign key properties in + /// For entities being tracked by the context, the values of foreign key properties in /// dependent entities are set to null when the related principal is deleted. /// This helps keep the graph of entities in a consistent state while they are being tracked, such that a /// fully consistent graph can then be written to the database. If a property cannot be set to null because /// it is not a nullable type, then an exception will be thrown when - /// - /// SaveChanges() is called. + /// SaveChanges() is called. /// /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// - /// EnsureCreated() method, then the behavior in the database + /// EnsureCreated() method, then the behavior in the database /// is to generate an error if a foreign key constraint is violated. /// /// @@ -52,35 +46,30 @@ public enum DeleteBehavior ClientSetNull, /// - /// For entities being tracked by the - /// DbContext, the values of foreign key properties in dependent entities are set to null when the related principal is deleted. + /// For entities being tracked by the context, the values of foreign key properties in dependent entities + /// are set to null when the related principal is deleted. /// This helps keep the graph of entities in a consistent state while they are being tracked, such that a /// fully consistent graph can then be written to the database. If a property cannot be set to null because /// it is not a nullable type, then an exception will be thrown when - /// - /// SaveChanges() is called. + /// SaveChanges() is called. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// - /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. + /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. /// Restrict, /// - /// For entities being tracked by the - /// DbContext, the values of foreign key properties in + /// For entities being tracked by the context, the values of foreign key properties in /// dependent entities are set to null when the related principal is deleted. /// This helps keep the graph of entities in a consistent state while they are being tracked, such that a /// fully consistent graph can then be written to the database. If a property cannot be set to null because /// it is not a nullable type, then an exception will be thrown when - /// - /// SaveChanges() is called. + /// SaveChanges() is called. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// - /// EnsureCreated() method, then the behavior in the database is + /// EnsureCreated() method, then the behavior in the database is /// the same as is described above for tracked entities. Keep in mind that some databases cannot easily /// support this behavior, especially if there are cycles in relationships, in which case it may /// be better to use which will allow EF to cascade null values @@ -89,15 +78,13 @@ public enum DeleteBehavior SetNull, /// - /// For entities being tracked by the - /// DbContext, dependent entities + /// For entities being tracked by the context, dependent entities /// will be deleted when the related principal is deleted. /// /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// - /// EnsureCreated() method, then the behavior in the database is + /// EnsureCreated() method, then the behavior in the database is /// the same as is described above for tracked entities. Keep in mind that some databases cannot easily /// support this behavior, especially if there are cycles in relationships, in which case it may /// be better to use which will allow EF to perform cascade deletes @@ -111,30 +98,25 @@ public enum DeleteBehavior Cascade, /// - /// For entities being tracked by the - /// DbContext, dependent entities + /// For entities being tracked by the context, dependent entities /// will be deleted when the related principal is deleted. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// - /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. + /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. /// ClientCascade, /// - /// For entities being tracked by the - /// DbContext, the values of foreign key properties in dependent entities are set to null when the related principal is deleted. + /// For entities being tracked by the context, the values of foreign key properties in dependent entities are set to null when the related principal is deleted. /// This helps keep the graph of entities in a consistent state while they are being tracked, such that a /// fully consistent graph can then be written to the database. If a property cannot be set to null because /// it is not a nullable type, then an exception will be thrown when - /// - /// SaveChanges() is called. + /// SaveChanges() is called. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// - /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. + /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. /// NoAction, @@ -144,15 +126,13 @@ public enum DeleteBehavior /// /// /// - /// For entities being tracked by the - /// DbContext, the values of foreign key properties in dependent entities are not changed when the related principal entity is deleted. + /// For entities being tracked by the context, the values of foreign key properties in dependent entities are not changed when the related principal entity is deleted. /// This can result in an inconsistent graph of entities where the values of foreign key properties do /// not match the relationships in the graph. /// /// /// If the database has been created from the model using Entity Framework Migrations or the - /// - /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. + /// EnsureCreated() method, then the behavior in the database is to generate an error if a foreign key constraint is violated. /// /// ClientNoAction From a92c66a907a1b576121422913b5e716f72baedcf Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 20 Apr 2022 20:08:59 +0200 Subject: [PATCH 25/33] Simplify the convention --- .../DeleteBehaviorAttributeConvention.cs | 45 +++++-------------- .../ProviderConventionSetBuilder.cs | 2 +- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index 779b4c17795..73f6109dbb2 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// /// See Model building conventions for more information and examples. /// -public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase, IForeignKeyAddedConvention +public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase, INavigationAddedConvention { /// /// Creates a new instance of . @@ -33,51 +33,28 @@ protected override void ProcessPropertyAdded( MemberInfo clrMember, IConventionContext context) { - var isForeignKey = propertyBuilder.Metadata.IsForeignKey(); - if (isForeignKey) - { - return; - } - throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty); } /// - /// Called after a foreign key is added to the entity type. + /// Called after a navigation is added to the entity type. /// - /// The builder for the foreign key. + /// The builder for the navigation. /// Additional information associated with convention execution. - public virtual void ProcessForeignKeyAdded(IConventionForeignKeyBuilder foreignKeyBuilder, IConventionContext context) + public void ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilder, IConventionContext context) { - var foreignKey = foreignKeyBuilder.Metadata; - var dependentSideNavigation = foreignKey.GetNavigation(true); - var principalSideNavigation = foreignKey.GetNavigation(false); - - if (dependentSideNavigation == null || principalSideNavigation == null) + var navAttribute = navigationBuilder.Metadata.PropertyInfo?.GetCustomAttribute(); + if (navAttribute == null) { return; } - var principalNavAttribute = principalSideNavigation.PropertyInfo?.GetCustomAttribute(); - if (principalNavAttribute != null) + if (!navigationBuilder.Metadata.IsOnDependent) { - throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty); - } - - var navigationAttribute = dependentSideNavigation.PropertyInfo?.GetCustomAttribute(); - if (navigationAttribute != null) - { - foreignKey.SetDeleteBehavior(navigationAttribute.Behavior); - } - - var backingProperties = foreignKey.Properties; - foreach (var property in backingProperties) - { - var attribute = property.PropertyInfo?.GetCustomAttribute(); - if (attribute != null) - { - throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty); - } + throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty); } + + var foreignKey = navigationBuilder.Metadata.ForeignKey; + foreignKey.SetDeleteBehavior(navAttribute.Behavior); } } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 87ef6ad7694..a41c2b8957f 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -150,7 +150,6 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ForeignKeyAddedConventions.Add(valueGeneratorConvention); conventionSet.ForeignKeyAddedConventions.Add(cascadeDeleteConvention); conventionSet.ForeignKeyAddedConventions.Add(foreignKeyIndexConvention); - conventionSet.ForeignKeyAddedConventions.Add(deleteBehaviorAttributeConvention); conventionSet.ForeignKeyRemovedConventions.Add(baseTypeDiscoveryConvention); conventionSet.ForeignKeyRemovedConventions.Add(relationshipDiscoveryConvention); @@ -187,6 +186,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.NavigationAddedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.NavigationAddedConventions.Add(relationshipDiscoveryConvention); conventionSet.NavigationAddedConventions.Add(foreignKeyAttributeConvention); + conventionSet.NavigationAddedConventions.Add(deleteBehaviorAttributeConvention); var manyToManyJoinEntityTypeConvention = new ManyToManyJoinEntityTypeConvention(Dependencies); conventionSet.SkipNavigationAddedConventions.Add(new NavigationBackingFieldAttributeConvention(Dependencies)); From 5357529488079777852ce7f62e38768e91b710f5 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Thu, 21 Apr 2022 19:04:20 +0200 Subject: [PATCH 26/33] Add navigation attribute convention tests and adjust code to work correctly --- .../DeleteBehaviorAttributeConvention.cs | 5 +- .../NavigationAttributeConventionTest.cs | 64 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index 73f6109dbb2..035fb41892d 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -55,6 +55,9 @@ public void ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilde } var foreignKey = navigationBuilder.Metadata.ForeignKey; - foreignKey.SetDeleteBehavior(navAttribute.Behavior); + if (foreignKey.GetDeleteBehaviorConfigurationSource() != ConfigurationSource.Explicit) + { + foreignKey.SetDeleteBehavior(navAttribute.Behavior); + } } } diff --git a/test/EFCore.Tests/Metadata/Conventions/NavigationAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/NavigationAttributeConventionTest.cs index 1fdcb16ce9e..126640c8873 100644 --- a/test/EFCore.Tests/Metadata/Conventions/NavigationAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/NavigationAttributeConventionTest.cs @@ -889,6 +889,7 @@ private void RunRequiredNavigationAttributeConvention(InternalForeignKeyBuilder new RequiredNavigationAttributeConvention(dependencies) .ProcessNavigationAdded(navigation.Builder, context); + } private void RunNavigationBackingFieldAttributeConvention( @@ -967,6 +968,68 @@ public void BackingFieldAttribute_does_not_override_configuration_from_explicit_ #endregion + #region DeleteBehaviorAttribute + [ConditionalFact] + public void DeleteBehaviorAttribute_overrides_configuration_from_convention_source() + { + var dependentEntityTypeBuilder = CreateInternalEntityTypeBuilder(); + var principalEntityTypeBuilder = + dependentEntityTypeBuilder.ModelBuilder.Entity( + typeof(Principal), ConfigurationSource.Convention); + + var relationshipBuilder = dependentEntityTypeBuilder.HasRelationship( + principalEntityTypeBuilder.Metadata, + nameof(Dependent.Principal), + nameof(Principal.Dependents), + ConfigurationSource.Convention); + + var navigationBuilder = relationshipBuilder.Metadata.DependentToPrincipal.Builder; + var foreignKey = navigationBuilder.Metadata.ForeignKey; + foreignKey.SetDeleteBehavior(DeleteBehavior.NoAction, ConfigurationSource.Convention); + + RunDeleteBehaviorAttributeConvention(relationshipBuilder, navigationBuilder); + + Assert.Equal(DeleteBehavior.Restrict, foreignKey.DeleteBehavior); + } + + [ConditionalFact] + public void DeleteBehaviorAttribute_does_not_override_configuration_from_explicit_source() + { + var dependentEntityTypeBuilder = CreateInternalEntityTypeBuilder(); + var principalEntityTypeBuilder = + dependentEntityTypeBuilder.ModelBuilder.Entity( + typeof(Principal), ConfigurationSource.Convention); + + var relationshipBuilder = dependentEntityTypeBuilder.HasRelationship( + principalEntityTypeBuilder.Metadata, + nameof(Dependent.Principal), + nameof(Principal.Dependents), + ConfigurationSource.Convention); + + var navigationBuilder = relationshipBuilder.Metadata.DependentToPrincipal.Builder; + var foreignKey = navigationBuilder.Metadata.ForeignKey; + foreignKey.SetDeleteBehavior(DeleteBehavior.NoAction, ConfigurationSource.Explicit); + + RunDeleteBehaviorAttributeConvention(relationshipBuilder, navigationBuilder); + + Assert.Equal(DeleteBehavior.NoAction, foreignKey.DeleteBehavior); + } + + + private void RunDeleteBehaviorAttributeConvention( + InternalForeignKeyBuilder relationshipBuilder, + InternalNavigationBuilder navigationBuilder + ) + { + var dependencies = CreateDependencies(); + var context = new ConventionContext( + relationshipBuilder.Metadata.DeclaringEntityType.Model.ConventionDispatcher); + + new DeleteBehaviorAttributeConvention(dependencies) + .ProcessNavigationAdded(navigationBuilder, context); + } + #endregion + [ConditionalFact] public void Navigation_attribute_convention_runs_for_private_property() { @@ -1093,6 +1156,7 @@ private class Dependent [ForeignKey("PrincipalFk")] [InverseProperty("Dependent")] + [DeleteBehavior(DeleteBehavior.Restrict)] public Principal Principal { get; set; } public Principal AnotherPrincipal { get; set; } From 8d9d18936237983eaf2497368f855d3fcbbba5de Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Sun, 15 May 2022 20:16:10 +0200 Subject: [PATCH 27/33] Implement 1:1 relations delete behavior setting and restriction to the dependent side --- .../DeleteBehaviorAttributeConvention.cs | 67 ++++++++++++++++++- .../ProviderConventionSetBuilder.cs | 2 + 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index 035fb41892d..1ca8ce25c83 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// /// See Model building conventions for more information and examples. /// -public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase, INavigationAddedConvention +public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase, INavigationAddedConvention, IForeignKeyPrincipalEndChangedConvention, IModelFinalizingConvention { /// /// Creates a new instance of . @@ -55,9 +55,74 @@ public void ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilde } var foreignKey = navigationBuilder.Metadata.ForeignKey; + if (foreignKey.IsUnique) + { + return; + } + if (foreignKey.GetDeleteBehaviorConfigurationSource() != ConfigurationSource.Explicit) { foreignKey.SetDeleteBehavior(navAttribute.Behavior); } } + + /// + /// Called after the principal end of a foreign key is changed. + /// + /// The builder for the foreign key. + /// Additional information associated with convention execution. + public void ProcessForeignKeyPrincipalEndChanged(IConventionForeignKeyBuilder relationshipBuilder, IConventionContext context) + { + if (!relationshipBuilder.Metadata.IsUnique) + { + return; + } + + var navigation = relationshipBuilder.Metadata.DependentToPrincipal; + if (navigation == null) + { + return; + } + + var navAttribute = navigation.PropertyInfo?.GetCustomAttribute(); + if (navAttribute == null) + { + return; + } + + if (relationshipBuilder.Metadata.GetDeleteBehaviorConfigurationSource() != ConfigurationSource.Explicit) + { + relationshipBuilder.Metadata.SetDeleteBehavior(navAttribute.Behavior); + } + } + + /// + /// Called when a model is being finalized. + /// + /// The builder for the model. + /// Additional information associated with convention execution. + public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + foreach (var navigation in entityType.GetNavigations()) + { + if (!navigation.ForeignKey.IsUnique) + { + return; + } + + var navAttribute = navigation.PropertyInfo?.GetCustomAttribute(); + if (navAttribute == null) + { + return; + } + + if (!navigation.IsOnDependent) + { + throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty); + } + } + } + } } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index a41c2b8957f..130a835244a 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -214,6 +214,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ForeignKeyPrincipalEndChangedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.ForeignKeyPrincipalEndChangedConventions.Add(requiredNavigationAttributeConvention); conventionSet.ForeignKeyPrincipalEndChangedConventions.Add(nonNullableNavigationConvention); + conventionSet.ForeignKeyPrincipalEndChangedConventions.Add(deleteBehaviorAttributeConvention); conventionSet.PropertyNullabilityChangedConventions.Add(foreignKeyPropertyDiscoveryConvention); @@ -242,6 +243,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions.Add(new QueryFilterRewritingConvention(Dependencies)); conventionSet.ModelFinalizingConventions.Add(inversePropertyAttributeConvention); conventionSet.ModelFinalizingConventions.Add(backingFieldConvention); + conventionSet.ModelFinalizingConventions.Add(deleteBehaviorAttributeConvention); conventionSet.ModelFinalizedConventions.Add(new RuntimeModelConvention(Dependencies)); From 3a1da1d8391fcee610cce6a3f6e952972c8b65a3 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Tue, 17 May 2022 22:56:15 +0200 Subject: [PATCH 28/33] Add 1:1 relationship tests and implement suggestions from PR (cherry picked from commit d7ef01192d41e5e45ffdfa837f318b2e186c9a85) (cherry picked from commit 958f49613ac7d0ed1918620a3d6fb1b1ae81b156) --- .../DeleteBehaviorAttributeConvention.cs | 34 ++++----------- .../DeleteBehaviorAttributeConventionTest.cs | 41 ++++++++++++++++++- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index 1ca8ce25c83..c721e6be012 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; @@ -49,21 +52,13 @@ public void ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilde return; } - if (!navigationBuilder.Metadata.IsOnDependent) - { - throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty); - } - var foreignKey = navigationBuilder.Metadata.ForeignKey; - if (foreignKey.IsUnique) + if (!navigationBuilder.Metadata.IsOnDependent && foreignKey.IsUnique) { return; } - - if (foreignKey.GetDeleteBehaviorConfigurationSource() != ConfigurationSource.Explicit) - { - foreignKey.SetDeleteBehavior(navAttribute.Behavior); - } + + foreignKey.Builder.OnDelete(navAttribute.Behavior, fromDataAnnotation: true); } /// @@ -79,21 +74,13 @@ public void ProcessForeignKeyPrincipalEndChanged(IConventionForeignKeyBuilder re } var navigation = relationshipBuilder.Metadata.DependentToPrincipal; - if (navigation == null) - { - return; - } - - var navAttribute = navigation.PropertyInfo?.GetCustomAttribute(); + var navAttribute = navigation?.PropertyInfo?.GetCustomAttribute(); if (navAttribute == null) { return; } - if (relationshipBuilder.Metadata.GetDeleteBehaviorConfigurationSource() != ConfigurationSource.Explicit) - { - relationshipBuilder.Metadata.SetDeleteBehavior(navAttribute.Behavior); - } + relationshipBuilder.OnDelete(navAttribute.Behavior, fromDataAnnotation: true); } /// @@ -107,11 +94,6 @@ public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConven { foreach (var navigation in entityType.GetNavigations()) { - if (!navigation.ForeignKey.IsUnique) - { - return; - } - var navAttribute = navigation.PropertyInfo?.GetCustomAttribute(); if (navAttribute == null) { diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index 02c88739741..edcaf6b3583 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -116,15 +116,32 @@ public void Throw_InvalidOperationException_if_attribute_was_set_on_principal_na { var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity() + .Property(e => e.Blog_On_PrincipalId); Assert.Equal( CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty, Assert.Throws(() => - modelBuilder.Entity() - .Property(e => e.Blog_On_PrincipalId)).Message + modelBuilder.FinalizeModel()).Message ); } + [ConditionalFact] + public void Throw_InvalidOperationException_if_attribute_was_set_on_principal_one_to_one_relationship() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Property(e => e.Blog_On_PrincipalId); + + Assert.Equal( + CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty, + Assert.Throws(() => + modelBuilder.FinalizeModel()).Message + ); + } + + #region DeleteBehaviorAttribute not set private class Blog { @@ -272,6 +289,26 @@ private class Post_On_Principal } #endregion + #region DeleteBehaviourAttribute set on principal 1:1 relationship + private class Blog_On_Principal_OneToOne + { + public int Id { get; set; } + + [DeleteBehavior(DeleteBehavior.Restrict)] + public Post_On_Principal_OneToOne Post_On_Principal_OneToOne { get; set; } + } + + private class Post_On_Principal_OneToOne + { + public int Id { get; set; } + + public Blog_On_Principal_OneToOne Blog_On_Principal_OneToOne { get; set; } + + public int? Blog_On_PrincipalId { get; set; } + } + #endregion + + private static ModelBuilder CreateModelBuilder() => InMemoryTestHelpers.Instance.CreateConventionBuilder(); } From 28b09bba5a5f2d03b7af4207f72bc628111d3a18 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 18 May 2022 18:44:34 +0200 Subject: [PATCH 29/33] Add stylistic changes suggested by PR and IDE --- .../DeleteBehaviorAttribute.cs | 9 ++-- .../DeleteBehaviorAttributeConvention.cs | 43 ++++++++++--------- .../DeleteBehaviorAttributeConventionTest.cs | 17 +++----- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs index 8315135ca10..56a6757c8f8 100644 --- a/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs +++ b/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs @@ -4,7 +4,8 @@ namespace Microsoft.EntityFrameworkCore; /// -/// Configures the property or field to indicate how a delete operation is applied to dependent entities +/// Configures the navigation property on the dependent side of a relationship +/// to indicate how a delete operation is applied to dependent entities /// in a relationship when it is deleted or the relationship is severed. /// /// @@ -19,11 +20,11 @@ public sealed class DeleteBehaviorAttribute : Attribute /// The to be configured. public DeleteBehaviorAttribute(DeleteBehavior behavior) { - Behavior = behavior; + this.Behavior = behavior; } - + /// - /// The to be configured. + /// Gets the to be configured. /// public DeleteBehavior Behavior { get; } } diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index c721e6be012..449e100b7b4 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Internal; - -namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// /// A convention that configures the delete behavior based on the applied on the property. @@ -17,26 +16,12 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; public class DeleteBehaviorAttributeConvention : PropertyAttributeConventionBase, INavigationAddedConvention, IForeignKeyPrincipalEndChangedConvention, IModelFinalizingConvention { /// - /// Creates a new instance of . + /// Initializes a new instance of the class. /// /// Parameter object containing dependencies for this convention. public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencies dependencies) - : base(dependencies) { } - - /// - /// Called after a property is added to the entity type with an attribute on the associated CLR property or field. - /// - /// The builder for the property. - /// The attribute. - /// The member that has the attribute. - /// Additional information associated with convention execution. - protected override void ProcessPropertyAdded( - IConventionPropertyBuilder propertyBuilder, - DeleteBehaviorAttribute attribute, - MemberInfo clrMember, - IConventionContext context) + : base(dependencies) { - throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty); } /// @@ -79,7 +64,7 @@ public void ProcessForeignKeyPrincipalEndChanged(IConventionForeignKeyBuilder re { return; } - + relationshipBuilder.OnDelete(navAttribute.Behavior, fromDataAnnotation: true); } @@ -99,7 +84,7 @@ public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConven { return; } - + if (!navigation.IsOnDependent) { throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty); @@ -107,4 +92,20 @@ public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConven } } } + + /// + /// Called after a property is added to the entity type with an attribute on the associated CLR property or field. + /// + /// The builder for the property. + /// The attribute. + /// The member that has the attribute. + /// Additional information associated with convention execution. + protected override void ProcessPropertyAdded( + IConventionPropertyBuilder propertyBuilder, + DeleteBehaviorAttribute attribute, + MemberInfo clrMember, + IConventionContext context) + { + throw new InvalidOperationException(CoreStrings.DeleteBehaviorAttributeNotOnNavigationProperty); + } } diff --git a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs index edcaf6b3583..59ee4cbc1de 100644 --- a/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/DeleteBehaviorAttributeConventionTest.cs @@ -1,19 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - - // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable UnusedMember.Local // ReSharper disable ClassNeverInstantiated.Local // ReSharper disable CollectionNeverUpdated.Local // ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; - public class DeleteBehaviorAttributeConventionTest { [ConditionalFact] @@ -121,8 +118,7 @@ public void Throw_InvalidOperationException_if_attribute_was_set_on_principal_na Assert.Equal( CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty, - Assert.Throws(() => - modelBuilder.FinalizeModel()).Message + Assert.Throws(() => modelBuilder.FinalizeModel()).Message ); } @@ -136,11 +132,12 @@ public void Throw_InvalidOperationException_if_attribute_was_set_on_principal_on Assert.Equal( CoreStrings.DeleteBehaviorAttributeOnPrincipalProperty, - Assert.Throws(() => - modelBuilder.FinalizeModel()).Message + Assert.Throws(() => modelBuilder.FinalizeModel()).Message ); } + private static ModelBuilder CreateModelBuilder() + => InMemoryTestHelpers.Instance.CreateConventionBuilder(); #region DeleteBehaviorAttribute not set private class Blog @@ -307,8 +304,4 @@ private class Post_On_Principal_OneToOne public int? Blog_On_PrincipalId { get; set; } } #endregion - - - private static ModelBuilder CreateModelBuilder() - => InMemoryTestHelpers.Instance.CreateConventionBuilder(); } From 665fdd81662df0374cfbf76908672ea6c272bde8 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 18 May 2022 18:53:56 +0200 Subject: [PATCH 30/33] Fix CoreStrings.resx merge conflicts --- src/EFCore/Properties/CoreStrings.Designer.cs | 48 ++++++++---- src/EFCore/Properties/CoreStrings.resx | 76 ++++++++++--------- 2 files changed, 74 insertions(+), 50 deletions(-) diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 667ede0ee72..967f3bddca6 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -433,7 +433,7 @@ public static string CompositeFkOnProperty(object? navigation, object? entityTyp navigation, entityType); /// - /// The entity type '{entityType}' has multiple properties with the [Key] attribute. Composite primary keys can only be set using 'HasKey' in 'OnModelCreating'. + /// The entity type '{entityType}' has multiple properties with the [Key] attribute. Composite primary keys configured by placing the [PrimaryKey] attribute on the entity type class, or by using 'HasKey' in 'OnModelCreating'. /// public static string CompositePKWithDataAnnotation(object? entityType) => string.Format( @@ -462,6 +462,14 @@ public static string ConflictingForeignKeyAttributes(object? propertyList, objec GetString("ConflictingForeignKeyAttributes", nameof(propertyList), nameof(entityType), nameof(principalEntityType)), propertyList, entityType, principalEntityType); + /// + /// The entity type '{entity}' has both [Keyless] and [PrimaryKey] attributes; one must be removed. + /// + public static string ConflictingKeylessAndPrimaryKeyAttributes(object? entity) + => string.Format( + GetString("ConflictingKeylessAndPrimaryKeyAttributes", nameof(entity)), + entity); + /// /// The property or navigation '{member}' cannot be added to the entity type '{entityType}' because a property or navigation with the same name already exists on entity type '{conflictingEntityType}'. /// @@ -572,12 +580,6 @@ public static string DbSetIncorrectGenericType(object? entityType, object? entit public static string DeleteBehaviorAttributeNotOnNavigationProperty => GetString("DeleteBehaviorAttributeNotOnNavigationProperty"); - /// - /// The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. - /// - public static string DeleteBehaviorAttributeOnPrincipalProperty - => GetString("DeleteBehaviorAttributeOnPrincipalProperty"); - /// /// You are configuring a relationship between '{dependentEntityType}' and '{principalEntityType}' but have specified a foreign key on '{entityType}'. The foreign key must be defined on a type that is part of the relationship. /// @@ -2001,6 +2003,30 @@ public static string PrincipalOwnedType(object? referencingEntityTypeOrNavigatio GetString("PrincipalOwnedType", nameof(referencingEntityTypeOrNavigation), nameof(referencedEntityTypeOrNavigation), nameof(ownedType)), referencingEntityTypeOrNavigation, referencedEntityTypeOrNavigation, ownedType); + /// + /// The derived type '{derivedType}' cannot have the [PrimaryKey] attribute since primary keys may only be declared on the root type. Move the attribute to '{rootType}', or remove '{rootType}' from the model by using [NotMapped] attribute or calling 'EntityTypeBuilder.Ignore' on the base type in 'OnModelCreating'. + /// + public static string PrimaryKeyAttributeOnDerivedEntity(object? derivedType, object? rootType) + => string.Format( + GetString("PrimaryKeyAttributeOnDerivedEntity", nameof(derivedType), nameof(rootType)), + derivedType, rootType); + + /// + /// The [PrimaryKey] attribute on the entity type '{entityType}' is invalid because the property '{propertyName}' was marked as unmapped by [NotMapped] attribute or 'Ignore()' in 'OnModelCreating'. A primary key cannot use unmapped properties. + /// + public static string PrimaryKeyDefinedOnIgnoredProperty(object? entityType, object? propertyName) + => string.Format( + GetString("PrimaryKeyDefinedOnIgnoredProperty", nameof(entityType), nameof(propertyName)), + entityType, propertyName); + + /// + /// The [PrimaryKey] attribute on the entity type '{entityType}' references properties {properties}, but no property with name '{propertyName}' exists on that entity type or any of its base types. + /// + public static string PrimaryKeyDefinedOnNonExistentProperty(object? entityType, object? properties, object? propertyName) + => string.Format( + GetString("PrimaryKeyDefinedOnNonExistentProperty", nameof(entityType), nameof(properties), nameof(propertyName)), + entityType, properties, propertyName); + /// /// '{property}' cannot be used as a property on entity type '{entityType}' because it is configured as a navigation. /// @@ -2763,14 +2789,6 @@ public static string ValueCannotBeNull(object? property, object? entityType, obj GetString("ValueCannotBeNull", "0_property", "1_entityType", nameof(propertyType)), property, entityType, propertyType); - /// - /// Value generation is not supported for property '{entityType}.{property}' because it has a '{converter}' converter configured. Configure the property to not use value generation using 'ValueGenerated.Never' or 'DatabaseGeneratedOption.None' and specify explicit values instead. - /// - public static string ValueGenWithConversion(object? entityType, object? property, object? converter) - => string.Format( - GetString("ValueGenWithConversion", nameof(entityType), nameof(property), nameof(converter)), - entityType, property, converter); - /// /// Calling '{visitMethodName}' is not allowed. Visit the expression manually for the relevant part in the visitor. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index a81c07fd68d..927ec323b02 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -271,7 +271,7 @@ There are multiple properties with the [ForeignKey] attribute pointing to navigation '{1_entityType}.{0_navigation}'. To define a composite foreign key using data annotations, use the [ForeignKey] attribute on the navigation. - The entity type '{entityType}' has multiple properties with the [Key] attribute. Composite primary keys can only be set using 'HasKey' in 'OnModelCreating'. + The entity type '{entityType}' has multiple properties with the [Key] attribute. Composite primary keys configured by placing the [PrimaryKey] attribute on the entity type class, or by using 'HasKey' in 'OnModelCreating'. A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913. @@ -282,6 +282,9 @@ There are multiple [ForeignKey] attributes which are pointing to same set of properties '{propertyList}' on entity type '{entityType}' and targeting the principal entity type '{principalEntityType}'. + + The entity type '{entity}' has both [Keyless] and [PrimaryKey] attributes; one must be removed. + The property or navigation '{member}' cannot be added to the entity type '{entityType}' because a property or navigation with the same name already exists on entity type '{conflictingEntityType}'. @@ -1163,6 +1166,15 @@ The relationship from '{referencingEntityTypeOrNavigation}' to '{referencedEntityTypeOrNavigation}' is not supported because the owned entity type '{ownedType}' cannot be on the principal side of a non-ownership relationship. Remove the relationship or configure the foreign key to be on '{ownedType}'. + + The derived type '{derivedType}' cannot have the [PrimaryKey] attribute since primary keys may only be declared on the root type. Move the attribute to '{rootType}', or remove '{rootType}' from the model by using [NotMapped] attribute or calling 'EntityTypeBuilder.Ignore' on the base type in 'OnModelCreating'. + + + The [PrimaryKey] attribute on the entity type '{entityType}' is invalid because the property '{propertyName}' was marked as unmapped by [NotMapped] attribute or 'Ignore()' in 'OnModelCreating'. A primary key cannot use unmapped properties. + + + The [PrimaryKey] attribute on the entity type '{entityType}' references properties {properties}, but no property with name '{propertyName}' exists on that entity type or any of its base types. + '{property}' cannot be used as a property on entity type '{entityType}' because it is configured as a navigation. @@ -1460,9 +1472,6 @@ The value for property '{1_entityType}.{0_property}' cannot be set to null because its type is '{propertyType}' which is not a nullable type. - - Value generation is not supported for property '{entityType}.{property}' because it has a '{converter}' converter configured. Configure the property to not use value generation using 'ValueGenerated.Never' or 'DatabaseGeneratedOption.None' and specify explicit values instead. - Calling '{visitMethodName}' is not allowed. Visit the expression manually for the relevant part in the visitor. @@ -1478,7 +1487,4 @@ The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. - - The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. - - \ No newline at end of file + From 02860a3d513d76e4818b5f1642b07d38c03a694e Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 18 May 2022 18:55:45 +0200 Subject: [PATCH 31/33] Fix CoreStrings.resx merge conflicts --- src/EFCore/Properties/CoreStrings.Designer.cs | 6 ++++++ src/EFCore/Properties/CoreStrings.resx | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 967f3bddca6..93d5ad7f43f 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -580,6 +580,12 @@ public static string DbSetIncorrectGenericType(object? entityType, object? entit public static string DeleteBehaviorAttributeNotOnNavigationProperty => GetString("DeleteBehaviorAttributeNotOnNavigationProperty"); + /// + /// The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. + /// + public static string DeleteBehaviorAttributeOnPrincipalProperty + => GetString("DeleteBehaviorAttributeOnPrincipalProperty"); + /// /// You are configuring a relationship between '{dependentEntityType}' and '{principalEntityType}' but have specified a foreign key on '{entityType}'. The foreign key must be defined on a type that is part of the relationship. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 927ec323b02..68542cb9df0 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1487,4 +1487,7 @@ The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. + + The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. + From 6fc84c14d6ee583336acb82b057143ffb6f22ea9 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 18 May 2022 20:26:39 +0200 Subject: [PATCH 32/33] Run Custom Tool on CoreStrings.Designer.tt --- src/EFCore/Properties/CoreStrings.Designer.cs | 52 +++++------ src/EFCore/Properties/CoreStrings.resx | 86 +++++++++---------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 93d5ad7f43f..6cf71024d68 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -579,13 +579,13 @@ public static string DbSetIncorrectGenericType(object? entityType, object? entit /// public static string DeleteBehaviorAttributeNotOnNavigationProperty => GetString("DeleteBehaviorAttributeNotOnNavigationProperty"); - + /// /// The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. /// public static string DeleteBehaviorAttributeOnPrincipalProperty => GetString("DeleteBehaviorAttributeOnPrincipalProperty"); - + /// /// You are configuring a relationship between '{dependentEntityType}' and '{principalEntityType}' but have specified a foreign key on '{entityType}'. The foreign key must be defined on a type that is part of the relationship. /// @@ -1977,6 +1977,30 @@ public static string PoolingContextCtorError(object? contextType) public static string PoolingOptionsModified => GetString("PoolingOptionsModified"); + /// + /// The derived type '{derivedType}' cannot have the [PrimaryKey] attribute since primary keys may only be declared on the root type. Move the attribute to '{rootType}', or remove '{rootType}' from the model by using [NotMapped] attribute or calling 'EntityTypeBuilder.Ignore' on the base type in 'OnModelCreating'. + /// + public static string PrimaryKeyAttributeOnDerivedEntity(object? derivedType, object? rootType) + => string.Format( + GetString("PrimaryKeyAttributeOnDerivedEntity", nameof(derivedType), nameof(rootType)), + derivedType, rootType); + + /// + /// The [PrimaryKey] attribute on the entity type '{entityType}' is invalid because the property '{propertyName}' was marked as unmapped by [NotMapped] attribute or 'Ignore()' in 'OnModelCreating'. A primary key cannot use unmapped properties. + /// + public static string PrimaryKeyDefinedOnIgnoredProperty(object? entityType, object? propertyName) + => string.Format( + GetString("PrimaryKeyDefinedOnIgnoredProperty", nameof(entityType), nameof(propertyName)), + entityType, propertyName); + + /// + /// The [PrimaryKey] attribute on the entity type '{entityType}' references properties {properties}, but no property with name '{propertyName}' exists on that entity type or any of its base types. + /// + public static string PrimaryKeyDefinedOnNonExistentProperty(object? entityType, object? properties, object? propertyName) + => string.Format( + GetString("PrimaryKeyDefinedOnNonExistentProperty", nameof(entityType), nameof(properties), nameof(propertyName)), + entityType, properties, propertyName); + /// /// When creating the relationship between '{navigationSpecification1}' and '{navigationSpecification2}' the entity type '{targetEntityType}' cannot be set as principal. /// @@ -2009,30 +2033,6 @@ public static string PrincipalOwnedType(object? referencingEntityTypeOrNavigatio GetString("PrincipalOwnedType", nameof(referencingEntityTypeOrNavigation), nameof(referencedEntityTypeOrNavigation), nameof(ownedType)), referencingEntityTypeOrNavigation, referencedEntityTypeOrNavigation, ownedType); - /// - /// The derived type '{derivedType}' cannot have the [PrimaryKey] attribute since primary keys may only be declared on the root type. Move the attribute to '{rootType}', or remove '{rootType}' from the model by using [NotMapped] attribute or calling 'EntityTypeBuilder.Ignore' on the base type in 'OnModelCreating'. - /// - public static string PrimaryKeyAttributeOnDerivedEntity(object? derivedType, object? rootType) - => string.Format( - GetString("PrimaryKeyAttributeOnDerivedEntity", nameof(derivedType), nameof(rootType)), - derivedType, rootType); - - /// - /// The [PrimaryKey] attribute on the entity type '{entityType}' is invalid because the property '{propertyName}' was marked as unmapped by [NotMapped] attribute or 'Ignore()' in 'OnModelCreating'. A primary key cannot use unmapped properties. - /// - public static string PrimaryKeyDefinedOnIgnoredProperty(object? entityType, object? propertyName) - => string.Format( - GetString("PrimaryKeyDefinedOnIgnoredProperty", nameof(entityType), nameof(propertyName)), - entityType, propertyName); - - /// - /// The [PrimaryKey] attribute on the entity type '{entityType}' references properties {properties}, but no property with name '{propertyName}' exists on that entity type or any of its base types. - /// - public static string PrimaryKeyDefinedOnNonExistentProperty(object? entityType, object? properties, object? propertyName) - => string.Format( - GetString("PrimaryKeyDefinedOnNonExistentProperty", nameof(entityType), nameof(properties), nameof(propertyName)), - entityType, properties, propertyName); - /// /// '{property}' cannot be used as a property on entity type '{entityType}' because it is configured as a navigation. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 68542cb9df0..e6debfc4779 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -327,6 +327,12 @@ Cannot create DbSet for entity type '{entityType}' since it is of type '{entityClrType}' but the generic type provided is of type '{genericType}'. + + The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. + + + The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. + You are configuring a relationship between '{dependentEntityType}' and '{principalEntityType}' but have specified a foreign key on '{entityType}'. The foreign key must be defined on a type that is part of the relationship. @@ -1154,6 +1160,15 @@ 'OnConfiguring' cannot be used to modify DbContextOptions when DbContext pooling is enabled. + + The derived type '{derivedType}' cannot have the [PrimaryKey] attribute since primary keys may only be declared on the root type. Move the attribute to '{rootType}', or remove '{rootType}' from the model by using [NotMapped] attribute or calling 'EntityTypeBuilder.Ignore' on the base type in 'OnModelCreating'. + + + The [PrimaryKey] attribute on the entity type '{entityType}' is invalid because the property '{propertyName}' was marked as unmapped by [NotMapped] attribute or 'Ignore()' in 'OnModelCreating'. A primary key cannot use unmapped properties. + + + The [PrimaryKey] attribute on the entity type '{entityType}' references properties {properties}, but no property with name '{propertyName}' exists on that entity type or any of its base types. + When creating the relationship between '{navigationSpecification1}' and '{navigationSpecification2}' the entity type '{targetEntityType}' cannot be set as principal. @@ -1166,15 +1181,6 @@ The relationship from '{referencingEntityTypeOrNavigation}' to '{referencedEntityTypeOrNavigation}' is not supported because the owned entity type '{ownedType}' cannot be on the principal side of a non-ownership relationship. Remove the relationship or configure the foreign key to be on '{ownedType}'. - - The derived type '{derivedType}' cannot have the [PrimaryKey] attribute since primary keys may only be declared on the root type. Move the attribute to '{rootType}', or remove '{rootType}' from the model by using [NotMapped] attribute or calling 'EntityTypeBuilder.Ignore' on the base type in 'OnModelCreating'. - - - The [PrimaryKey] attribute on the entity type '{entityType}' is invalid because the property '{propertyName}' was marked as unmapped by [NotMapped] attribute or 'Ignore()' in 'OnModelCreating'. A primary key cannot use unmapped properties. - - - The [PrimaryKey] attribute on the entity type '{entityType}' references properties {properties}, but no property with name '{propertyName}' exists on that entity type or any of its base types. - '{property}' cannot be used as a property on entity type '{entityType}' because it is configured as a navigation. @@ -1484,10 +1490,4 @@ Cannot start tracking the entry for entity type '{entityType}' because it was created by a different StateManager instance. - - The [DeleteBehavior] attribute may only be specified on navigation properties, and is not supported not on properties making up the foreign key. - - - The [DeleteBehavior] attribute may only be specified on dependent side of the relationship. - - + \ No newline at end of file From 4cb749127a0791070e8cc19144159f49539a6b56 Mon Sep 17 00:00:00 2001 From: Artur Porowski Date: Wed, 18 May 2022 20:29:36 +0200 Subject: [PATCH 33/33] Add virtual modifier to the DeleteBehaviorAttributeConvention's public APIs --- .../Conventions/DeleteBehaviorAttributeConvention.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs index 449e100b7b4..fe381319862 100644 --- a/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs +++ b/src/EFCore/Metadata/Conventions/DeleteBehaviorAttributeConvention.cs @@ -29,7 +29,7 @@ public DeleteBehaviorAttributeConvention(ProviderConventionSetBuilderDependencie /// /// The builder for the navigation. /// Additional information associated with convention execution. - public void ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilder, IConventionContext context) + public virtual void ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilder, IConventionContext context) { var navAttribute = navigationBuilder.Metadata.PropertyInfo?.GetCustomAttribute(); if (navAttribute == null) @@ -51,7 +51,7 @@ public void ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilde /// /// The builder for the foreign key. /// Additional information associated with convention execution. - public void ProcessForeignKeyPrincipalEndChanged(IConventionForeignKeyBuilder relationshipBuilder, IConventionContext context) + public virtual void ProcessForeignKeyPrincipalEndChanged(IConventionForeignKeyBuilder relationshipBuilder, IConventionContext context) { if (!relationshipBuilder.Metadata.IsUnique) { @@ -73,7 +73,7 @@ public void ProcessForeignKeyPrincipalEndChanged(IConventionForeignKeyBuilder re /// /// The builder for the model. /// Additional information associated with convention execution. - public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) { foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) {