From c73f4a301330f1c0ef7cfd5d7a8050dab775dcec Mon Sep 17 00:00:00 2001 From: Christopher Jolly Date: Wed, 1 Nov 2023 01:11:11 +0800 Subject: [PATCH] Add the onDelete convention so that we match the behaviour of sql server when configuring self-referencing skip navigations. Ends up configured as ClientCascade instead of Cascade --- .../Conventions/JetConventionSetBuilder.cs | 2 + .../Conventions/JetOnDeleteConvention.cs | 135 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 src/EFCore.Jet/Metadata/Conventions/JetOnDeleteConvention.cs diff --git a/src/EFCore.Jet/Metadata/Conventions/JetConventionSetBuilder.cs b/src/EFCore.Jet/Metadata/Conventions/JetConventionSetBuilder.cs index 8b768e4..29e155d 100644 --- a/src/EFCore.Jet/Metadata/Conventions/JetConventionSetBuilder.cs +++ b/src/EFCore.Jet/Metadata/Conventions/JetConventionSetBuilder.cs @@ -30,6 +30,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions conventionSet.Add(new RelationalMaxIdentifierLengthConvention(64, Dependencies, RelationalDependencies)); conventionSet.Add(new JetIndexConvention(Dependencies, RelationalDependencies, _sqlGenerationHelper)); + conventionSet.Replace( + new JetOnDeleteConvention(Dependencies, RelationalDependencies)); conventionSet.Replace( new JetStoreGenerationConvention(Dependencies, RelationalDependencies)); conventionSet.Replace( diff --git a/src/EFCore.Jet/Metadata/Conventions/JetOnDeleteConvention.cs b/src/EFCore.Jet/Metadata/Conventions/JetOnDeleteConvention.cs new file mode 100644 index 0000000..57b21cd --- /dev/null +++ b/src/EFCore.Jet/Metadata/Conventions/JetOnDeleteConvention.cs @@ -0,0 +1,135 @@ +// ReSharper disable once CheckNamespace + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that configures the OnDelete behavior for foreign keys on the join entity type for +/// self-referencing skip navigations +/// +public class JetOnDeleteConvention : CascadeDeleteConvention, + ISkipNavigationForeignKeyChangedConvention, + IEntityTypeAnnotationChangedConvention +{ + public JetOnDeleteConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : base(dependencies) + { + RelationalDependencies = relationalDependencies; + } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + public virtual void ProcessSkipNavigationForeignKeyChanged( + IConventionSkipNavigationBuilder skipNavigationBuilder, + IConventionForeignKey? foreignKey, + IConventionForeignKey? oldForeignKey, + IConventionContext context) + { + if (foreignKey is not null && foreignKey.IsInModel) + { + foreignKey.Builder.OnDelete(GetTargetDeleteBehavior(foreignKey)); + } + } + + /// + protected override DeleteBehavior GetTargetDeleteBehavior(IConventionForeignKey foreignKey) + { + var deleteBehavior = base.GetTargetDeleteBehavior(foreignKey); + if (deleteBehavior != DeleteBehavior.Cascade) + { + return deleteBehavior; + } + + return ProcessSkipNavigations(foreignKey.GetReferencingSkipNavigations()) ?? deleteBehavior; + } + + private DeleteBehavior? ProcessSkipNavigations(IEnumerable skipNavigations) + { + var skipNavigation = skipNavigations + .FirstOrDefault( + s => s.Inverse != null + && IsMappedToSameTable(s.DeclaringEntityType, s.TargetEntityType)); + + if (skipNavigation != null) + { + var isFirstSkipNavigation = IsFirstSkipNavigation(skipNavigation); + if (!isFirstSkipNavigation) + { + skipNavigation = skipNavigation.Inverse!; + } + + var inverseSkipNavigation = skipNavigation.Inverse!; + + var deleteBehavior = DefaultDeleteBehavior(skipNavigation); + var inverseDeleteBehavior = DefaultDeleteBehavior(inverseSkipNavigation); + + if (deleteBehavior == DeleteBehavior.Cascade + && inverseDeleteBehavior == DeleteBehavior.Cascade + && !(inverseSkipNavigation.ForeignKey!.GetDeleteBehaviorConfigurationSource() == ConfigurationSource.Explicit + && inverseSkipNavigation.ForeignKey!.DeleteBehavior != DeleteBehavior.Cascade)) + { + deleteBehavior = DeleteBehavior.ClientCascade; + } + + skipNavigation.ForeignKey!.Builder.OnDelete(deleteBehavior); + inverseSkipNavigation.ForeignKey!.Builder.OnDelete(inverseDeleteBehavior); + + return isFirstSkipNavigation ? deleteBehavior : inverseDeleteBehavior; + } + + return null; + + DeleteBehavior DefaultDeleteBehavior(IConventionSkipNavigation conventionSkipNavigation) + => conventionSkipNavigation.ForeignKey!.IsRequired ? DeleteBehavior.Cascade : DeleteBehavior.ClientSetNull; + + bool IsMappedToSameTable(IConventionEntityType entityType1, IConventionEntityType entityType2) + { + var tableName1 = entityType1.GetTableName(); + var tableName2 = entityType2.GetTableName(); + + return tableName1 != null + && tableName2 != null + && tableName1 == tableName2 + && entityType1.GetSchema() == entityType2.GetSchema(); + } + + bool IsFirstSkipNavigation(IConventionSkipNavigation navigation) + => navigation.DeclaringEntityType != navigation.TargetEntityType + ? string.Compare(navigation.DeclaringEntityType.Name, navigation.TargetEntityType.Name, StringComparison.Ordinal) < 0 + : string.Compare(navigation.Name, navigation.Inverse!.Name, StringComparison.Ordinal) < 0; + } + + /// + public virtual void ProcessEntityTypeAnnotationChanged( + IConventionEntityTypeBuilder entityTypeBuilder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation, + IConventionContext context) + { + if (name is RelationalAnnotationNames.TableName or RelationalAnnotationNames.Schema) + { + ProcessSkipNavigations(entityTypeBuilder.Metadata.GetDeclaredSkipNavigations()); + + foreach (var foreignKey in entityTypeBuilder.Metadata.GetDeclaredForeignKeys()) + { + var deleteBehavior = GetTargetDeleteBehavior(foreignKey); + if (foreignKey.DeleteBehavior != deleteBehavior) + { + foreignKey.Builder.OnDelete(deleteBehavior); + } + } + } + } +}