Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Rerun relationship discovery if an ambiguity has been resolved on the inverse #25263

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public override ConventionSet CreateConventionSet()
ReplaceConvention(conventionSet.EntityTypeBaseTypeChangedConventions, (DiscriminatorConvention)discriminatorConvention);
ReplaceConvention(conventionSet.EntityTypeBaseTypeChangedConventions, (KeyDiscoveryConvention)keyDiscoveryConvention);

ReplaceConvention(conventionSet.EntityTypeMemberIgnoredConventions, (KeyDiscoveryConvention)keyDiscoveryConvention);

conventionSet.EntityTypePrimaryKeyChangedConventions.Add(storeKeyConvention);

conventionSet.KeyAddedConventions.Add(storeKeyConvention);
Expand Down
1 change: 0 additions & 1 deletion src/EFCore.Relational/Metadata/Builders/TableBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.ComponentModel;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace Microsoft.EntityFrameworkCore.Metadata.Builders
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class RelationalCollectionShaperExpression : Expression, IPrintableExpres
/// <param name="innerShaper"> An expression used to create individual elements of the collection. </param>
/// <param name="navigation"> A navigation associated with this collection, if any. </param>
/// <param name="elementType"> The clr type of individual elements in the collection. </param>
[Obsolete("Use ctor which takes value comaprers.")]
[Obsolete("Use ctor which takes value comparers.")]
public RelationalCollectionShaperExpression(
int collectionId,
Expression parentIdentifier,
Expand Down
5 changes: 3 additions & 2 deletions src/EFCore/Diagnostics/CoreEventId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Logging;

// ReSharper disable UnusedMember.Local
namespace Microsoft.EntityFrameworkCore.Diagnostics
{
/// <summary>
Expand Down Expand Up @@ -686,7 +687,7 @@ private static EventId MakeModelValidationId(Id id)

/// <summary>
/// <para>
/// There navigation that <see cref="InversePropertyAttribute" /> points to is not the defining navigation.
/// The navigation that <see cref="InversePropertyAttribute" /> points to is not the defining navigation.
/// </para>
/// <para>
/// This event is in the <see cref="DbLoggerCategory.Model" /> category.
Expand All @@ -700,7 +701,7 @@ private static EventId MakeModelValidationId(Id id)

/// <summary>
/// <para>
/// There navigation that <see cref="InversePropertyAttribute" /> points to is not the defining navigation.
/// The navigation that <see cref="InversePropertyAttribute" /> points to is not the defining navigation.
/// </para>
/// <para>
/// This event is in the <see cref="DbLoggerCategory.Model" /> category.
Expand Down
71 changes: 71 additions & 0 deletions src/EFCore/Internal/TypeFullNameComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;

namespace Microsoft.EntityFrameworkCore.Internal
{
/// <summary>
/// <para>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </para>
/// </summary>
public sealed class TypeFullNameComparer : IComparer<Type>, IEqualityComparer<Type>
{
private TypeFullNameComparer()
{
}

/// <summary>
/// The singleton instance of the comparer to use.
/// </summary>
public static readonly TypeFullNameComparer Instance = new();

/// <summary>
/// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
/// </summary>
/// <param name="x"> The first object to compare. </param>
/// <param name="y"> The second object to compare. </param>
/// <returns> A negative number if 'x' is less than 'y'; a positive number if 'x' is greater than 'y'; zero otherwise. </returns>
public int Compare(Type? x, Type? y)
{
if (ReferenceEquals(x, y))
{
return 0;
}

if (x == null)
{
return -1;
}

if (y == null)
{
return 1;
}

return StringComparer.Ordinal.Compare(x.Name, y.Name);
AndriySvyryd marked this conversation as resolved.
Show resolved Hide resolved
AndriySvyryd marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Determines whether the specified objects are equal.
/// </summary>
/// <param name="x"> The first object to compare. </param>
/// <param name="y"> The second object to compare. </param>
/// <returns> <see langword="true" /> if the specified objects are equal; otherwise, <see langword="false" />. </returns>
public bool Equals(Type? x, Type? y)
=> Compare(x, y) == 0;

/// <summary>
/// Returns a hash code for the specified object.
/// </summary>
/// <param name="obj"> The for which a hash code is to be returned. </param>
/// <returns> A hash code for the specified object. </returns>
public int GetHashCode(Type obj)
=> obj.Name.GetHashCode();
}
}
6 changes: 6 additions & 0 deletions src/EFCore/Metadata/Conventions/ConventionSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ public class ConventionSet
public virtual IList<IForeignKeyAnnotationChangedConvention> ForeignKeyAnnotationChangedConventions { get; }
= new List<IForeignKeyAnnotationChangedConvention>();

/// <summary>
/// Conventions to run when a navigation is set to <see langword="null"/> on a foreign key.
/// </summary>
public virtual IList<IForeignKeyNullNavigationSetConvention> ForeignKeyNullNavigationSetConventions { get; }
= new List<IForeignKeyNullNavigationSetConvention>();

/// <summary>
/// Conventions to run when a navigation property is added.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
{
/// <summary>
/// Represents an operation that should be performed when a navigation is set to <see langword="null"/> on a foreign key.
/// </summary>
public interface IForeignKeyNullNavigationSetConvention : IConvention
{
/// <summary>
/// Called after a navigation is set to <see langword="null"/> on a foreign key.
/// </summary>
/// <param name="relationshipBuilder"> The builder for the foreign key. </param>
/// <param name="pointsToPrincipal">
/// A value indicating whether the <see langword="null"/> navigation would be pointing to the principal entity type.
/// </param>
/// <param name="context"> Additional information associated with convention execution. </param>
void ProcessForeignKeyNullNavigationSet(
IConventionForeignKeyBuilder relationshipBuilder,
bool pointsToPrincipal,
IConventionContext<IConventionNavigation> context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public virtual ConventionSet CreateConventionSet()

conventionSet.EntityTypeMemberIgnoredConventions.Add(inversePropertyAttributeConvention);
conventionSet.EntityTypeMemberIgnoredConventions.Add(relationshipDiscoveryConvention);
conventionSet.EntityTypeMemberIgnoredConventions.Add(keyDiscoveryConvention);
conventionSet.EntityTypeMemberIgnoredConventions.Add(foreignKeyPropertyDiscoveryConvention);

var keyAttributeConvention = new KeyAttributeConvention(Dependencies);
Expand Down Expand Up @@ -169,6 +170,8 @@ public virtual ConventionSet CreateConventionSet()
conventionSet.ForeignKeyOwnershipChangedConventions.Add(relationshipDiscoveryConvention);
conventionSet.ForeignKeyOwnershipChangedConventions.Add(valueGeneratorConvention);

conventionSet.ForeignKeyNullNavigationSetConventions.Add(relationshipDiscoveryConvention);

var requiredNavigationAttributeConvention = new RequiredNavigationAttributeConvention(Dependencies);
var nonNullableNavigationConvention = new NonNullableNavigationConvention(Dependencies);
conventionSet.NavigationAddedConventions.Add(new NavigationBackingFieldAttributeConvention(Dependencies));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ public int GetLeafCount()
public abstract bool? OnForeignKeyUniquenessChanged(
IConventionForeignKeyBuilder relationshipBuilder);

public abstract IConventionNavigation? OnForeignKeyNullNavigationSet(
IConventionForeignKeyBuilder relationshipBuilder,
bool pointsToPrincipal);

public abstract IConventionIndexBuilder? OnIndexAdded(IConventionIndexBuilder indexBuilder);

public abstract IConventionAnnotation? OnIndexAnnotationChanged(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ public override IConventionForeignKey OnForeignKeyRemoved(
return foreignKey;
}

public override IConventionNavigation? OnForeignKeyNullNavigationSet(
IConventionForeignKeyBuilder relationshipBuilder,
bool pointsToPrincipal)
{
Add(new OnForeignKeyNullNavigationSetNode(relationshipBuilder, pointsToPrincipal));
return null;
}

public override IConventionAnnotation? OnForeignKeyAnnotationChanged(
IConventionForeignKeyBuilder relationshipBuilder,
string name,
Expand Down Expand Up @@ -602,6 +610,21 @@ public override void Run(ConventionDispatcher dispatcher)
=> dispatcher._immediateConventionScope.OnForeignKeyOwnershipChanged(RelationshipBuilder);
}

private sealed class OnForeignKeyNullNavigationSetNode : ConventionNode
{
public OnForeignKeyNullNavigationSetNode(IConventionForeignKeyBuilder relationshipBuilder, bool pointsToPrincipal)
{
RelationshipBuilder = relationshipBuilder;
PointsToPrincipal = pointsToPrincipal;
}

public IConventionForeignKeyBuilder RelationshipBuilder { get; }
public bool PointsToPrincipal { get; }

public override void Run(ConventionDispatcher dispatcher)
=> dispatcher._immediateConventionScope.OnForeignKeyNullNavigationSet(RelationshipBuilder, PointsToPrincipal);
}

private sealed class OnForeignKeyPrincipalEndChangedNode : ConventionNode
{
public OnForeignKeyPrincipalEndChangedNode(IConventionForeignKeyBuilder relationshipBuilder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ private sealed class ImmediateConventionScope : ConventionScope
private readonly ConventionContext<IConventionSkipNavigationBuilder> _skipNavigationBuilderConventionContext;
private readonly ConventionContext<IConventionSkipNavigation> _skipNavigationConventionContext;
private readonly ConventionContext<IConventionNavigationBuilder> _navigationConventionBuilderContext;
private readonly ConventionContext<IConventionNavigation> _navigationConventionContext;
private readonly ConventionContext<IConventionIndexBuilder> _indexBuilderConventionContext;
private readonly ConventionContext<IConventionIndex> _indexConventionContext;
private readonly ConventionContext<IConventionKeyBuilder> _keyBuilderConventionContext;
Expand All @@ -46,6 +47,7 @@ public ImmediateConventionScope(ConventionSet conventionSet, ConventionDispatche
_skipNavigationBuilderConventionContext = new ConventionContext<IConventionSkipNavigationBuilder>(dispatcher);
_skipNavigationConventionContext = new ConventionContext<IConventionSkipNavigation>(dispatcher);
_navigationConventionBuilderContext = new ConventionContext<IConventionNavigationBuilder>(dispatcher);
_navigationConventionContext = new ConventionContext<IConventionNavigation>(dispatcher);
_indexBuilderConventionContext = new ConventionContext<IConventionIndexBuilder>(dispatcher);
_indexConventionContext = new ConventionContext<IConventionIndex>(dispatcher);
_keyBuilderConventionContext = new ConventionContext<IConventionKeyBuilder>(dispatcher);
Expand Down Expand Up @@ -617,6 +619,32 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB
return annotation;
}

public override IConventionNavigation? OnForeignKeyNullNavigationSet(
IConventionForeignKeyBuilder relationshipBuilder,
bool pointsToPrincipal)
{
using (_dispatcher.DelayConventions())
{
_navigationConventionContext.ResetState(null);
foreach (var foreignKeyConvention in _conventionSet.ForeignKeyNullNavigationSetConventions)
{
if (!relationshipBuilder.Metadata.IsInModel)
{
return null;
}

foreignKeyConvention.ProcessForeignKeyNullNavigationSet(
relationshipBuilder, pointsToPrincipal, _navigationConventionContext);
if (_navigationConventionContext.ShouldStopProcessing())
{
return _navigationConventionContext.Result;
}
}
}

return null;
}

public override IConventionNavigationBuilder? OnNavigationAdded(IConventionNavigationBuilder navigationBuilder)
{
if (!navigationBuilder.Metadata.IsInModel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,17 @@ public virtual IConventionModelBuilder OnModelFinalizing(IConventionModelBuilder
oldAnnotation);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual IConventionNavigation? OnForeignKeyNullNavigationSet(
IConventionForeignKeyBuilder relationshipBuilder,
bool pointsToPrincipal)
=> _scope.OnForeignKeyNullNavigationSet(relationshipBuilder, pointsToPrincipal);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
22 changes: 22 additions & 0 deletions src/EFCore/Metadata/Conventions/KeyDiscoveryConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class KeyDiscoveryConvention :
IPropertyAddedConvention,
IKeyRemovedConvention,
IEntityTypeBaseTypeChangedConvention,
IEntityTypeMemberIgnoredConvention,
IForeignKeyAddedConvention,
IForeignKeyRemovedConvention,
IForeignKeyPropertiesChangedConvention,
Expand Down Expand Up @@ -188,6 +189,27 @@ public static IEnumerable<IConventionProperty> DiscoverKeyProperties(
// ReSharper restore PossibleMultipleEnumeration
}

/// <summary>
/// Called after an entity type member is ignored.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type. </param>
/// <param name="name"> The name of the ignored member. </param>
/// <param name="context"> Additional information associated with convention execution. </param>
public virtual void ProcessEntityTypeMemberIgnored(
IConventionEntityTypeBuilder entityTypeBuilder,
string name,
IConventionContext<string> context)
{
var entityTypeName = entityTypeBuilder.Metadata.ShortName();
if (string.Equals(name, KeySuffix, StringComparison.OrdinalIgnoreCase)
|| (name.Length == entityTypeName.Length + KeySuffix.Length
&& name.StartsWith(entityTypeName, StringComparison.OrdinalIgnoreCase)
&& name.EndsWith(KeySuffix, StringComparison.OrdinalIgnoreCase)))
{
TryConfigurePrimaryKey(entityTypeBuilder);
}
}

/// <inheritdoc />
public virtual void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
Expand Down
Loading