Skip to content

Commit 4ce3c99

Browse files
committed
Improve the relationship cycle breaking logic to detect cycles even when not starting on one.
Additionally, detect multiple relationship chains that end with incompatible conversions. Fixes #32422
1 parent 5a4ffe6 commit 4ce3c99

File tree

9 files changed

+199
-90
lines changed

9 files changed

+199
-90
lines changed

src/EFCore/Metadata/Internal/EntityType.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics.CodeAnalysis;
5-
using System.Runtime.CompilerServices;
65
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
76
using Microsoft.EntityFrameworkCore.Internal;
87

src/EFCore/Metadata/Internal/InternalForeignKeyBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3677,7 +3677,11 @@ private bool CanSetRelatedTypes(
36773677
&& (((navigationToPrincipal != null)
36783678
&& (navigationToPrincipal.Value.Name == Metadata.PrincipalToDependent?.Name))
36793679
|| ((navigationToDependent != null)
3680-
&& (navigationToDependent.Value.Name == Metadata.DependentToPrincipal?.Name)));
3680+
&& (navigationToDependent.Value.Name == Metadata.DependentToPrincipal?.Name))
3681+
|| ((navigationToPrincipal == null)
3682+
&& (navigationToDependent == null)
3683+
&& principalEntityType == Metadata.DeclaringEntityType
3684+
&& dependentEntityType == Metadata.PrincipalEntityType));
36813685

36823686
var someAspectsFitNonInverted = false;
36833687
if (!sameHierarchyInvertedNavigations

src/EFCore/Metadata/Internal/Property.cs

Lines changed: 102 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Diagnostics.CodeAnalysis;
55
using System.Globalization;
66
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
7+
using Microsoft.EntityFrameworkCore.Infrastructure;
78
using Microsoft.EntityFrameworkCore.Internal;
89
using Microsoft.EntityFrameworkCore.Storage.Json;
910

@@ -767,53 +768,10 @@ private object? DefaultSentinel
767768
public virtual ValueConverter? GetValueConverter()
768769
{
769770
var annotation = FindAnnotation(CoreAnnotationNames.ValueConverter);
770-
if (annotation != null)
771-
{
772-
return (ValueConverter?)annotation.Value;
773-
}
774-
775-
var property = this;
776-
var i = 0;
777-
for (; i < ForeignKey.LongestFkChainAllowedLength; i++)
778-
{
779-
Property? nextProperty = null;
780-
foreach (var foreignKey in property.GetContainingForeignKeys())
781-
{
782-
for (var propertyIndex = 0; propertyIndex < foreignKey.Properties.Count; propertyIndex++)
783-
{
784-
if (property == foreignKey.Properties[propertyIndex])
785-
{
786-
var principalProperty = foreignKey.PrincipalKey.Properties[propertyIndex];
787-
if (principalProperty == this
788-
|| principalProperty == property)
789-
{
790-
break;
791-
}
792-
793-
annotation = principalProperty.FindAnnotation(CoreAnnotationNames.ValueConverter);
794-
if (annotation != null)
795-
{
796-
return (ValueConverter?)annotation.Value;
797-
}
798-
799-
nextProperty = principalProperty;
800-
}
801-
}
802-
}
803-
804-
if (nextProperty == null)
805-
{
806-
break;
807-
}
808-
809-
property = nextProperty;
810-
}
811-
812-
return i == ForeignKey.LongestFkChainAllowedLength
813-
? throw new InvalidOperationException(
814-
CoreStrings.RelationshipCycle(
815-
DeclaringType.DisplayName(), Name, "ValueConverter"))
816-
: null;
771+
return annotation != null
772+
? (ValueConverter?)annotation.Value
773+
: GetConversion(throwOnProviderClrTypeConflict: FindAnnotation(CoreAnnotationNames.ProviderClrType) == null)
774+
.ValueConverter;
817775
}
818776

819777
/// <summary>
@@ -859,53 +817,120 @@ private object? DefaultSentinel
859817
public virtual Type? GetProviderClrType()
860818
{
861819
var annotation = FindAnnotation(CoreAnnotationNames.ProviderClrType);
862-
if (annotation != null)
863-
{
864-
return (Type?)annotation.Value;
865-
}
820+
return annotation != null
821+
? (Type?)annotation.Value
822+
: GetConversion(throwOnValueConverterConflict: FindAnnotation(CoreAnnotationNames.ValueConverter) == null)
823+
.ProviderClrType;
824+
}
825+
826+
private (ValueConverter? ValueConverter, Type? ProviderClrType) GetConversion(
827+
bool throwOnValueConverterConflict = true, bool throwOnProviderClrTypeConflict = true)
828+
{
829+
var queue = new Queue<(Property CurrentProperty, Property CycleBreakingPropert, int CyclePosition, int MaxCycleLength)>();
830+
queue.Enqueue((this, this, 0, 2));
866831

867-
var property = this;
868-
var i = 0;
869-
for (; i < ForeignKey.LongestFkChainAllowedLength; i++)
832+
ValueConverter? valueConverter = null;
833+
Type? providerClrType = null;
834+
while (queue.Count > 0)
870835
{
871-
Property? nextProperty = null;
836+
var (property, cycleBreakingProperty, cyclePosition, maxCycleLength) = queue.Dequeue();
837+
if (cyclePosition >= ForeignKey.LongestFkChainAllowedLength)
838+
{
839+
throw new InvalidOperationException(
840+
CoreStrings.RelationshipCycle(DeclaringType.DisplayName(), Name, "ValueConverter"));
841+
}
842+
872843
foreach (var foreignKey in property.GetContainingForeignKeys())
873844
{
874845
for (var propertyIndex = 0; propertyIndex < foreignKey.Properties.Count; propertyIndex++)
875846
{
876-
if (property == foreignKey.Properties[propertyIndex])
847+
if (property != foreignKey.Properties[propertyIndex])
848+
{
849+
continue;
850+
}
851+
852+
var principalProperty = foreignKey.PrincipalKey.Properties[propertyIndex];
853+
if (principalProperty == cycleBreakingProperty)
854+
{
855+
break;
856+
}
857+
858+
var annotationFound = false;
859+
var valueConverterAnnotation = principalProperty.FindAnnotation(CoreAnnotationNames.ValueConverter);
860+
if (valueConverterAnnotation != null)
877861
{
878-
var principalProperty = foreignKey.PrincipalKey.Properties[propertyIndex];
879-
if (principalProperty == this
880-
|| principalProperty == property)
862+
var annotationValue = (ValueConverter?)valueConverterAnnotation.Value;
863+
if (annotationValue != null)
881864
{
882-
break;
865+
if (valueConverter != null)
866+
{
867+
throw new InvalidOperationException(
868+
CoreStrings.ConflictingRelationshipConversions(
869+
DeclaringType.DisplayName(), Name,
870+
valueConverter.GetType().ShortDisplayName(), annotationValue.GetType().ShortDisplayName()));
871+
}
872+
873+
if (providerClrType != null
874+
&& throwOnProviderClrTypeConflict)
875+
{
876+
throw new InvalidOperationException(
877+
CoreStrings.ConflictingRelationshipConversions(
878+
DeclaringType.DisplayName(), Name,
879+
providerClrType.ShortDisplayName(), annotationValue.GetType().ShortDisplayName()));
880+
}
881+
882+
valueConverter = annotationValue;
883883
}
884+
annotationFound = true;
885+
}
884886

885-
annotation = principalProperty.FindAnnotation(CoreAnnotationNames.ProviderClrType);
886-
if (annotation != null)
887+
var providerClrTypeAnnotation = principalProperty.FindAnnotation(CoreAnnotationNames.ProviderClrType);
888+
if (providerClrTypeAnnotation != null)
889+
{
890+
var annotationValue = (Type?)providerClrTypeAnnotation.Value;
891+
if (annotationValue != null)
887892
{
888-
return (Type?)annotation.Value;
893+
if (providerClrType != null)
894+
{
895+
throw new InvalidOperationException(
896+
CoreStrings.ConflictingRelationshipConversions(
897+
DeclaringType.DisplayName(), Name,
898+
providerClrType.ShortDisplayName(), annotationValue.ShortDisplayName()));
899+
}
900+
901+
if (valueConverter != null
902+
&& throwOnValueConverterConflict)
903+
{
904+
throw new InvalidOperationException(
905+
CoreStrings.ConflictingRelationshipConversions(
906+
DeclaringType.DisplayName(), Name,
907+
valueConverter.GetType().ShortDisplayName(), annotationValue.ShortDisplayName()));
908+
}
909+
910+
providerClrType = annotationValue;
889911
}
912+
annotationFound = true;
913+
}
890914

891-
nextProperty = principalProperty;
915+
if (!annotationFound)
916+
{
917+
if (cyclePosition == maxCycleLength - 1)
918+
{
919+
// We need to use different primes to ensure a different cycleBreakingProperty is selected
920+
// each time when traversing properties that participate in multiple relationship cycles
921+
queue.Enqueue((principalProperty, property, 0, HashHelpers.GetPrime(maxCycleLength << 1)));
922+
}
923+
else
924+
{
925+
queue.Enqueue((principalProperty, cycleBreakingProperty, cyclePosition + 1, maxCycleLength));
926+
}
892927
}
928+
break;
893929
}
894930
}
895-
896-
if (nextProperty == null)
897-
{
898-
break;
899-
}
900-
901-
property = nextProperty;
902931
}
903932

904-
return i == ForeignKey.LongestFkChainAllowedLength
905-
? throw new InvalidOperationException(
906-
CoreStrings.RelationshipCycle(
907-
DeclaringType.DisplayName(), Name, "ProviderClrType"))
908-
: null;
933+
return (valueConverter, providerClrType);
909934
}
910935

911936
/// <summary>
@@ -1376,7 +1401,7 @@ public virtual bool IsForeignKey()
13761401
/// doing so can result in application failures when updating to a new Entity Framework Core release.
13771402
/// </summary>
13781403
public virtual IEnumerable<ForeignKey> GetContainingForeignKeys()
1379-
=> ForeignKeys?.OrderBy(fk => fk.Properties, PropertyListComparer.Instance) ?? Enumerable.Empty<ForeignKey>();
1404+
=> ForeignKeys?.OrderBy(fk => fk, ForeignKeyComparer.Instance) ?? Enumerable.Empty<ForeignKey>();
13801405

13811406
/// <summary>
13821407
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to

src/EFCore/Metadata/RuntimeEntityType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ public virtual RuntimeForeignKey AddForeignKey(
212212
{
213213
if (property.ForeignKeys == null)
214214
{
215-
property.ForeignKeys = new List<RuntimeForeignKey> { foreignKey };
215+
property.ForeignKeys = new SortedSet<RuntimeForeignKey>(ForeignKeyComparer.Instance) { foreignKey };
216216
}
217217
else
218218
{

src/EFCore/Metadata/RuntimeProperty.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ private IEnumerable<RuntimeKey> GetContainingKeys()
207207
/// doing so can result in application failures when updating to a new Entity Framework Core release.
208208
/// </summary>
209209
[EntityFrameworkInternal]
210-
public virtual List<RuntimeForeignKey>? ForeignKeys { get; set; }
210+
public virtual ISet<RuntimeForeignKey>? ForeignKeys { get; set; }
211211

212212
private IEnumerable<RuntimeForeignKey> GetContainingForeignKeys()
213213
=> ForeignKeys ?? Enumerable.Empty<RuntimeForeignKey>();

src/EFCore/Properties/CoreStrings.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore/Properties/CoreStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,9 @@
342342
<data name="ConflictingPropertyOrNavigation" xml:space="preserve">
343343
<value>The property or navigation '{member}' cannot be added to the '{type}' type because a property or navigation with the same name already exists on the '{conflictingType}' type.</value>
344344
</data>
345+
<data name="ConflictingRelationshipConversions" xml:space="preserve">
346+
<value>The property '{entityType}.{property}' participates in several relationship chains that have conflicting conversions: '{valueConversion}' and '{conflictingValueConversion}'.</value>
347+
</data>
345348
<data name="ConflictingRelationshipNavigation" xml:space="preserve">
346349
<value>Cannot create a relationship between '{newPrincipalNavigationSpecification}' and '{newDependentNavigationSpecification}' because a relationship already exists between '{existingPrincipalNavigationSpecification}' and '{existingDependentNavigationSpecification}'. Navigations can only participate in a single relationship. If you want to override an existing relationship call 'Ignore' on the navigation '{newDependentNavigationSpecification}' first in 'OnModelCreating'.</value>
347350
</data>

test/EFCore.Specification.Tests/TestUtilities/ModelAsserter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,13 @@ public virtual bool AssertEqual(
665665
Assert.Equal(expected.DeclaringEntityType.Name, actual.DeclaringEntityType.Name);
666666
}
667667
},
668+
() =>
669+
{
670+
if (compareBackreferences)
671+
{
672+
Assert.Equal(expected.GetReferencingForeignKeys().ToList(), actual.GetReferencingForeignKeys(), ForeignKeyComparer.Instance);
673+
}
674+
},
668675
() => Assert.Equal(expectedAnnotations, actualAnnotations, TestAnnotationComparer.Instance));
669676

670677
return true;

0 commit comments

Comments
 (0)