Skip to content

Commit

Permalink
Create Join Entity automatically by RelationshipDiscoveryConvention a…
Browse files Browse the repository at this point in the history
…t convention-time and through HasMany().WithMany() for fluent API. All tests passing.
  • Loading branch information
lajones committed Jul 1, 2020
1 parent 4795eee commit fe3f06d
Show file tree
Hide file tree
Showing 25 changed files with 1,575 additions and 55 deletions.
9 changes: 9 additions & 0 deletions src/EFCore/Metadata/Builders/CollectionCollectionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ public virtual EntityTypeBuilder UsingEntity(
[NotNull] Func<EntityTypeBuilder, ReferenceCollectionBuilder> configureRight,
[NotNull] Func<EntityTypeBuilder, ReferenceCollectionBuilder> configureLeft)
{
var existingAssociationEntityType = (EntityType)
(LeftNavigation.ForeignKey?.DeclaringEntityType
?? RightNavigation.ForeignKey?.DeclaringEntityType);
if (existingAssociationEntityType != null)
{
ModelBuilder.RemoveAssociationEntityIfAutomaticallyCreated(
existingAssociationEntityType, false, ConfigurationSource.Explicit);
}

var entityTypeBuilder = new EntityTypeBuilder(
ModelBuilder.Entity(joinEntity, ConfigurationSource.Explicit).Metadata);

Expand Down
10 changes: 10 additions & 0 deletions src/EFCore/Metadata/Builders/CollectionCollectionBuilder`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata.Builders
{
Expand Down Expand Up @@ -50,6 +51,15 @@ public virtual EntityTypeBuilder<TAssociationEntity> UsingEntity<TAssociationEnt
[NotNull] Func<EntityTypeBuilder<TAssociationEntity>, ReferenceCollectionBuilder<TRightEntity, TAssociationEntity>> configureLeft)
where TAssociationEntity : class
{
var existingAssociationEntityType = (EntityType)
(LeftNavigation.ForeignKey?.DeclaringEntityType
?? RightNavigation.ForeignKey?.DeclaringEntityType);
if (existingAssociationEntityType != null)
{
ModelBuilder.RemoveAssociationEntityIfAutomaticallyCreated(
existingAssociationEntityType, false, ConfigurationSource.Explicit);
}

var entityTypeBuilder = new EntityTypeBuilder<TAssociationEntity>(
ModelBuilder.Entity(typeof(TAssociationEntity), ConfigurationSource.Explicit).Metadata);

Expand Down
3 changes: 2 additions & 1 deletion src/EFCore/Metadata/Conventions/BackingFieldConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ private FieldInfo GetFieldToSet(IConventionPropertyBase propertyBase)
{
if (propertyBase == null
|| !ConfigurationSource.Convention.Overrides(propertyBase.GetFieldInfoConfigurationSource())
|| propertyBase.IsIndexerProperty())
|| propertyBase.IsIndexerProperty()
|| (propertyBase.PropertyInfo == null && propertyBase.FieldInfo == null))
{
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public virtual void ProcessEntityTypeAdded(
&& t.FindDeclaredOwnership() == null
&& model.FindIsOwnedConfigurationSource(t.ClrType) == null
&& ((t.BaseType == null && clrType.IsAssignableFrom(t.ClrType))
|| (t.BaseType == entityType.BaseType && FindClosestBaseType(t) == entityType)))
|| (t.BaseType == entityType.BaseType && FindClosestBaseType(t) == entityType))
&& !t.HasSharedClrType)
.ToList();

foreach (var directlyDerivedType in directlyDerivedTypes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ public virtual ConventionSet CreateConventionSet()

conventionSet.NavigationRemovedConventions.Add(relationshipDiscoveryConvention);

conventionSet.SkipNavigationAddedConventions.Add(new ManyToManyConvention(Dependencies));

conventionSet.IndexAddedConventions.Add(foreignKeyIndexConvention);

conventionSet.IndexRemovedConventions.Add(foreignKeyIndexConvention);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,6 @@ public override IConventionSkipNavigationBuilder OnSkipNavigationAdded(
{
if (navigationBuilder.Metadata.Builder == null)
{
Check.DebugAssert(false, "null builder");
return null;
}

Expand Down
79 changes: 79 additions & 0 deletions src/EFCore/Metadata/Conventions/ManyToManyConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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.Linq;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
{
/// <summary>
/// A convention which looks for matching skip navigations and automatically creates
/// a many-to-many association entity with suitable foreign keys, sets the two
/// matching skip navigations to use those foreign keys and makes them inverses of
/// one another.
/// </summary>
public class ManyToManyConvention : ISkipNavigationAddedConvention
{
/// <summary>
/// Creates a new instance of <see cref="ManyToManyConvention" />.
/// </summary>
/// <param name="dependencies"> Parameter object containing dependencies for this convention. </param>
public ManyToManyConvention([NotNull] ProviderConventionSetBuilderDependencies dependencies)
{
Dependencies = dependencies;
}

/// <summary>
/// Parameter object containing service dependencies.
/// </summary>
protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; }

/// <summary>
/// Called after a skip navigation is added to an entity type.
/// </summary>
/// <param name="skipNavigationBuilder"> The builder for the skip navigation. </param>
/// <param name="context"> Additional information associated with convention execution. </param>
public virtual void ProcessSkipNavigationAdded(
IConventionSkipNavigationBuilder skipNavigationBuilder,
IConventionContext<IConventionSkipNavigationBuilder> context)
{
Check.NotNull(skipNavigationBuilder, "skipNavigationBuilder");
Check.NotNull(context, "context");

var skipNavigation = skipNavigationBuilder.Metadata;
if (skipNavigation.ForeignKey != null
|| skipNavigation.TargetEntityType == skipNavigation.DeclaringEntityType
|| !skipNavigation.IsCollection)
{
// do not create an automatic many-to-many association entity type
// for a self-referencing skip navigation, or for one that
// is already "in use" (i.e. has its Foreign Key assigned).
return;
}

var matchingSkipNavigation = skipNavigation.TargetEntityType
.GetSkipNavigations()
.FirstOrDefault(sn => sn.TargetEntityType == skipNavigation.DeclaringEntityType);

if (matchingSkipNavigation == null
|| matchingSkipNavigation.ForeignKey != null
|| !matchingSkipNavigation.IsCollection)
{
// do not create an automatic many-to-many association entity type if
// the matching skip navigation is already "in use" (i.e.
// has its Foreign Key assigned).
return;
}

var model = (Model)skipNavigation.DeclaringEntityType.Model;
model.Builder.AssociationEntity(
(SkipNavigation)skipNavigation,
(SkipNavigation)matchingSkipNavigation,
ConfigurationSource.Convention);
}
}
}
11 changes: 8 additions & 3 deletions src/EFCore/Metadata/Conventions/ModelCleanupConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ private IReadOnlyList<IConventionEntityType> GetRoots(IConventionModel model, Co

private void RemoveNavigationlessForeignKeys(IConventionModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
.Where(e => !((EntityType)e).IsAutomaticallyCreatedAssociationEntityType))
{
foreach (var foreignKey in entityType.GetDeclaredForeignKeys().ToList())
{
Expand Down Expand Up @@ -106,11 +107,15 @@ public GraphAdapter([NotNull] IConventionModel model)

public override IEnumerable<IConventionEntityType> GetOutgoingNeighbors(IConventionEntityType from)
=> from.GetForeignKeys().Where(fk => fk.DependentToPrincipal != null).Select(fk => fk.PrincipalEntityType)
.Union(from.GetReferencingForeignKeys().Where(fk => fk.PrincipalToDependent != null).Select(fk => fk.DeclaringEntityType));
.Union(from.GetReferencingForeignKeys().Where(fk => fk.PrincipalToDependent != null).Select(fk => fk.DeclaringEntityType))
.Union(from.GetSkipNavigations().Where(sn => sn.ForeignKey != null).Select(sn => sn.ForeignKey.DeclaringEntityType))
.Union(from.GetSkipNavigations().Where(sn => sn.TargetEntityType != null).Select(sn => sn.TargetEntityType));

public override IEnumerable<IConventionEntityType> GetIncomingNeighbors(IConventionEntityType to)
=> to.GetForeignKeys().Where(fk => fk.PrincipalToDependent != null).Select(fk => fk.PrincipalEntityType)
.Union(to.GetReferencingForeignKeys().Where(fk => fk.DependentToPrincipal != null).Select(fk => fk.DeclaringEntityType));
.Union(to.GetReferencingForeignKeys().Where(fk => fk.DependentToPrincipal != null).Select(fk => fk.DeclaringEntityType))
.Union(to.GetSkipNavigations().Where(sn => sn.ForeignKey != null).Select(sn => sn.ForeignKey.DeclaringEntityType))
.Union(to.GetSkipNavigations().Where(sn => sn.TargetEntityType != null).Select(sn => sn.TargetEntityType));

public override void Clear()
{
Expand Down
Loading

0 comments on commit fe3f06d

Please sign in to comment.