diff --git a/src/EFCore/ChangeTracking/CollectionEntry.cs b/src/EFCore/ChangeTracking/CollectionEntry.cs index 1a052f675c1..7bb46345ca2 100644 --- a/src/EFCore/ChangeTracking/CollectionEntry.cs +++ b/src/EFCore/ChangeTracking/CollectionEntry.cs @@ -82,6 +82,111 @@ private void LocalDetectChanges() [param: CanBeNull] set => base.CurrentValue = value; } + /// + /// 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 is called. + /// + 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; + var inverseForeignKey = skipNavigation.Inverse.ForeignKey; + foreach (var joinEntry in stateManager.Entries) + { + if (joinEntry.EntityType == joinEntityType + && stateManager.FindPrincipal(joinEntry, foreignKey) == InternalEntry + && (joinEntry.EntityState == EntityState.Added + || joinEntry.EntityState == EntityState.Deleted + || foreignKey.Properties.Any(joinEntry.IsModified) + || inverseForeignKey.Properties.Any(joinEntry.IsModified) + || (stateManager.FindPrincipal(joinEntry, inverseForeignKey)?.EntityState == EntityState.Deleted))) + { + return true; + } + } + } + else + { + var navigationValue = CurrentValue; + if (navigationValue != null) + { + var targetEntityType = Metadata.TargetEntityType; + var foreignKey = ((INavigation)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 + .GetEntriesForState(added: !value, modified: !value, deleted: !value, unchanged: value).Where( + e => e.EntityType == joinEntityType + && 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); + } + } + } + } + } + } + } + } + /// /// /// Loads the entities referenced by this navigation property, unless diff --git a/src/EFCore/ChangeTracking/EntityEntry.cs b/src/EFCore/ChangeTracking/EntityEntry.cs index e5657dbd160..8a7d3d6a801 100644 --- a/src/EFCore/ChangeTracking/EntityEntry.cs +++ b/src/EFCore/ChangeTracking/EntityEntry.cs @@ -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 @@ -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 diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index 5d078f7aaee..48f62f1c39a 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -847,7 +847,7 @@ private void InitialFixup( try { _inFixup = false; - joinEntry.SetEntityState(EntityState.Added); + joinEntry.SetEntityState(setModified ? EntityState.Added : EntityState.Unchanged); } finally { @@ -892,7 +892,7 @@ private void DelayedFixup( try { _inFixup = false; - joinEntry.SetEntityState(EntityState.Added); + joinEntry.SetEntityState(setModified ? EntityState.Added : EntityState.Unchanged); } finally { diff --git a/src/EFCore/ChangeTracking/NavigationEntry.cs b/src/EFCore/ChangeTracking/NavigationEntry.cs index 9a0dbe87c6f..6aa906e3403 100644 --- a/src/EFCore/ChangeTracking/NavigationEntry.cs +++ b/src/EFCore/ChangeTracking/NavigationEntry.cs @@ -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; @@ -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. /// [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) @@ -177,103 +178,10 @@ public virtual bool IsLoaded private IEntityFinder TargetFinder => InternalEntry.StateManager.CreateEntityFinder(Metadata.TargetEntityType); - /// - /// 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 is called. - /// - public override bool IsModified - { - get - { - if (Metadata.IsOnDependent) - { - return AnyFkPropertiesModified(InternalEntry); - } - - var navigationValue = CurrentValue; - - return navigationValue != null - && (Metadata.IsCollection - ? ((IEnumerable)navigationValue).OfType().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); - /// /// Gets the metadata that describes the facets of this property and how it maps to the database. /// - public new virtual INavigation Metadata - => (INavigation)base.Metadata; + public new virtual INavigationBase Metadata + => (INavigationBase)base.Metadata; } } diff --git a/src/EFCore/ChangeTracking/ReferenceEntry.cs b/src/EFCore/ChangeTracking/ReferenceEntry.cs index ac45c6ddb28..0ce542d8bd2 100644 --- a/src/EFCore/ChangeTracking/ReferenceEntry.cs +++ b/src/EFCore/ChangeTracking/ReferenceEntry.cs @@ -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 { @@ -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."); } /// @@ -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) @@ -65,6 +74,75 @@ private void LocalDetectChanges() } } + /// + /// 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 is called. + /// + 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)); + } + /// /// The of the entity this navigation targets. /// diff --git a/src/EFCore/Internal/EntityFinder.cs b/src/EFCore/Internal/EntityFinder.cs index 40d7c3ce3bd..fc66d8a1a41 100644 --- a/src/EFCore/Internal/EntityFinder.cs +++ b/src/EFCore/Internal/EntityFinder.cs @@ -126,7 +126,7 @@ ValueTask IEntityFinder.FindAsync(object[] keyValues, CancellationToken /// 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. /// - public virtual void Load(INavigation navigation, InternalEntityEntry entry) + public virtual void Load(INavigationBase navigation, InternalEntityEntry entry) { if (entry.EntityState == EntityState.Detached) { @@ -150,7 +150,7 @@ public virtual void Load(INavigation navigation, InternalEntityEntry entry) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual async Task LoadAsync( - INavigation navigation, + INavigationBase navigation, InternalEntityEntry entry, CancellationToken cancellationToken = default) { @@ -176,7 +176,7 @@ await Query(navigation, keyValues).LoadAsync(cancellationToken) /// 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. /// - public virtual IQueryable Query(INavigation navigation, InternalEntityEntry entry) + public virtual IQueryable Query(INavigationBase navigation, InternalEntityEntry entry) { if (entry.EntityState == EntityState.Detached) { @@ -236,7 +236,7 @@ private IQueryable GetDatabaseValuesQuery(InternalEntityEntry entry) .Select(BuildProjection(entityType)); } - private IQueryable Query(INavigation navigation, object[] keyValues) + private IQueryable Query(INavigationBase navigation, object[] keyValues) => _queryRoot.Where(BuildLambda(GetLoadProperties(navigation), new ValueBuffer(keyValues))).AsTracking(); /// @@ -245,14 +245,14 @@ private IQueryable Query(INavigation navigation, object[] keyValues) /// 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. /// - IQueryable IEntityFinder.Query(INavigation navigation, InternalEntityEntry entry) + IQueryable IEntityFinder.Query(INavigationBase navigation, InternalEntityEntry entry) => Query(navigation, entry); - private static object[] GetLoadValues(INavigation navigation, InternalEntityEntry entry) + private static object[] GetLoadValues(INavigationBase navigation, InternalEntityEntry entry) { - var properties = navigation.IsOnDependent - ? navigation.ForeignKey.Properties - : navigation.ForeignKey.PrincipalKey.Properties; + var properties = ((INavigation)navigation).IsOnDependent + ? ((INavigation)navigation).ForeignKey.Properties + : ((INavigation)navigation).ForeignKey.PrincipalKey.Properties; var values = new object[properties.Count]; @@ -270,10 +270,10 @@ private static object[] GetLoadValues(INavigation navigation, InternalEntityEntr return values; } - private static IReadOnlyList GetLoadProperties(INavigation navigation) - => navigation.IsOnDependent - ? navigation.ForeignKey.PrincipalKey.Properties - : navigation.ForeignKey.Properties; + private static IReadOnlyList GetLoadProperties(INavigationBase navigation) + => ((INavigation)navigation).IsOnDependent + ? ((INavigation)navigation).ForeignKey.PrincipalKey.Properties + : ((INavigation)navigation).ForeignKey.Properties; private TEntity FindTracked(object[] keyValues, out IReadOnlyList keyProperties) { diff --git a/src/EFCore/Internal/IEntityFinder.cs b/src/EFCore/Internal/IEntityFinder.cs index c0d3ae252de..fcb06c0e238 100644 --- a/src/EFCore/Internal/IEntityFinder.cs +++ b/src/EFCore/Internal/IEntityFinder.cs @@ -40,7 +40,7 @@ public interface IEntityFinder /// 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. /// - void Load([NotNull] INavigation navigation, [NotNull] InternalEntityEntry entry); + void Load([NotNull] INavigationBase navigation, [NotNull] InternalEntityEntry entry); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -49,7 +49,7 @@ public interface IEntityFinder /// doing so can result in application failures when updating to a new Entity Framework Core release. /// Task LoadAsync( - [NotNull] INavigation navigation, + [NotNull] INavigationBase navigation, [NotNull] InternalEntityEntry entry, CancellationToken cancellationToken = default); @@ -59,7 +59,7 @@ Task LoadAsync( /// 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. /// - IQueryable Query([NotNull] INavigation navigation, [NotNull] InternalEntityEntry entry); + IQueryable Query([NotNull] INavigationBase navigation, [NotNull] InternalEntityEntry entry); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Internal/IEntityFinder`.cs b/src/EFCore/Internal/IEntityFinder`.cs index 47ce5c27e6e..865760874be 100644 --- a/src/EFCore/Internal/IEntityFinder`.cs +++ b/src/EFCore/Internal/IEntityFinder`.cs @@ -41,6 +41,6 @@ public interface IEntityFinder : IEntityFinder /// 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. /// - new IQueryable Query([NotNull] INavigation navigation, [NotNull] InternalEntityEntry entry); + new IQueryable Query([NotNull] INavigationBase navigation, [NotNull] InternalEntityEntry entry); } } diff --git a/src/EFCore/Metadata/Internal/PropertyBase.cs b/src/EFCore/Metadata/Internal/PropertyBase.cs index f2740f03c4d..d00efb7c871 100644 --- a/src/EFCore/Metadata/Internal/PropertyBase.cs +++ b/src/EFCore/Metadata/Internal/PropertyBase.cs @@ -400,22 +400,27 @@ public static Expression CreateMemberAccess( if (property?.IsIndexerProperty() == true) { Expression expression = Expression.MakeIndex( - instanceExpression, (PropertyInfo)memberInfo, new List() { Expression.Constant(property.Name) }); + instanceExpression, (PropertyInfo)memberInfo, new List { Expression.Constant(property.Name) }); + + if (expression.Type != property.ClrType) + { + expression = Expression.Convert(expression, property.ClrType); + } if (property.DeclaringType.IsPropertyBag) { + var defaultValueConstant = property.ClrType.GetDefaultValueConstant(); + expression = Expression.Condition( - Expression.Call(instanceExpression, _containsKeyMethod, new List() { Expression.Constant(property.Name) }), + Expression.Call(instanceExpression, _containsKeyMethod, new List { Expression.Constant(property.Name) }), expression, - Expression.Convert(property.ClrType.GetDefaultValueConstant(), expression.Type)); + defaultValueConstant); } return expression; } - else - { - return Expression.MakeMemberAccess(instanceExpression, memberInfo); - } + + return Expression.MakeMemberAccess(instanceExpression, memberInfo); } /// diff --git a/src/EFCore/Update/UpdateEntryExtensions.cs b/src/EFCore/Update/UpdateEntryExtensions.cs index e8618452f4c..33cbad705c7 100644 --- a/src/EFCore/Update/UpdateEntryExtensions.cs +++ b/src/EFCore/Update/UpdateEntryExtensions.cs @@ -145,7 +145,8 @@ public static string ToDebugString( if ((options & ChangeTrackerDebugStringOptions.IncludeNavigations) != 0) { - foreach (var navigation in entry.EntityType.GetNavigations()) + foreach (var navigation in entry.EntityType.GetNavigations() + .Concat(entry.EntityType.GetSkipNavigations())) { builder.AppendLine().Append(indentString); diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index d07723b9288..cbd8a7b00c9 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -507,6 +507,7 @@ public static ConstantExpression GetDefaultValueConstant(this Type type) private static readonly MethodInfo _generateDefaultValueConstantMethod = typeof(SharedTypeExtensions).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant)); - private static ConstantExpression GenerateDefaultValueConstant() => Expression.Constant(default(TDefault)); + private static ConstantExpression GenerateDefaultValueConstant() + => Expression.Constant(default(TDefault), typeof(TDefault)); } } diff --git a/test/EFCore.Tests/ChangeTracking/SkipCollectionEntryTest.cs b/test/EFCore.Tests/ChangeTracking/SkipCollectionEntryTest.cs new file mode 100644 index 00000000000..372d786fa28 --- /dev/null +++ b/test/EFCore.Tests/ChangeTracking/SkipCollectionEntryTest.cs @@ -0,0 +1,583 @@ +// 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; +using System.Linq; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking +{ + public class SkipCollectionEntryTest + { + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_back_reference(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var entity = new Cherry(); + context.Add(entity); + + var entityEntry = context.Entry(entity); + Assert.Same(entityEntry.Entity, entityEntry.Collection("Chunkies").EntityEntry.Entity); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_back_reference_generic(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var entity = new Cherry(); + context.Add(entity); + + var entityEntry = context.Entry(entity); + Assert.Same(entityEntry.Entity, entityEntry.Collection(e => e.Chunkies).EntityEntry.Entity); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_metadata(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var entity = new Cherry(); + context.Add(entity); + + Assert.Equal("Chunkies", context.Entry(entity).Collection("Chunkies").Metadata.Name); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_metadata_generic(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var entity = new Cherry(); + context.Add(entity); + + Assert.Equal("Chunkies", context.Entry(entity).Collection(e => e.Chunkies).Metadata.Name); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_and_set_current_value(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry = new Cherry(); + var chunky = new Chunky(); + context.AddRange(chunky, cherry); + + var collection = context.Entry(cherry).Collection("Chunkies"); + var inverseCollection = context.Entry(chunky).Collection("Cherries"); + + Assert.Null(collection.CurrentValue); + + collection.CurrentValue = new List { chunky }; + + Assert.Same(chunky, cherry.Chunkies.Single()); + Assert.Same(cherry, chunky.Cherries.Single()); + Assert.Same(chunky, collection.CurrentValue.Cast().Single()); + Assert.Same(cherry, inverseCollection.CurrentValue.Cast().Single()); + Assert.Same(collection.FindEntry(chunky).GetInfrastructure(), context.Entry(chunky).GetInfrastructure()); + + collection.CurrentValue = null; + + Assert.Empty(chunky.Cherries); + Assert.Null(cherry.Chunkies); + Assert.Null(collection.CurrentValue); + Assert.Empty(inverseCollection.CurrentValue); + Assert.Null(collection.FindEntry(chunky)); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_and_set_current_value_generic(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry = new Cherry(); + var chunky = new Chunky(); + context.AddRange(chunky, cherry); + + var collection = context.Entry(cherry).Collection(e => e.Chunkies); + var inverseCollection = context.Entry(chunky).Collection(e => e.Cherries); + + Assert.Null(collection.CurrentValue); + + collection.CurrentValue = new List { chunky }; + + Assert.Same(chunky, cherry.Chunkies.Single()); + Assert.Same(cherry, chunky.Cherries.Single()); + Assert.Same(chunky, collection.CurrentValue.Single()); + Assert.Same(cherry, inverseCollection.CurrentValue.Single()); + Assert.Same(collection.FindEntry(chunky).GetInfrastructure(), context.Entry(chunky).GetInfrastructure()); + + collection.CurrentValue = null; + + Assert.Empty(chunky.Cherries); + Assert.Null(cherry.Chunkies); + Assert.Null(collection.CurrentValue); + Assert.Empty(inverseCollection.CurrentValue); + Assert.Null(collection.FindEntry(chunky)); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_and_set_current_value_not_tracked(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry = new Cherry(); + var chunky = new Chunky(); + + var collection = context.Entry(cherry).Collection("Chunkies"); + var inverseCollection = context.Entry(chunky).Collection("Cherries"); + + Assert.Null(collection.CurrentValue); + + collection.CurrentValue = new List { chunky }; + + Assert.Same(chunky, cherry.Chunkies.Single()); + Assert.Null(chunky.Cherries); + Assert.Same(chunky, collection.CurrentValue.Cast().Single()); + Assert.Null(inverseCollection.CurrentValue); + + collection.CurrentValue = null; + + Assert.Null(chunky.Cherries); + Assert.Null(cherry.Chunkies); + Assert.Null(collection.CurrentValue); + Assert.Null(inverseCollection.CurrentValue); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_and_set_current_value_generic_not_tracked(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry = new Cherry(); + var chunky = new Chunky(); + + var collection = context.Entry(cherry).Collection(e => e.Chunkies); + var inverseCollection = context.Entry(chunky).Collection(e => e.Cherries); + + Assert.Null(collection.CurrentValue); + + collection.CurrentValue = new List { chunky }; + + Assert.Same(chunky, cherry.Chunkies.Single()); + Assert.Null(chunky.Cherries); + Assert.Same(chunky, collection.CurrentValue.Single()); + Assert.Null(inverseCollection.CurrentValue); + + collection.CurrentValue = null; + + Assert.Null(chunky.Cherries); + Assert.Null(cherry.Chunkies); + Assert.Null(collection.CurrentValue); + Assert.Null(inverseCollection.CurrentValue); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_and_set_current_value_start_tracking(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry = new Cherry(); + var chunky = new Chunky(); + context.Add(cherry); + + var collection = context.Entry(cherry).Collection("Chunkies"); + var inverseCollection = context.Entry(chunky).Collection("Cherries"); + + Assert.Null(collection.CurrentValue); + + collection.CurrentValue = new List { chunky }; + + Assert.Same(chunky, cherry.Chunkies.Single()); + Assert.Same(cherry, chunky.Cherries.Single()); + Assert.Same(chunky, collection.CurrentValue.Cast().Single()); + Assert.Same(cherry, inverseCollection.CurrentValue.Cast().Single()); + + Assert.Equal(EntityState.Added, context.Entry(cherry).State); + Assert.Equal(EntityState.Added, context.Entry(chunky).State); + + collection.CurrentValue = null; + + Assert.Empty(chunky.Cherries); + Assert.Null(cherry.Chunkies); + Assert.Null(collection.CurrentValue); + Assert.Empty(inverseCollection.CurrentValue); + + Assert.Equal(EntityState.Added, context.Entry(cherry).State); + Assert.Equal(EntityState.Added, context.Entry(chunky).State); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_get_and_set_current_value_start_tracking_generic(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry = new Cherry(); + var chunky = new Chunky(); + context.Add(cherry); + + var collection = context.Entry(cherry).Collection(e => e.Chunkies); + var inverseCollection = context.Entry(chunky).Collection(e => e.Cherries); + + Assert.Null(collection.CurrentValue); + + collection.CurrentValue = new List { chunky }; + + Assert.Same(chunky, cherry.Chunkies.Single()); + Assert.Same(cherry, chunky.Cherries.Single()); + Assert.Same(chunky, collection.CurrentValue.Single()); + Assert.Same(cherry, inverseCollection.CurrentValue.Single()); + + Assert.Equal(EntityState.Added, context.Entry(cherry).State); + Assert.Equal(EntityState.Added, context.Entry(chunky).State); + + collection.CurrentValue = null; + + Assert.Empty(chunky.Cherries); + Assert.Null(cherry.Chunkies); + Assert.Null(collection.CurrentValue); + Assert.Empty(inverseCollection.CurrentValue); + + Assert.Equal(EntityState.Added, context.Entry(cherry).State); + Assert.Equal(EntityState.Added, context.Entry(chunky).State); + } + + [ConditionalTheory] + [InlineData(false, CascadeTiming.Immediate)] + [InlineData(true, CascadeTiming.Immediate)] + [InlineData(false, CascadeTiming.OnSaveChanges)] + [InlineData(true, CascadeTiming.OnSaveChanges)] + [InlineData(false, CascadeTiming.Never)] + [InlineData(true, CascadeTiming.Never)] + public void IsModified_tracks_detects_deletion_of_related_entity(bool useExplicitPk, CascadeTiming cascadeTiming) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + context.ChangeTracker.CascadeDeleteTiming = cascadeTiming; + + var cherry1 = new Cherry { Id = 1 }; + var cherry2 = new Cherry { Id = 2 }; + var chunky1 = new Chunky { Id = 1 }; + var chunky2 = new Chunky { Id = 2 }; + + AttachGraph(context, cherry1, cherry2, chunky1, chunky2); + + var relatedToCherry1 = context.Entry(cherry1).Collection(e => e.Chunkies); + var relatedToCherry2 = context.Entry(cherry2).Collection(e => e.Chunkies); + var relatedToChunky1 = context.Entry(chunky1).Collection(e => e.Cherries); + var relatedToChunky2 = context.Entry(chunky2).Collection(e => e.Cherries); + + Assert.False(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.False(relatedToChunky2.IsModified); + + context.Entry(chunky1).State = EntityState.Deleted; + + Assert.True(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.True(relatedToChunky1.IsModified); + Assert.False(relatedToChunky2.IsModified); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void IsModified_tracks_adding_new_related_entity(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry1 = new Cherry { Id = 1 }; + var cherry2 = new Cherry { Id = 2 }; + var chunky1 = new Chunky { Id = 1 }; + var chunky2 = new Chunky { Id = 2 }; + + AttachGraph(context, cherry1, cherry2, chunky1, chunky2); + + var relatedToCherry1 = context.Entry(cherry1).Collection(e => e.Chunkies); + var relatedToCherry2 = context.Entry(cherry2).Collection(e => e.Chunkies); + var relatedToChunky1 = context.Entry(chunky1).Collection(e => e.Cherries); + var relatedToChunky2 = context.Entry(chunky2).Collection(e => e.Cherries); + + var chunky3 = new Chunky { Id = 3 }; + cherry1.Chunkies.Add(chunky3); + context.ChangeTracker.DetectChanges(); + + Assert.True(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.False(relatedToChunky2.IsModified); + + context.Entry(chunky3).State = EntityState.Detached; + + Assert.False(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.False(relatedToChunky2.IsModified); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void IsModified_tracks_removing_items_from_the_join_table(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry1 = new Cherry { Id = 1 }; + var cherry2 = new Cherry { Id = 2 }; + var chunky1 = new Chunky { Id = 1 }; + var chunky2 = new Chunky { Id = 2 }; + + AttachGraph(context, cherry1, cherry2, chunky1, chunky2); + + var relatedToCherry1 = context.Entry(cherry1).Collection(e => e.Chunkies); + var relatedToCherry2 = context.Entry(cherry2).Collection(e => e.Chunkies); + var relatedToChunky1 = context.Entry(chunky1).Collection(e => e.Cherries); + var relatedToChunky2 = context.Entry(chunky2).Collection(e => e.Cherries); + + cherry1.Chunkies.Remove(chunky2); + context.ChangeTracker.DetectChanges(); + + Assert.True(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.True(relatedToChunky2.IsModified); + + cherry1.Chunkies.Add(chunky2); + context.ChangeTracker.DetectChanges(); + + Assert.True(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.True(relatedToChunky2.IsModified); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void IsModified_tracks_adding_items_to_the_join_table(bool useExplicitPk) + { + using var context = useExplicitPk ? new ExplicitFreezerContext() : new FreezerContext(); + + var cherry1 = new Cherry { Id = 1 }; + var cherry2 = new Cherry { Id = 2 }; + var chunky1 = new Chunky { Id = 1 }; + var chunky2 = new Chunky { Id = 2 }; + + AttachGraph(context, cherry1, cherry2, chunky1, chunky2); + + var relatedToCherry1 = context.Entry(cherry1).Collection(e => e.Chunkies); + var relatedToCherry2 = context.Entry(cherry2).Collection(e => e.Chunkies); + var relatedToChunky1 = context.Entry(chunky1).Collection(e => e.Cherries); + var relatedToChunky2 = context.Entry(chunky2).Collection(e => e.Cherries); + + cherry2.Chunkies.Add(chunky1); + context.ChangeTracker.DetectChanges(); + + Assert.False(relatedToCherry1.IsModified); + Assert.True(relatedToCherry2.IsModified); + Assert.True(relatedToChunky1.IsModified); + Assert.False(relatedToChunky2.IsModified); + } + + [ConditionalFact] + public void IsModified_tracks_mutation_of_join_fks() + { + using var context = new ExplicitFreezerContext(); + + var cherry1 = new Cherry { Id = 1 }; + var cherry2 = new Cherry { Id = 2 }; + var chunky1 = new Chunky { Id = 1 }; + var chunky2 = new Chunky { Id = 2 }; + + AttachGraph(context, cherry1, cherry2, chunky1, chunky2); + + var relatedToCherry1 = context.Entry(cherry1).Collection(e => e.Chunkies); + var relatedToCherry2 = context.Entry(cherry2).Collection(e => e.Chunkies); + var relatedToChunky1 = context.Entry(chunky1).Collection(e => e.Cherries); + var relatedToChunky2 = context.Entry(chunky2).Collection(e => e.Cherries); + + var joinEntity = context.ChangeTracker.Entries>() + .Single(e => e.Property("CherryId").CurrentValue == 1 && e.Property("ChunkyId").CurrentValue == 2) + .Entity; + + joinEntity["CherryId"] = 2; + context.ChangeTracker.DetectChanges(); + + Assert.False(relatedToCherry1.IsModified); + Assert.True(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.True(relatedToChunky2.IsModified); + + joinEntity["CherryId"] = 1; + context.ChangeTracker.DetectChanges(); + + Assert.True(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.True(relatedToChunky2.IsModified); + } + + [ConditionalFact] + public void Setting_IsModified_true_marks_all_join_table_FK_modified() + { + using var context = new ExplicitFreezerContext(); + + var cherry1 = new Cherry { Id = 1 }; + var cherry2 = new Cherry { Id = 2 }; + var chunky1 = new Chunky { Id = 1 }; + var chunky2 = new Chunky { Id = 2 }; + + AttachGraph(context, cherry1, cherry2, chunky1, chunky2); + + var relatedToCherry1 = context.Entry(cherry1).Collection(e => e.Chunkies); + var relatedToCherry2 = context.Entry(cherry2).Collection(e => e.Chunkies); + var relatedToChunky1 = context.Entry(chunky1).Collection(e => e.Cherries); + var relatedToChunky2 = context.Entry(chunky2).Collection(e => e.Cherries); + + Assert.False(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.False(relatedToChunky2.IsModified); + + foreach (var joinEntry in context.ChangeTracker.Entries>()) + { + Assert.Equal(EntityState.Unchanged, joinEntry.State); + } + + relatedToCherry1.IsModified = true; + + Assert.True(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.True(relatedToChunky1.IsModified); + Assert.True(relatedToChunky2.IsModified); + + foreach (var joinEntry in context.ChangeTracker.Entries>()) + { + Assert.Equal(EntityState.Modified, joinEntry.State); + } + } + + [ConditionalFact] + public void Setting_IsModified_false_reverts_changes_to_join_table_FKs() + { + using var context = new ExplicitFreezerContext(); + + var cherry1 = new Cherry { Id = 1 }; + var cherry2 = new Cherry { Id = 2 }; + var chunky1 = new Chunky { Id = 1 }; + var chunky2 = new Chunky { Id = 2 }; + + AttachGraph(context, cherry1, cherry2, chunky1, chunky2); + + var relatedToCherry1 = context.Entry(cherry1).Collection(e => e.Chunkies); + var relatedToCherry2 = context.Entry(cherry2).Collection(e => e.Chunkies); + var relatedToChunky1 = context.Entry(chunky1).Collection(e => e.Cherries); + var relatedToChunky2 = context.Entry(chunky2).Collection(e => e.Cherries); + + var joinEntity = context.ChangeTracker.Entries>() + .Single(e => e.Property("CherryId").CurrentValue == 1 && e.Property("ChunkyId").CurrentValue == 2) + .Entity; + + joinEntity["CherryId"] = 2; + context.ChangeTracker.DetectChanges(); + + Assert.False(relatedToCherry1.IsModified); + Assert.True(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.True(relatedToChunky2.IsModified); + + relatedToCherry2.IsModified = false; + + Assert.False(relatedToCherry1.IsModified); + Assert.False(relatedToCherry2.IsModified); + Assert.False(relatedToChunky1.IsModified); + Assert.False(relatedToChunky2.IsModified); + + foreach (var joinEntry in context.ChangeTracker.Entries>()) + { + Assert.Equal(EntityState.Unchanged, joinEntry.State); + } + } + + private static void AttachGraph(FreezerContext context, Cherry cherry1, Cherry cherry2, Chunky chunky1, Chunky chunky2) + { + cherry1.Chunkies = new List { chunky1, chunky2 }; + cherry2.Chunkies = new List(); + + if (context is ExplicitFreezerContext) + { + context.AddRange(cherry1, cherry2, chunky1, chunky2); // So that PKs get generated values + context.ChangeTracker.Entries().ToList().ForEach(e => e.State = EntityState.Unchanged); + } + else + { + context.AttachRange(cherry1, cherry2, chunky1, chunky2); + } + } + + private class Chunky + { + public int Id { get; set; } + public ICollection Cherries { get; set; } + } + + private class Cherry + { + public int Id { get; set; } + public ICollection Chunkies { get; set; } + } + + private class FreezerContext : DbContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase(nameof(FreezerContext)); + + public DbSet Icecream { get; set; } + } + + private class ExplicitFreezerContext : FreezerContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase(nameof(ExplicitFreezerContext)); + + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity().HasMany(e => e.Chunkies).WithMany(e => e.Cherries) + .UsingEntity>( + "CherryChunky", + b => b.HasOne().WithMany().HasForeignKey("ChunkyId"), + b => b.HasOne().WithMany().HasForeignKey("CherryId")) + .IndexerProperty("Id"); + } + } + } +} diff --git a/test/EFCore.Tests/ChangeTracking/SkipMemberEntryTest.cs b/test/EFCore.Tests/ChangeTracking/SkipMemberEntryTest.cs new file mode 100644 index 00000000000..c1584450841 --- /dev/null +++ b/test/EFCore.Tests/ChangeTracking/SkipMemberEntryTest.cs @@ -0,0 +1,86 @@ +// 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.Collections; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking +{ + public class SkipMemberEntryTest + { + [ConditionalFact] + public void Can_get_back_reference_collection() + { + using var context = new FreezerContext(); + var entity = new Cherry(); + context.Add(entity); + + var entityEntry = context.Entry(entity); + Assert.Same(entityEntry.Entity, entityEntry.Member("Chunkies").EntityEntry.Entity); + } + + [ConditionalFact] + public void Can_get_metadata_collection() + { + using var context = new FreezerContext(); + var entity = new Cherry(); + context.Add(entity); + + Assert.Equal("Chunkies", context.Entry(entity).Member("Chunkies").Metadata.Name); + } + + [ConditionalFact] + public void Can_get_and_set_current_value_collection() + { + using var context = new FreezerContext(); + + var cherry = new Cherry(); + var chunky = new Chunky(); + context.AddRange(chunky, cherry); + + var collection = context.Entry(cherry).Member("Chunkies"); + var inverseCollection = context.Entry(chunky).Member("Cherries"); + + Assert.Null(collection.CurrentValue); + Assert.Null(inverseCollection.CurrentValue); + + collection.CurrentValue = new List { chunky }; + + Assert.Same(cherry, chunky.Cherries.Single()); + Assert.Same(chunky, cherry.Chunkies.Single()); + Assert.Equal(cherry, ((ICollection)inverseCollection.CurrentValue).Single()); + Assert.Same(chunky, ((ICollection)collection.CurrentValue).Single()); + + collection.CurrentValue = null; + + Assert.Empty(chunky.Cherries); + Assert.Null(cherry.Chunkies); + Assert.Empty((IEnumerable)inverseCollection.CurrentValue); + Assert.Null(collection.CurrentValue); + } + + private class Chunky + { + public int Id { get; set; } + public ICollection Cherries { get; set; } + } + + private class Cherry + { + public int Id { get; set; } + public ICollection Chunkies { get; set; } + } + + private class FreezerContext : DbContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase(nameof(FreezerContext)); + + public DbSet Icecream { get; set; } + } + } +} diff --git a/test/EFCore.Tests/ChangeTracking/SkipNavigationEntryTest.cs b/test/EFCore.Tests/ChangeTracking/SkipNavigationEntryTest.cs new file mode 100644 index 00000000000..6fe614e98d0 --- /dev/null +++ b/test/EFCore.Tests/ChangeTracking/SkipNavigationEntryTest.cs @@ -0,0 +1,88 @@ +// 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.Collections; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking +{ + public class SkipNavigationEntryTest + { + [ConditionalFact] + public void Can_get_back_reference_collection() + { + using var context = new FreezerContext(); + + var entity = new Cherry(); + context.Add(entity); + + var entityEntry = context.Entry(entity); + Assert.Same(entityEntry.Entity, entityEntry.Navigation("Chunkies").EntityEntry.Entity); + } + + [ConditionalFact] + public void Can_get_metadata_collection() + { + using var context = new FreezerContext(); + + var entity = new Cherry(); + context.Add(entity); + + Assert.Equal("Chunkies", context.Entry(entity).Navigation("Chunkies").Metadata.Name); + } + + [ConditionalFact] + public void Can_get_and_set_current_value_collection() + { + using var context = new FreezerContext(); + + var cherry = new Cherry(); + var chunky = new Chunky(); + context.AddRange(chunky, cherry); + + var collection = context.Entry(cherry).Navigation("Chunkies"); + var inverseCollection = context.Entry(chunky).Navigation("Cherries"); + + Assert.Null(collection.CurrentValue); + Assert.Null(inverseCollection.CurrentValue); + + collection.CurrentValue = new List { chunky }; + + Assert.Same(cherry, chunky.Cherries.Single()); + Assert.Same(chunky, cherry.Chunkies.Single()); + Assert.Equal(cherry, ((ICollection)inverseCollection.CurrentValue).Single()); + Assert.Same(chunky, ((ICollection)collection.CurrentValue).Single()); + + collection.CurrentValue = null; + + Assert.Empty(chunky.Cherries); + Assert.Null(cherry.Chunkies); + Assert.Empty((IEnumerable)inverseCollection.CurrentValue); + Assert.Null(collection.CurrentValue); + } + + private class Chunky + { + public int Id { get; set; } + public ICollection Cherries { get; set; } + } + + private class Cherry + { + public int Id { get; set; } + public ICollection Chunkies { get; set; } + } + + private class FreezerContext : DbContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase(nameof(FreezerContext)); + + public DbSet Icecream { get; set; } + } + } +}