Skip to content

Commit

Permalink
MemberEntry support for skip navigations
Browse files Browse the repository at this point in the history
Part of #19003
  • Loading branch information
ajcvickers committed Aug 7, 2020
1 parent 11bb751 commit 2ab96c9
Show file tree
Hide file tree
Showing 14 changed files with 979 additions and 130 deletions.
102 changes: 102 additions & 0 deletions src/EFCore/ChangeTracking/CollectionEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,108 @@ private void LocalDetectChanges()
[param: CanBeNull] set => base.CurrentValue = value;
}

/// <summary>
/// Gets or sets a value indicating whether any of foreign key property values associated
/// with this navigation property have been modified and should be updated in the database
/// when <see cref="DbContext.SaveChanges()" /> is called.
/// </summary>
public override bool IsModified
{
get
{
var stateManager = InternalEntry.StateManager;

if (Metadata is ISkipNavigation skipNavigation)
{
if (InternalEntry.EntityState != EntityState.Unchanged
&& InternalEntry.EntityState != EntityState.Detached)
{
return true;
}

var joinEntityType = skipNavigation.JoinEntityType;
var foreignKey = skipNavigation.ForeignKey;
foreach (var joinEntry in stateManager.Entries)
{
if (joinEntry.EntityType == joinEntityType
&& joinEntry.EntityState != EntityState.Unchanged
&& joinEntry.EntityState != EntityState.Detached
&& stateManager.FindPrincipal(joinEntry, foreignKey) == InternalEntry)
{
return true;
}
}
}

var navigationValue = CurrentValue;
if (navigationValue != null)
{
var targetEntityType = Metadata.TargetEntityType;
var foreignKey = Metadata is INavigation navigation
? navigation.ForeignKey
: ((ISkipNavigation)Metadata).ForeignKey;

foreach (var relatedEntity in navigationValue)
{
var relatedEntry = stateManager.TryGetEntry(relatedEntity, targetEntityType);

if (relatedEntry != null
&& (relatedEntry.EntityState == EntityState.Added
|| relatedEntry.EntityState == EntityState.Deleted
|| (foreignKey.Properties.Any(relatedEntry.IsModified))))
{
return true;
}
}
}

return false;
}
set
{
var stateManager = InternalEntry.StateManager;

if (Metadata is ISkipNavigation skipNavigation)
{
var joinEntityType = skipNavigation.JoinEntityType;
var foreignKey = skipNavigation.ForeignKey;
foreach (var joinEntry in stateManager.Entries.Where(
e => e.EntityType == joinEntityType
&& e.EntityState != EntityState.Detached
&& (value == (e.EntityState == EntityState.Unchanged))
&& stateManager.FindPrincipal(e, foreignKey) == InternalEntry)
.ToList())
{
joinEntry.SetEntityState(value ? EntityState.Modified : EntityState.Unchanged);
}
}
else
{
var foreignKey = ((INavigation)Metadata).ForeignKey;
var navigationValue = CurrentValue;
if (navigationValue != null)
{
foreach (var relatedEntity in navigationValue)
{
var relatedEntry = InternalEntry.StateManager.TryGetEntry(relatedEntity, Metadata.TargetEntityType);
if (relatedEntry != null)
{
var anyNonPk = foreignKey.Properties.Any(p => !p.IsPrimaryKey());
foreach (var property in foreignKey.Properties)
{
if (anyNonPk
&& !property.IsPrimaryKey())
{
relatedEntry.SetPropertyModified(property, isModified: value, acceptChanges: false);
}
}
}
}
}
}
}
}

/// <summary>
/// <para>
/// Loads the entities referenced by this navigation property, unless <see cref="NavigationEntry.IsLoaded" />
Expand Down
7 changes: 5 additions & 2 deletions src/EFCore/ChangeTracking/EntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ public virtual MemberEntry Member([NotNull] string propertyName)
return new PropertyEntry(InternalEntry, propertyName);
}

var navigation = InternalEntry.EntityType.FindNavigation(propertyName);
var navigation = (INavigationBase)InternalEntry.EntityType.FindNavigation(propertyName)
?? InternalEntry.EntityType.FindSkipNavigation(propertyName);
if (navigation != null)
{
return navigation.IsCollection
Expand Down Expand Up @@ -170,7 +171,9 @@ public virtual NavigationEntry Navigation([NotNull] string propertyName)
{
Check.NotEmpty(propertyName, nameof(propertyName));

var navigation = InternalEntry.EntityType.FindNavigation(propertyName);
var navigation = (INavigationBase)InternalEntry.EntityType.FindNavigation(propertyName)
?? InternalEntry.EntityType.FindSkipNavigation(propertyName);

if (navigation != null)
{
return navigation.IsCollection
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore/ChangeTracking/Internal/NavigationFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ private void InitialFixup(
try
{
_inFixup = false;
joinEntry.SetEntityState(EntityState.Added);
joinEntry.SetEntityState(setModified ? EntityState.Added : EntityState.Unchanged);
}
finally
{
Expand Down Expand Up @@ -893,7 +893,7 @@ private void DelayedFixup(
try
{
_inFixup = false;
joinEntry.SetEntityState(EntityState.Added);
joinEntry.SetEntityState(setModified ? EntityState.Added : EntityState.Unchanged);
}
finally
{
Expand Down
106 changes: 7 additions & 99 deletions src/EFCore/ChangeTracking/NavigationEntry.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;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -47,14 +46,16 @@ protected NavigationEntry([NotNull] InternalEntityEntry internalEntry, [NotNull]
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
protected NavigationEntry([NotNull] InternalEntityEntry internalEntry, [NotNull] INavigation navigation)
protected NavigationEntry([NotNull] InternalEntityEntry internalEntry, [NotNull] INavigationBase navigation)
: base(internalEntry, navigation)
{
}

private static INavigation GetNavigation(InternalEntityEntry internalEntry, string name, bool collection)
private static INavigationBase GetNavigation(InternalEntityEntry internalEntry, string name, bool collection)
{
var navigation = internalEntry.EntityType.FindNavigation(name);
var navigation = (INavigationBase)internalEntry.EntityType.FindNavigation(name)
?? internalEntry.EntityType.FindSkipNavigation(name);

if (navigation == null)
{
if (internalEntry.EntityType.FindProperty(name) != null)
Expand Down Expand Up @@ -177,103 +178,10 @@ public virtual bool IsLoaded
private IEntityFinder TargetFinder
=> InternalEntry.StateManager.CreateEntityFinder(Metadata.TargetEntityType);

/// <summary>
/// Gets or sets a value indicating whether any of foreign key property values associated
/// with this navigation property have been modified and should be updated in the database
/// when <see cref="DbContext.SaveChanges()" /> is called.
/// </summary>
public override bool IsModified
{
get
{
if (Metadata.IsOnDependent)
{
return AnyFkPropertiesModified(InternalEntry);
}

var navigationValue = CurrentValue;

return navigationValue != null
&& (Metadata.IsCollection
? ((IEnumerable)navigationValue).OfType<object>().Any(CollectionContainsNewOrChangedRelationships)
: AnyFkPropertiesModified(navigationValue));
}
set
{
if (Metadata.IsOnDependent)
{
SetFkPropertiesModified(InternalEntry, value);
}
else
{
var navigationValue = CurrentValue;
if (navigationValue != null)
{
if (Metadata.IsCollection)
{
foreach (var relatedEntity in (IEnumerable)navigationValue)
{
SetFkPropertiesModified(relatedEntity, value);
}
}
else
{
SetFkPropertiesModified(navigationValue, value);
}
}
}
}
}

private bool CollectionContainsNewOrChangedRelationships(object relatedEntity)
{
var relatedEntry = InternalEntry.StateManager.TryGetEntry(relatedEntity, Metadata.TargetEntityType);

return relatedEntry != null
&& (relatedEntry.EntityState == EntityState.Added
|| relatedEntry.EntityState == EntityState.Deleted
|| Metadata.ForeignKey.Properties.Any(relatedEntry.IsModified));
}

private bool AnyFkPropertiesModified(object relatedEntity)
{
var relatedEntry = InternalEntry.StateManager.TryGetEntry(relatedEntity, Metadata.TargetEntityType);

return relatedEntry != null
&& (relatedEntry.EntityState == EntityState.Added
|| relatedEntry.EntityState == EntityState.Deleted
|| Metadata.ForeignKey.Properties.Any(relatedEntry.IsModified));
}

private void SetFkPropertiesModified(object relatedEntity, bool modified)
{
var relatedEntry = InternalEntry.StateManager.TryGetEntry(relatedEntity, Metadata.TargetEntityType);
if (relatedEntry != null)
{
SetFkPropertiesModified(relatedEntry, modified);
}
}

private void SetFkPropertiesModified(InternalEntityEntry internalEntityEntry, bool modified)
{
var anyNonPk = Metadata.ForeignKey.Properties.Any(p => !p.IsPrimaryKey());
foreach (var property in Metadata.ForeignKey.Properties)
{
if (anyNonPk
&& !property.IsPrimaryKey())
{
internalEntityEntry.SetPropertyModified(property, isModified: modified, acceptChanges: false);
}
}
}

private bool AnyFkPropertiesModified(InternalEntityEntry internalEntityEntry)
=> Metadata.ForeignKey.Properties.Any(internalEntityEntry.IsModified);

/// <summary>
/// Gets the metadata that describes the facets of this property and how it maps to the database.
/// </summary>
public new virtual INavigation Metadata
=> (INavigation)base.Metadata;
public new virtual INavigationBase Metadata
=> (INavigationBase)base.Metadata;
}
}
80 changes: 79 additions & 1 deletion src/EFCore/ChangeTracking/ReferenceEntry.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// 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.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.ChangeTracking
{
Expand All @@ -33,6 +35,9 @@ public ReferenceEntry([NotNull] InternalEntityEntry internalEntry, [NotNull] str
: base(internalEntry, name, collection: false)
{
LocalDetectChanges();

// ReSharper disable once VirtualMemberCallInConstructor
Check.DebugAssert(Metadata is INavigation, "Issue #21673. Non-collection skip navigations not supported.");
}

/// <summary>
Expand All @@ -46,11 +51,15 @@ public ReferenceEntry([NotNull] InternalEntityEntry internalEntry, [NotNull] INa
: base(internalEntry, navigation)
{
LocalDetectChanges();

// ReSharper disable once VirtualMemberCallInConstructor
Check.DebugAssert(Metadata is INavigation, "Issue #21673. Non-collection skip navigations not supported.");
}

private void LocalDetectChanges()
{
if (!Metadata.IsOnDependent)
if (!(Metadata is INavigation navigation
&& navigation.IsOnDependent))
{
var target = GetTargetEntry();
if (target != null)
Expand All @@ -65,6 +74,75 @@ private void LocalDetectChanges()
}
}

/// <summary>
/// Gets or sets a value indicating whether any of foreign key property values associated
/// with this navigation property have been modified and should be updated in the database
/// when <see cref="DbContext.SaveChanges()" /> is called.
/// </summary>
public override bool IsModified
{
get
{
var navigation = (INavigation)Metadata;

return navigation.IsOnDependent
? navigation.ForeignKey.Properties.Any(InternalEntry.IsModified)
: AnyFkPropertiesModified(navigation, CurrentValue);
}
set
{
var navigation = (INavigation)Metadata;

if (navigation.IsOnDependent)
{
SetFkPropertiesModified(navigation, InternalEntry, value);
}
else
{
var navigationValue = CurrentValue;
if (navigationValue != null)
{
var relatedEntry = InternalEntry.StateManager.TryGetEntry(navigationValue, Metadata.TargetEntityType);
if (relatedEntry != null)
{
SetFkPropertiesModified(navigation, relatedEntry, value);
}
}
}
}
}

private void SetFkPropertiesModified(
INavigation navigation,
InternalEntityEntry internalEntityEntry,
bool modified)
{
var anyNonPk = navigation.ForeignKey.Properties.Any(p => !p.IsPrimaryKey());
foreach (var property in navigation.ForeignKey.Properties)
{
if (anyNonPk
&& !property.IsPrimaryKey())
{
internalEntityEntry.SetPropertyModified(property, isModified: modified, acceptChanges: false);
}
}
}

private bool AnyFkPropertiesModified(INavigation navigation, object relatedEntity)
{
if (relatedEntity == null)
{
return false;
}

var relatedEntry = InternalEntry.StateManager.TryGetEntry(relatedEntity, Metadata.TargetEntityType);

return relatedEntry != null
&& (relatedEntry.EntityState == EntityState.Added
|| relatedEntry.EntityState == EntityState.Deleted
|| navigation.ForeignKey.Properties.Any(relatedEntry.IsModified));
}

/// <summary>
/// The <see cref="EntityEntry" /> of the entity this navigation targets.
/// </summary>
Expand Down
Loading

0 comments on commit 2ab96c9

Please sign in to comment.